/* Copyright 2016 Evan A. Thompson
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.alvanson.xltsearch;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;

class Configurator {
    private static final int SCENE_WIDTH = 640;
    private static final int SCENE_HEIGHT = 480;
    private static final int BUTTON_WIDTH = 100;

    @FXML private BorderPane border;
    @FXML private TextField newField;
    @FXML private ListView<String> listView;

    private Config config;
    private boolean dirty;
    private final Stage stage = new Stage();
    private final Map<String,ComboBox<String>> optionsMap = new HashMap<>();
    private final ObjectProperty<Catalog> catalog = new SimpleObjectProperty<>();

    Configurator() {
        stage.setTitle("Configuration");
        stage.getIcons().addAll(new Image(getClass().getResourceAsStream("/icon32.png")),
                                new Image(getClass().getResourceAsStream("/icon22.png")),
                                new Image(getClass().getResourceAsStream("/icon16.png")));
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/configurator.fxml"));
        fxmlLoader.setController(this);
        try {
            Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
            stage.setScene(scene);
        } catch (Exception ex) {
            DetailedAlert alert = new DetailedAlert(DetailedAlert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Exception while loading Configurator");
            alert.setDetailsText(MessageLogger.getStackTrace(ex));
            alert.showAndWait();
        }
    }

    @FXML
    private void initialize() {
        catalog.addListener((o, oldValue, newValue) -> {
            dirty = false;
            if (newValue != null) {
                List<String> configs = newValue.getConfigs();
                Collections.sort(configs);
                listView.setItems(FXCollections.observableList(configs));
            } else {
                listView.getItems().clear();
            }
            drawCenterPane();
        });

        listView.getSelectionModel().selectedItemProperty().addListener(
                (o, oldValue, newValue) -> {
            dirty = false;
            config = catalog.get().getConfig(newValue);
            drawCenterPane();
        });
    }

    private void drawCenterPane() {
        final GridPane grid = new GridPane();
        grid.setAlignment(Pos.BASELINE_LEFT);
        grid.setPadding(new Insets(10, 10, 10, 0));

        final ColumnConstraints cc = new ColumnConstraints();
        final ColumnConstraints ccGrow = new ColumnConstraints();
        ccGrow.setHgrow(Priority.ALWAYS);
        grid.getColumnConstraints().addAll(cc, ccGrow, cc);

        grid.setHgap(10);
        grid.setVgap(10);

        if (config == null) {
           final Label label = new Label("No configuration selected");
           grid.add(label, 0, 0);
        } else {
            // add each property, one per row
            int row = 0;
            optionsMap.clear();
            for (String property : config.getPropertyNames()) {
                // format property label
                StringBuilder sb = new StringBuilder();
                String[] words = property.split("\\.");
                for (String word : words) {
                    sb.append(word.substring(0, 1).toUpperCase());  // capitalize first letter
                    sb.append(word.substring(1));   // add remaining letters
                    sb.append(" ");
                }
                sb.setCharAt(sb.length()-1, ':');   // replace trailing space with ':'
                final Label label = new Label(sb.toString());
                label.getStyleClass().add("caption");
                label.setMinWidth(Label.USE_PREF_SIZE);

                grid.add(label, 0, row);

                // populate property options
                final ComboBox comboBox = new ComboBox();
                comboBox.setItems(FXCollections.observableArrayList(
                    config.getOptions(property)));
                comboBox.setValue(config.getValue(property));
                comboBox.setMaxWidth(Double.MAX_VALUE);
                comboBox.valueProperty().addListener((o, ov, nv) -> {
                    dirty = true;
                });

                grid.add(comboBox, 1, row, 2, 1);
                // keep references to property comboBoxes
                optionsMap.put(property, comboBox);
                row++;
            }

            grid.add(new Label(" "), 0, row, 3, 1);  // row spacer

            // index status
            final Label statusMessage = new Label(config.getStatus());
            statusMessage.setAlignment(Pos.BASELINE_RIGHT);
            statusMessage.setMaxWidth(Double.MAX_VALUE);

            grid.add(statusMessage, 0, row+1, 3, 1);

            final Button updateIndexButton = new Button("Update");
            updateIndexButton.setPrefWidth(BUTTON_WIDTH);
            final Button rebuildIndexButton = new Button("Rebuild");
            rebuildIndexButton.setPrefWidth(BUTTON_WIDTH);

            long lastUpdated = config.getLastUpdated();
            if (lastUpdated == Config.INDEX_INVALIDATED) {
                grid.add(rebuildIndexButton, 2, row+2);
            } else {
                if (lastUpdated < 0) {
                    updateIndexButton.setText("Build");
                }
                grid.add(updateIndexButton, 2, row+2);
            }

            grid.add(new Label(" "), 0, row+3, 3, 1);  // row spacer

            final Button saveButton = new Button("Save");
            saveButton.setPrefWidth(BUTTON_WIDTH);

            grid.add(saveButton, 2, row+4);

            final Button deleteButton = new Button("Delete");
            deleteButton.getStyleClass().add("alert");
            deleteButton.setPrefWidth(BUTTON_WIDTH);

            grid.add(deleteButton, 2, row+5);

            updateIndexButton.setOnAction((event) -> updateIndex());
            rebuildIndexButton.setOnAction((event) -> rebuildIndex());
            saveButton.setOnAction((event) -> saveConfig());
            deleteButton.setOnAction((event) -> deleteConfig());
        }

        border.setCenter(grid);
    }

    @FXML
    private void newConfig() {
        // validate name (only allow word characters)
        String name = newField.getText();
        name = name.replaceAll("[^\\w]","");
        newField.clear();
        newField.setPromptText("");
        if (!name.equals("")) {
            int index = Collections.binarySearch(listView.getItems(), name);
            if (index < 0) {    // not found in list (good)
                // add in sorted order and select
                listView.getItems().add(-index-1, name);
                listView.getSelectionModel().select(-index-1);
            } else {
                listView.getSelectionModel().select(index);
            }
        } else {
            newField.setPromptText("Invalid name");
        }
    }

    @FXML
    private void loadConfig() {
        if (config == null) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("No configuration selected");
            alert.setContentText("");
            alert.showAndWait();
            return;
        } else if (dirty) {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Configuration not saved");
            alert.setContentText("Do you want to save?");
            alert.getDialogPane().getButtonTypes().clear();
            alert.getDialogPane().getButtonTypes().addAll(
                ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
            Optional<ButtonType> result = alert.showAndWait();
            if (!result.isPresent() || result.get() == ButtonType.CANCEL) {
                return;
            } else if (result.get() == ButtonType.YES) {
                saveConfig();
            }
        }
        catalog.get().open(config.getName());
        hide();
    }

    private void updateIndex() {
        if (dirty) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Configuration not saved");
            alert.setContentText("Configuration must be saved before updating index.");
            alert.showAndWait();
        } else {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Update index");
            alert.setContentText("Are you sure you want to load this configuration\n" +
                "and update the index?");
            Optional<ButtonType> result = alert.showAndWait();
            if (result.isPresent() && result.get() == ButtonType.OK) {
                catalog.get().open(config.getName());
                catalog.get().updateIndex();
                hide();
            }
        }
    }

    private void rebuildIndex() {
        if (dirty) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Configuration not saved");
            alert.setContentText("Configuration must be saved before rebuilding index.");
            alert.showAndWait();
        } else {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Rebuild index");
            alert.setContentText("Are you sure you want to rebuild the index?");
            Optional<ButtonType> result = alert.showAndWait();
            if (result.isPresent() && result.get() == ButtonType.OK) {
                config.deleteIndex();
                catalog.get().open(config.getName());
                catalog.get().updateIndex();
                hide();
            }
        }
    }

    private void saveConfig() {
        // saving is potentially expensive: confirm with user before saving
        if (config == null) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("No configuration selected");
            alert.setContentText("");
            alert.showAndWait();
            return;
        } else if (config.getLastUpdated() >= Config.INDEX_UPDATE_FAILED) {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Saving invalidates index");
            alert.setContentText("Index will have to be rebuilt. Do you still want to save?");
            Optional<ButtonType> result = alert.showAndWait();
            if (!result.isPresent() || result.get() != ButtonType.OK) {
                return;
            }
        }
        // actually save
        for (Map.Entry<String,ComboBox<String>> e : optionsMap.entrySet()) {
            String value = e.getValue().getValue();
            if (value != null) {
                config.set(e.getKey(), value);
            }
        }
        dirty = false;
        drawCenterPane();
    }

    private void deleteConfig() {
        Config candidate = config;   // avoid race conditions
        // confim delete
        if (catalog.get().getConfigName().equals(candidate.getName())) {
            Alert alert = new Alert(Alert.AlertType.ERROR);
            alert.setTitle("Error");
            alert.setHeaderText("Current configuration");
            alert.setContentText("Cannot delete current configuration.");
            alert.showAndWait();
        } else {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Delete configuration");
            alert.setContentText("Are you sure you want to delete this configuration?\n" +
                                 "This operation cannot be undone.");
            Optional<ButtonType> result = alert.showAndWait();
            if (result.isPresent() && result.get() == ButtonType.OK) {
                // config is not guaranteed to be == candidate or != null, so do not use
                listView.getItems().remove(candidate.getName());
                listView.getSelectionModel().clearSelection();
                candidate.delete();
                candidate = null;
            }
        }
    }

    void show() {
        if (catalog.get().isIndexing()) {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Confirmation");
            alert.setHeaderText("Indexing in progress");
            alert.setContentText("Changing configuration will cancel indexing. Continue?");
            Optional<ButtonType> result = alert.showAndWait();
            if (result.isPresent() && result.get() == ButtonType.OK) {
                catalog.get().cancelAllTasks();
            } else {    // cancel
                return;
            }
        }
        // select current config
        String configName = catalog.get().getConfigName();
        if (configName != null) {
            int index = Collections.binarySearch(listView.getItems(), configName);
            if (index >= 0) {
                listView.getSelectionModel().select(index);
            }
        }
        stage.show();
    }

    @FXML
    private void showAbout() {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("About");
        alert.setHeaderText("XLTSearch " + App.XLT_VERSION);
        alert.setContentText("Copyright 2016 Evan A. Thompson\n" +
                             "https://github.com/alvanson/xltsearch");
        alert.showAndWait();
    }

    @FXML
    void hide() {
        listView.getSelectionModel().clearSelection();
        stage.hide();
    }

    ObjectProperty<Catalog> catalogProperty() { return catalog; }
}