/**
 * Copyright (c) 2015 Bosch Software Innovations GmbH and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.eclipse.hawkbit.ui.tenantconfiguration.polling;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import javax.validation.constraints.NotNull;

import com.vaadin.data.Property;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.util.converter.Converter.ConversionException;
import com.vaadin.shared.ui.datefield.Resolution;
import com.vaadin.ui.DateField;
import com.vaadin.ui.themes.ValoTheme;

/**
 * This class represents a Field which is optimized to enter a time duration in
 * form HH:mm:ss (see {@link #DURATION_FORMAT_STIRNG}). It uses the vaadin
 * DateField as a basic element, but the format is optimized for the duration
 * input. For a correct view of the popup it is recommended not to display the
 * css-class "v-datefield-calendarpanel-header" and
 * "v-datefield-calendarpanel-body" (see systemconfig.scss}
 */
public class DurationField extends DateField {

    private static final long serialVersionUID = 1L;

    private static final String CSS_STYLE_NAME = "durationfield";

    private static final String ADDITIONAL_DURATION_STRING = "HHmmss";
    private static final String DURATION_FORMAT_STIRNG = "HH:mm:ss";

    private static final ZoneId ZONEID_UTC = ZoneId.of("+0");

    private static final Duration MAXIMUM_DURATION = Duration.ofHours(23).plusMinutes(59).plusSeconds(59);

    private final SimpleDateFormat durationFormat = new SimpleDateFormat(DURATION_FORMAT_STIRNG);
    private final SimpleDateFormat additionalFormat = new SimpleDateFormat(ADDITIONAL_DURATION_STRING);

    private Date minimumDuration;
    private Date maximumDuration;

    /**
     * Creates a DurationField
     */
    protected DurationField() {
        this.setTimeZone(TimeZone.getTimeZone(ZONEID_UTC));
        durationFormat.setTimeZone(TimeZone.getTimeZone(ZONEID_UTC));
        additionalFormat.setTimeZone(TimeZone.getTimeZone(ZONEID_UTC));
        durationFormat.setLenient(false);
        additionalFormat.setLenient(false);

        this.setResolution(Resolution.SECOND);
        this.setDateFormat(DURATION_FORMAT_STIRNG);
        this.addStyleName(CSS_STYLE_NAME);
        this.addStyleName(ValoTheme.TEXTFIELD_TINY);
        this.setWidth("100px");

        // needed that popup shows a 24h clock
        this.setLocale(Locale.GERMANY);

        this.addValueChangeListener(this);
    }

    /**
     * This method is called to handle a non-empty date string from the client
     * if the client could not parse it as a Date. In the current case two
     * different parsing schemas are tried. If parsing is not possible a
     * ConversionException is thrown which marks the DurationField as invalid.
     */
    @Override
    protected Date handleUnparsableDateString(final String value) throws ConversionException {

        try {
            return durationFormat.parse(value);

        } catch (final ParseException e1) {
            try {

                return additionalFormat.parse("000000".substring(value.length() <= 6 ? value.length() : 6) + value);
            } catch (final ParseException e2) {
                // if Parsing is not possible ConversionException is thrown
            }
        }
        throw new ConversionException("input is not in HH:MM:SS format.");
    }

    @Override
    public void valueChange(final Property.ValueChangeEvent event) {
        // do not delete this method, even when removing the code inside this
        // method. This method overwrites the super method, which is
        // necessary, that parsing works correctly on pressing enter key

        if (!(event.getProperty() instanceof DurationField)) {
            return;
        }
        final Date value = (Date) event.getProperty().getValue();

        // setValue() calls valueChanged again, when the minimum is greater
        // than the maximum this can lead to an endless loop
        if (value != null && minimumDuration != null && maximumDuration != null
                && minimumDuration.before(maximumDuration)) {

            if (compareTimeOfDates(value, maximumDuration) > 0) {
                ((DateField) event.getProperty()).setValue(maximumDuration);
            }

            if (compareTimeOfDates(minimumDuration, value) > 0) {
                ((DateField) event.getProperty()).setValue(minimumDuration);
            }
        }
    }

