package ctbrec.ui.settings;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.Config;
import ctbrec.Settings;
import ctbrec.StringUtil;
import ctbrec.ui.controls.DirectorySelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox;
import ctbrec.ui.controls.range.DiscreteRange;
import ctbrec.ui.controls.range.RangeSlider;
import ctbrec.ui.settings.api.ExclusiveSelectionProperty;
import ctbrec.ui.settings.api.Preferences;
import ctbrec.ui.settings.api.PreferencesStorage;
import ctbrec.ui.settings.api.Setting;
import ctbrec.ui.settings.api.SimpleDirectoryProperty;
import ctbrec.ui.settings.api.SimpleFileProperty;
import ctbrec.ui.settings.api.SimpleRangeProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.Property;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;
import javafx.util.converter.NumberStringConverter;

public class CtbrecPreferencesStorage implements PreferencesStorage {

    private static final Logger LOG = LoggerFactory.getLogger(CtbrecPreferencesStorage.class);
    public static final String PATTERN_NOT_A_DIGIT = "[^\\d]";
    public static final String COULDNT_SAVE_MSG = "Couldn't save config setting";

    private Config config;
    private Settings settings;
    private Preferences prefs;

    public CtbrecPreferencesStorage(Config config) {
        this.config = config;
        this.settings = config.getSettings();
    }

    public void setPreferences(Preferences prefs) {
        this.prefs = prefs;
    }

    @Override
    public void save(Preferences preferences) throws IOException {
        throw new RuntimeException("not implemented");
    }

    @Override
    public void load(Preferences preferences) {
        throw new RuntimeException("not implemented");
    }

    @Override
    public Node createGui(Setting setting) throws Exception {
        Property<?> prop = setting.getProperty();
        if (prop instanceof ExclusiveSelectionProperty) {
            return createRadioGroup(setting);
        } else if (prop instanceof SimpleRangeProperty) {
            return createRangeSlider(setting);
        } else if (prop instanceof SimpleDirectoryProperty) {
            return createDirectorySelector(setting);
        } else if (prop instanceof SimpleFileProperty) {
            return createFileSelector(setting);
        } else if (prop instanceof IntegerProperty) {
            return createIntegerProperty(setting);
        } else if (prop instanceof LongProperty) {
            return createLongProperty(setting);
        } else if (prop instanceof BooleanProperty) {
            return createBooleanProperty(setting);
        } else if (prop instanceof ListProperty) {
            return createComboBox(setting);
        } else if (prop instanceof StringProperty) {
            return createStringProperty(setting);
        } else {
            return new Label("Unsupported Type for key " + setting.getKey() + ": " + setting.getProperty());
        }
    }

