package ctbrec.ui;
import java.util.regex.Pattern;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TimeTextFieldTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        VBox root = new VBox(5);
        root.setPadding(new Insets(5));
        Label hrLabel = new Label();
        Label minLabel = new Label();
        Label secLabel = new Label();
        TimeTextField timeTextField = new TimeTextField();
        hrLabel.textProperty().bind(Bindings.format("Hours: %d", timeTextField.hoursProperty()));
        minLabel.textProperty().bind(Bindings.format("Minutes: %d", timeTextField.minutesProperty()));
        secLabel.textProperty().bind(Bindings.format("Seconds: %d", timeTextField.secondsProperty()));

        root.getChildren().addAll(timeTextField, hrLabel, minLabel, secLabel);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);

    }

    public static class TimeTextField extends TextField {

        enum Unit {
            HOURS, MINUTES, SECONDS
        };

        private final Pattern timePattern;
        private final ReadOnlyIntegerWrapper hours;
        private final ReadOnlyIntegerWrapper minutes;
        private final ReadOnlyIntegerWrapper seconds;

        public TimeTextField() {
            this("00:00:00");
        }

        public TimeTextField(String time) {
            super(time);
            timePattern = Pattern.compile("\\d\\d:\\d\\d:\\d\\d");
            if (!validate(time)) {
                throw new IllegalArgumentException("Invalid time: " + time);
            }
            hours = new ReadOnlyIntegerWrapper(this, "hours");
            minutes = new ReadOnlyIntegerWrapper(this, "minutes");
            seconds = new ReadOnlyIntegerWrapper(this, "seconds");
            hours.bind(new TimeTextField.TimeUnitBinding(Unit.HOURS));
            minutes.bind(new TimeTextField.TimeUnitBinding(Unit.MINUTES));
            seconds.bind(new TimeTextField.TimeUnitBinding(Unit.SECONDS));
        }

        public ReadOnlyIntegerProperty hoursProperty() {
            return hours.getReadOnlyProperty();
        }

        public int getHours() {
            return hours.get();
        }

        public ReadOnlyIntegerProperty minutesProperty() {
            return minutes.getReadOnlyProperty();
        }

        public int getMinutes() {
            return minutes.get();
        }

        public ReadOnlyIntegerProperty secondsProperty() {
            return seconds.getReadOnlyProperty();
        }

        public int getSeconds() {
            return seconds.get();
        }

        @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);
                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()));
            }
            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);
                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));
            }
            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());
        }

        @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);
                }
            }
        }

        private boolean validate(String time) {
            if (!timePattern.matcher(time).matches()) {
                return false;
            }
            String[] tokens = time.split(":");
            assert tokens.length == 3;
            try {
                int hours = Integer.parseInt(tokens[0]);
                int mins = Integer.parseInt(tokens[1]);
                int secs = Integer.parseInt(tokens[2]);
                if (hours < 0 || hours > 23) {
                    return false;
                }
                if (mins < 0 || mins > 59) {
                    return false;
                }
                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 final class TimeUnitBinding extends IntegerBinding {

            final Unit unit;

            TimeUnitBinding(Unit unit) {
                this.bind(textProperty());
                this.unit = unit;
            }

            @Override
            protected int computeValue() {
                // Crazy enum magic
                String token = getText().split(":")[unit.ordinal()];
                return Integer.parseInt(token);
            }

        }

    }
}
