package be.iminds.ilabt.jfed.experimenter_gui.ui;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.IndexRange;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;

import javax.annotation.Nonnull;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.regex.Pattern;

/*
 Taken from stackoverflow
 http://stackoverflow.com/questions/17188712/text-field-validation-in-javafx
 */
public class LocalTimeTextField extends TextField {

    private static final String DEFAULT_WITH_SECONDS = "00:00:00";
    private static final String DEFAULT_WITHOUT_SECONDS = "00:00";

    private static final Pattern TIME_WITH_SECONDS_PATTERN = Pattern.compile("\\d\\d:\\d\\d:\\d\\d");
    private static final Pattern TIME_WITHOUT_SECONDS_PATTERN = Pattern.compile("\\d\\d:\\d\\d");

    private static final DateTimeFormatter FORMAT_WITH_SECONDS_PATTERN = DateTimeFormatter.ofPattern("HH:mm:ss");
    private static final DateTimeFormatter FORMAT_WITHOUT_SECONDS_PATTERN = DateTimeFormatter.ofPattern("HH:mm");

    private final ObjectProperty<LocalTime> time = new SimpleObjectProperty<>(LocalTime.of(0, 0, 0));
    private final BooleanProperty showSeconds = new SimpleBooleanProperty(true);

    private Pattern timePattern = TIME_WITH_SECONDS_PATTERN;

    {
        showSeconds.addListener(observable -> {
            if (showSeconds.get()) {
                timePattern = TIME_WITH_SECONDS_PATTERN;
            } else {
                timePattern = TIME_WITHOUT_SECONDS_PATTERN;
            }
            setText(getText());//force update
        });

        textProperty().bindBidirectional(timeProperty(), new StringConverter<LocalTime>() {
            @Override
            public String toString(LocalTime object) {
                if (showSeconds.get()) {
                    return object.format(FORMAT_WITH_SECONDS_PATTERN);
                } else {
                    return object.format(FORMAT_WITHOUT_SECONDS_PATTERN);
                }
            }

            @Override
            public LocalTime fromString(String string) {
                if (showSeconds.get()) {
                    return LocalTime.parse(string, FORMAT_WITH_SECONDS_PATTERN);
                } else {
                    return LocalTime.parse(string, FORMAT_WITHOUT_SECONDS_PATTERN);
                }
            }
        });

        setAlignment(Pos.CENTER);
    }


    public LocalTimeTextField() {
        this(true);
    }

    public LocalTimeTextField(boolean showSeconds) {
        super(showSeconds ? DEFAULT_WITH_SECONDS : DEFAULT_WITHOUT_SECONDS);
        this.showSeconds.set(showSeconds);
    }

    public LocalTimeTextField(@Nonnull LocalTime time, boolean showSeconds) {
        super(timeToString(time, showSeconds));
        this.time.set(time);
        this.showSeconds.set(showSeconds);

    }

    private static String timeToString(LocalTime time, boolean showSeconds) {
        if (showSeconds) {
            return time.format(FORMAT_WITH_SECONDS_PATTERN);
        } else {
            return time.format(FORMAT_WITHOUT_SECONDS_PATTERN);
        }
    }

    public void setTime(LocalTime time) {
        this.time.set(time);
        super.setText(timeToString(time, showSeconds.get()));
    }

    public LocalTime getTime() {
        return time.get();
    }

    public ObjectProperty<LocalTime> timeProperty() {
        return time;
    }

    public boolean getShowSeconds() {
        return showSeconds.get();
    }

    public BooleanProperty showSecondsProperty() {
        return showSeconds;
    }

    public void setShowSeconds(boolean showSeconds) {
        this.showSeconds.set(showSeconds);
    }

    @Override
    public void appendText(String text) {
        // Ignore this. Our text is always 8 characters long, we cannot append anything  
    }

    @Override
    public boolean deleteNextChar() {
        boolean success = false;

        // If there's a selection, delete it:  
        final IndexRange selection = getSelection();
        if (selection.getLength() > 0) {
            int selectionEnd = selection.getEnd();
            this.deleteText(selection);
            this.positionCaret(selectionEnd);
            skipSeparatorCharactersBackward();
            success = true;
        } else {
            // If the caret preceeds a digit, replace that digit with a zero and move the caret forward. Else just move the caret forward.  
            int caret = this.getCaretPosition();
            if (caret % 3 != 2) { // not preceeding a colon  
                String currentText = this.getText();
                setText(currentText.substring(0, caret) + "0" + currentText.substring(caret + 1));
                success = true;
            }
            this.positionCaret(Math.min(caret + 1, this.getText().length()));
            skipSeparatorCharactersBackward();
        }
        return success;
    }