    @Override
    public void validate(final Date value) throws InvalidValueException {
        super.validate(value);

        if (value != null && maximumDuration != null && compareTimeOfDates(value, maximumDuration) > 0) {
            throw new InvalidValueException("value is greater than the allowed maximum value");
        }

        if (value != null && minimumDuration != null && compareTimeOfDates(minimumDuration, value) > 0) {
            throw new InvalidValueException("value is smaller than the allowed minimum value");
        }
    }

    /**
     * Sets the duration value
     * 
     * @param duration
     *            duration, only values less then 23:59:59 are excepted
     */
    public void setDuration(@NotNull final Duration duration) {
        if (duration.compareTo(MAXIMUM_DURATION) > 0) {
            throw new IllegalArgumentException("The duaration has to be smaller than 23:59:59.");
        }
        super.setValue(durationToDate(duration));
    }

    /**
     * Gets the duration value of the TextField
     * 
     * @return duration which is written in the vaadin Field
     */
    public Duration getDuration() {
        if (this.getValue() == null) {
            return null;
        }
        return dateToDuration(this.getValue());
    }

    /**
     * Sets the minimal allowed duration value as a String
     * 
     * @param minimumDuration
     *            minimum Duration, only values smaller 23:59:59 are excepted
     */
    public void setMinimumDuration(@NotNull final Duration minimumDuration) {
        if (minimumDuration.compareTo(MAXIMUM_DURATION) > 0) {
            throw new IllegalArgumentException("The minimum duaration has to be smaller than 23:59:59.");
        }
        this.minimumDuration = durationToDate(minimumDuration);
    }

    /**
     * Sets the maximum allowed duration value as a String
     * 
     * @param maximumDuration
     *            maximumDuration, only values smaller 23:59:59 are excepted
     */
    public void setMaximumDuration(@NotNull final Duration maximumDuration) {
        if (maximumDuration.compareTo(MAXIMUM_DURATION) > 0) {
            throw new IllegalArgumentException("The maximum duaration has to be smaller than 23:59:59.");
        }
        this.maximumDuration = durationToDate(maximumDuration);
    }

    private static Date durationToDate(final Duration duration) {
        if (duration.compareTo(MAXIMUM_DURATION) > 0) {
            throw new IllegalArgumentException("The duaration has to be smaller than 23:59:59.");
        }

        final LocalTime lt = LocalTime.ofNanoOfDay(duration.toNanos());
        return Date.from(lt.atDate(LocalDate.now(ZONEID_UTC)).atZone(ZONEID_UTC).toInstant());
    }

    private static Duration dateToDuration(final Date date) {
        final LocalTime endExclusive = LocalDateTime.ofInstant(date.toInstant(), ZONEID_UTC).toLocalTime();
        return Duration.between(LocalTime.MIDNIGHT, LocalTime.from(endExclusive));
    }

    /**
     * Because parsing done by base class returns a different date than parsing
     * done by the user or converting duration to a date. But for the
     * DurationField comparison only the time is important. This function helps
     * comparing the time and ignores the values for day, month and year.
     * 
     * @param d1
     *            date, which time will compared with the time of d2
     * @param d2
     *            date, which time will compared with the time of d1
     * @return the value 0 if the time represented d1 is equal to the time
     *         represented by d2; a value less than 0 if the time of d1 is
     *         before the time of d2; and a value greater than 0 if the time of
     *         d1 is after the time represented by d2.
     */
    private int compareTimeOfDates(final Date d1, final Date d2) {
        final LocalTime lt1 = LocalDateTime.ofInstant(d1.toInstant(), ZONEID_UTC).toLocalTime();
        final LocalTime lt2 = LocalDateTime.ofInstant(d2.toInstant(), ZONEID_UTC).toLocalTime();

        return lt1.compareTo(lt2);
    }
}