    private Node createRadioGroup(Setting setting) {
        ExclusiveSelectionProperty prop = (ExclusiveSelectionProperty) setting.getProperty();
        ToggleGroup toggleGroup = new ToggleGroup();
        RadioButton optionA = new RadioButton(prop.getOptionA());
        optionA.setSelected(prop.getValue());
        optionA.setToggleGroup(toggleGroup);
        RadioButton optionB = new RadioButton(prop.getOptionB());
        optionB.setSelected(!optionA.isSelected());
        optionB.setToggleGroup(toggleGroup);
        optionA.selectedProperty().bindBidirectional(prop);
        prop.addListener((obs, oldV, newV) -> saveValue(() -> {
            Field field = Settings.class.getField(setting.getKey());
            field.set(settings, newV);
            if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
                runRestartRequiredCallback();
            }
            config.save();
        }));
        HBox row = new HBox();
        row.getChildren().addAll(optionA, optionB);
        HBox.setMargin(optionA, new Insets(5));
        HBox.setMargin(optionB, new Insets(5));
        return row;
    }

    private void runRestartRequiredCallback() {
        Optional.ofNullable(prefs).map(Preferences::getRestartRequiredCallback).ifPresent(r -> {
            try {
                r.run();
            } catch (RuntimeException e) {
                LOG.warn("Error while calling \"restart required\" callback", e);
            }
        });
    }

    @SuppressWarnings("unchecked")
    private Node createRangeSlider(Setting setting) {
        SimpleRangeProperty<Integer> rangeProperty = (SimpleRangeProperty<Integer>) setting.getProperty();
        DiscreteRange<Integer> range = (DiscreteRange<Integer>) rangeProperty.getRange();
        List<Integer> labels = (List<Integer>) range.getLabels();
        List<Integer> values = range.getTicks();
        RangeSlider<Integer> resolutionRange = new RangeSlider<>(rangeProperty.getRange());
        resolutionRange.setShowTickMarks(true);
        resolutionRange.setShowTickLabels(true);
        int lowValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().minimumResolution);
        resolutionRange.setLow(lowValue >= 0 ? lowValue : values.get(0));
        int highValue = getRangeSliderValue(values, labels, Config.getInstance().getSettings().maximumResolution);
        resolutionRange.setHigh(highValue >= 0 ? highValue : values.get(values.size() - 1));
        resolutionRange.getLow().addListener((obs, o, n) -> saveValue(() -> {
            int newV = labels.get(n.intValue());
            Field field = Settings.class.getField(rangeProperty.getLowKey());
            field.set(settings, newV);
            config.save();
        }));
        resolutionRange.getHigh().addListener((obs, o, n) -> saveValue(() -> {
            int newV = labels.get(n.intValue());
            Field field = Settings.class.getField(rangeProperty.getHighKey());
            field.set(settings, newV);
            config.save();
        }));
        return resolutionRange;
    }

    private int getRangeSliderValue(List<Integer> values, List<Integer> labels, int value) {
        for (int i = 0; i < labels.size(); i++) {
            int label = labels.get(i).intValue();
            if (label == value) {
                return values.get(i);
            }
        }
        return -1;
    }

    private Node createFileSelector(Setting setting) {
        ProgramSelectionBox programSelector = new ProgramSelectionBox("");
        programSelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> {
            String path = n;
            Field field = Settings.class.getField(setting.getKey());
            String oldValue = (String) field.get(settings);
            if (!Objects.equals(path, oldValue)) {
                field.set(settings, path);
                if (setting.doesNeedRestart()) {
                    runRestartRequiredCallback();
                }
                config.save();
            }
        }));
        StringProperty property = (StringProperty) setting.getProperty();
        programSelector.fileProperty().bindBidirectional(property);
        return programSelector;
    }

    private Node createDirectorySelector(Setting setting) {
        DirectorySelectionBox directorySelector = new DirectorySelectionBox("");
        directorySelector.prefWidth(400);
        directorySelector.fileProperty().addListener((obs, o, n) -> saveValue(() -> {
            String path = n;
            Field field = Settings.class.getField(setting.getKey());
            String oldValue = (String) field.get(settings);
            if (!Objects.equals(path, oldValue)) {
                field.set(settings, path);
                if (setting.doesNeedRestart()) {
                    runRestartRequiredCallback();
                }
                config.save();
            }
        }));
        StringProperty property = (StringProperty) setting.getProperty();
        directorySelector.fileProperty().bindBidirectional(property);
        return directorySelector;
    }

    private Node createStringProperty(Setting setting) {
        TextField ctrl = new TextField();
        ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
            Field field = Settings.class.getField(setting.getKey());
            field.set(settings, newV);
            if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
                runRestartRequiredCallback();
            }
            config.save();
        }));
        StringProperty prop = (StringProperty) setting.getProperty();
        ctrl.textProperty().bindBidirectional(prop);
        return ctrl;
    }

    @SuppressWarnings("unchecked")
    private Node createIntegerProperty(Setting setting) {
        TextField ctrl = new TextField();
        ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
            if (!newV.matches("\\d*")) {
                ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, ""));
            }
            if (!ctrl.getText().isEmpty()) {
                Field field = Settings.class.getField(setting.getKey());
                field.set(settings, Integer.parseInt(ctrl.getText()));
                if (setting.doesNeedRestart() && !Objects.equals(oldV, newV) && prefs != null) {
                    runRestartRequiredCallback();
                }
                config.save();
            }
        }));
        Property<Number> prop = setting.getProperty();
        ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter());
        return ctrl;
    }

    @SuppressWarnings("unchecked")
    private Node createLongProperty(Setting setting) {
        TextField ctrl = new TextField();
        ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
            if (!newV.matches("\\d*")) {
                ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, ""));
            }
            if (!ctrl.getText().isEmpty()) {
                long value = Long.parseLong(ctrl.getText());
                if (setting.getConverter() != null) {
                    value = (long) setting.getConverter().convertFrom(value);
                }
                Field field = Settings.class.getField(setting.getKey());
                field.set(settings, value);
                if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
                    runRestartRequiredCallback();
                }
                config.save();
            }
        }));
        Property<Number> prop = setting.getProperty();
        ctrl.textProperty().bindBidirectional(prop, new NumberStringConverter());
        return ctrl;
    }

    private Node createBooleanProperty(Setting setting) {
        CheckBox ctrl = new CheckBox();
        ctrl.selectedProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
            Field field = Settings.class.getField(setting.getKey());
            field.set(settings, newV);
            if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
                runRestartRequiredCallback();
            }
            config.save();
        }));
        BooleanProperty prop = (BooleanProperty) setting.getProperty();
        ctrl.selectedProperty().bindBidirectional(prop);
        return ctrl;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private Node createComboBox(Setting setting) throws NoSuchFieldException, IllegalAccessException {
        ListProperty<?> listProp = (ListProperty<?>) setting.getProperty();
        ComboBox<Object> comboBox = new ComboBox(listProp);
        Field field = Settings.class.getField(setting.getKey());
        Object value = field.get(settings);
        if (StringUtil.isNotBlank(value.toString())) {
            if (setting.getConverter() != null) {
                comboBox.getSelectionModel().select(setting.getConverter().convertTo(value));
            } else {
                comboBox.getSelectionModel().select(value);
            }
        }
        comboBox.valueProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
            LOG.debug("Saving setting {}", setting.getKey());
            if (setting.getConverter() != null) {
                field.set(settings, setting.getConverter().convertFrom(newV));
            } else {
                field.set(settings, newV);
            }
            if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
                runRestartRequiredCallback();
            }
            config.save();
        }));
        if (setting.getChangeListener() != null) {
            comboBox.valueProperty().addListener((ChangeListener<? super Object>) setting.getChangeListener());
        }
        return comboBox;
    }

    private void saveValue(Exec exe) {
        try {
            exe.run();
        } catch (Exception e) {
            LOG.error(COULDNT_SAVE_MSG, e);
        }
    }

    @FunctionalInterface
    private interface Exec {
        public void run() throws Exception;
    }
}