    @Override
    public boolean deletePreviousChar() {
        boolean success = false;
        // If there's a selection, delete it:  
        final IndexRange selection = getSelection();
        if (selection.getLength() > 0) {
            int selectionStart = selection.getStart();
            this.deleteText(selection);
            this.positionCaret(selectionStart);
            skipSeparatorCharactersBackward();
            success = true;
        } else {
            // If the caret is after a digit, replace that digit with a zero and move the caret backward. Else just move the caret back.  
            int caret = this.getCaretPosition();
            if (caret % 3 != 0) { // not following a colon  
                String currentText = this.getText();
                setText(currentText.substring(0, caret - 1) + "0" + currentText.substring(caret));
                success = true;
            }
            this.positionCaret(Math.max(caret - 1, 0));
            skipSeparatorCharactersBackward();
        }
        return success;
    }

    @Override
    public void deleteText(IndexRange range) {
        this.deleteText(range.getStart(), range.getEnd());
    }

    @Override
    public void deleteText(int begin, int end) {
        // Replace all digits in the given range with zero:  
        StringBuilder builder = new StringBuilder(this.getText());
        for (int c = begin; c < end; c++) {
            if (c % 3 != 2) { // Not at a colon:  
                builder.replace(c, c + 1, "0");
            }
        }
        this.setText(builder.toString());
    }

    @Override
    public void insertText(int index, String text) {
        // Handle an insert by replacing the range from index to index+text.length() with text, if that results in a valid string:  
        StringBuilder builder = new StringBuilder(this.getText());
        builder.replace(index, index + text.length(), text);
        final String testText = builder.toString();
        if (validate(testText)) {
            this.setText(testText);
            this.positionCaret(index + text.length());
            skipSeparatorCharactersForward();
        }

    }

    @Override
    public void replaceSelection(String replacement) {
        final IndexRange selection = this.getSelection();
        if (selection.getLength() == 0) {
            this.insertText(selection.getStart(), replacement);
        } else {
            this.replaceText(selection.getStart(), selection.getEnd(), replacement);
        }
    }

    @Override
    public void replaceText(IndexRange range, String text) {
        this.replaceText(range.getStart(), range.getEnd(), text);
    }

    @Override
    public void replaceText(int begin, int end, String text) {
        if (begin == end) {
            this.insertText(begin, text);
        } else {
            // only handle this if text.length() is equal to the number of characters being replaced, and if the replacement results in a valid string:  
            if (text.length() == end - begin) {
                StringBuilder builder = new StringBuilder(this.getText());
                builder.replace(begin, end, text);
                String testText = builder.toString();
                if (validate(testText)) {
                    this.setText(testText);
                    this.positionCaret(end);
                    skipSeparatorCharactersForward();
                }

            }
        }
    }

    private boolean validate(String time) {
        if (!timePattern.matcher(time).matches()) {
            return false;
        }
        String[] tokens = time.split(":");

//        if (showSeconds.get())
//            assert tokens.length >= 2;
//        else
//            assert tokens.length == 3;

        try {
            int hours = Integer.parseInt(tokens[0]);
            if (hours < 0 || hours > 23) {
                return false;
            }

            int mins = Integer.parseInt(tokens[1]);
            if (mins < 0 || mins > 59) {
                return false;
            }

            if (showSeconds.get()) {
                int secs = Integer.parseInt(tokens[2]);
                if (secs < 0 || secs > 59) {
                    return false;
                }
            }
            return true;
        } catch (NumberFormatException nfe) {
            // regex matching should assure we never reach this catch block  
            assert false;
            return false;
        }
    }

    private void skipSeparatorCharactersForward(){
            int pos = getCaretPosition();

            if (pos < getText().length() && Objects.equals(getText().substring(pos, pos + 1), ":")) {
                positionCaret(pos + 1);
            }
    }

    private void skipSeparatorCharactersBackward(){
        int pos = getCaretPosition();

        if (pos > 0 && Objects.equals(getText().substring(pos - 1, pos), ":")) {
            positionCaret(pos -1);
        }
    }
}
