/**
 * erlyberly, erlang trace debugger
 * Copyright (C) 2016 Andy Till
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package erlyberly;

import java.text.DecimalFormat;
import java.net.URL;
import java.util.ResourceBundle;

import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangString;

import de.jensd.fx.fontawesome.AwesomeIcon;
import floatyfield.FloatyFieldView;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Orientation;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.PieChart.Data;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Separator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import ui.FAIcon;

/**
 * Handles UI related tasks and delegates processing to {@link ProcController}.
 */
public class ProcView implements Initializable {

    private final ProcController procController;

    @FXML
    private TableView<ProcInfo> processView;
    @FXML
    private Button refreshButton;
    @FXML
    private Button pollButton;
    @FXML
    private Button heapPieButton;
    @FXML
    private Button stackPieButton;
    @FXML
    private Button totalHeapPieButton;
    @FXML
    private HBox headerBox;

    /**
     * Total of the last opened pie chart
     */
    private double total = 0d;

    /**
     * Only use on the javafx thread
     */
    private final DecimalFormat percentFormatter = new DecimalFormat("#.#");


    public ProcView() {
        procController = new ProcController();
    }

    @Override
    @SuppressWarnings("unchecked")
    public void initialize(URL url, ResourceBundle r) {
        MenuItem menuItem;

        menuItem = new MenuItem("Get process state (R16B03 or higher)");
        menuItem.setOnAction(this::onShowProcessStateClicked);

        menuItem.disableProperty().bind(processView.getSelectionModel().selectedItemProperty().isNull());

        processView.setContextMenu(new ContextMenu(menuItem));

        // #23 when the context menu is showing, temporarily suspend polling, polling
        // loses selection making the right click context menu no longer enabled since
        // no process is selected
        processView
            .getContextMenu()
            .showingProperty()
            .addListener((o, oldv, newv) -> { procController.setTemporarilySuspendPolling(newv); });

        final BooleanBinding notConnected = ErlyBerly.nodeAPI().connectedProperty().not();

        ErlyBerly.nodeAPI().connectedProperty().addListener(this::onConnected);

        heapPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART));
        heapPieButton.getStyleClass().add("erlyberly-icon-button");
        heapPieButton.setStyle("-fx-background-color: transparent;");
        heapPieButton.setText("");
        heapPieButton.disableProperty().bind(notConnected);

        stackPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART));
        stackPieButton.setStyle("-fx-background-color: transparent;");
        stackPieButton.setText("");
        stackPieButton.disableProperty().bind(notConnected);

        totalHeapPieButton.setGraphic(FAIcon.create().icon(AwesomeIcon.PIE_CHART));
        totalHeapPieButton.setStyle("-fx-background-color: transparent;");
        totalHeapPieButton.setText("");
        totalHeapPieButton.disableProperty().bind(notConnected);

        refreshButton.setGraphic(FAIcon.create().icon(AwesomeIcon.ROTATE_LEFT));
        refreshButton.setGraphicTextGap(8d);
        refreshButton.disableProperty().bind(procController.pollingProperty().or(notConnected));

        pollButton.setGraphic(FAIcon.create().icon(AwesomeIcon.REFRESH));
        pollButton.setGraphicTextGap(9d);
        pollButton.disableProperty().bind(notConnected);

        procController.pollingProperty().addListener(this::onPollingChange);
        procController.setListComparator(processView.comparatorProperty());

        onPollingChange(null);

        TableColumn<ProcInfo, String> pidColumn = (TableColumn<ProcInfo, String>) processView.getColumns().get(0);
        TableColumn<ProcInfo, String> procColumn = (TableColumn<ProcInfo, String>) processView.getColumns().get(1);
        TableColumn<ProcInfo, Long> reducColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(2);
        TableColumn<ProcInfo, Long> mQueueLenColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(3);
        TableColumn<ProcInfo, Long> heapSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(4);
        TableColumn<ProcInfo, Long> stackSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(5);
        TableColumn<ProcInfo, Long> totalHeapSizeColumn = (TableColumn<ProcInfo, Long>) processView.getColumns().get(6);

        pidColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, String>("pid"));
        pidColumn.setId("pid");

        procColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, String>("processName"));
        procColumn.setId("proc");

        reducColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("reductions"));
        reducColumn.setId("reduc");

        mQueueLenColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("msgQueueLen"));
        mQueueLenColumn.setId("mqueue");

        heapSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("heapSize"));
        heapSizeColumn.setId("heapsize");

        stackSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("stackSize"));
        stackSizeColumn.setId("stacksize");

        totalHeapSizeColumn.setCellValueFactory(new PropertyValueFactory<ProcInfo, Long>("totalHeapSize"));
        totalHeapSizeColumn.setId("totalheapsize");

        processView.setItems(procController.getProcs());
        processView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        addFloatySearchControl();

        initialiseProcessSorting();
    }

    private FxmlLoadable addFloatySearchControl() {
        FxmlLoadable loader = new FxmlLoadable("/floatyfield/floaty-field.fxml");

        loader.load();

        FloatyFieldView ffView;

        ffView = (FloatyFieldView) loader.controller;
        ffView.promptTextProperty().set("Filter on process pid and registered name");

        HBox.setHgrow(loader.fxmlNode, Priority.ALWAYS);

        headerBox.getChildren().add(0, new Separator(Orientation.VERTICAL));
        headerBox.getChildren().add(0, loader.fxmlNode);

        procController.filterProperty().bind(ffView.textProperty());

        Platform.runLater(() -> {
            FilterFocusManager.addFilter((Control) loader.fxmlNode.getChildrenUnmodifiable().get(1), 0);
        });

        return loader;
    }

    private void onShowProcessStateClicked(ActionEvent e) {
        ProcInfo proc = processView.getSelectionModel().getSelectedItem();

        if(proc == null)
            return;

        procController.processState(proc, (eobj) -> {showProcessStateInWindow(proc, eobj); });
    }

    private void showProcessStateInWindow(ProcInfo procInfo, OtpErlangObject obj) {
        if(obj == null)
            obj = new OtpErlangString("Error, erlyberly cannot get process state. Probably not OTP compliant process");

        TermTreeView termTreeView;

        termTreeView = new TermTreeView();
        termTreeView.setMaxHeight(Integer.MAX_VALUE);
        VBox.setVgrow(termTreeView, Priority.ALWAYS);
        termTreeView.populateFromTerm(obj);

        ErlyBerly.showPane("Process State for " + procInfo.getShortName(), ErlyBerly.wrapInPane(termTreeView));
    }

    @FXML
    private void onHeapPie() {
        ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getHeapSize(); });

        showPieChart("Process Heap", data);
    }

    @FXML
    private void onStackPie() {
        ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getStackSize(); });

        showPieChart("Process Stack", data);
    }

    @FXML
    private void onTotalHeapPie() {
        ObservableList<PieChart.Data> data = buildData(chartableProcs(), (p) -> {return p.getTotalHeapSize(); });

        showPieChart("Total Heap", data);
    }

    private ObservableList<PieChart.Data> buildData(ObservableList<ProcInfo> procs, Callback<ProcInfo, Long> extractor) {

        total = 0;

        for (ProcInfo proc : procs) {
            total += extractor.call(proc);
        }

        // threshold is 0.5%, this is a limit on how many segments are added to
        // the pie chart too many seems to crash the process
        double threshold = total / 200;

        double other = 0;

        ObservableList<PieChart.Data> data = FXCollections.observableArrayList();

        for (ProcInfo proc : procs) {
            double value = extractor.call(proc);

            if(value >= threshold)
                data.add(new Data(procDescription(proc), extractor.call(proc)));
            else
                other += value;
        }

        if(other > 0)
            data.add(new Data("All processes less than 0.5% of total", other));

        return data;
    }

    private ObservableList<ProcInfo> chartableProcs() {
        ObservableList<ProcInfo> procs = processView.getSelectionModel().getSelectedItems();

        if(procs.isEmpty() || procs.size() == 1) {
            procs = procController.getProcs();
        }
        return procs;
    }

    private void showPieChart(String title, ObservableList<PieChart.Data> data) {
        PieChart pieChart;
        pieChart = new PieChart(data);
        pieChart.setTitle(title);
        pieChart.getData().stream().forEach(d -> {
            Tooltip tooltip;
            tooltip = new Tooltip();
            String percent = percentFormatter.format((d.getPieValue()/total)*100);
            tooltip.setText(d.getName() + " " + percent + "%");
            Tooltip.install(d.getNode(), tooltip);
        });
        ErlyBerly.showPane(title, ErlyBerly.wrapInPane(pieChart));
    }

    private String procDescription(ProcInfo proc) {
        String pid = proc.getProcessName();
        if(pid == null || "".equals(pid)) {
            pid = proc.getPid();
        }
        if(pid == null || "".equals(pid)) {
            pid = "unknown pid";
        }
        return pid;
    }

    private void onPollingChange(Observable o) {
        if(procController.pollingProperty().get())
            pollButton.setText("Stop Polling");
        else
            pollButton.setText("Start Polling");
    }

    @FXML
    private void onRefresh() {
        procController.refreshOnce();
    }

    @FXML
    private void onTogglePolling() {
        procController.togglePolling();
    }

    private void onConnected(Observable o) {

        boolean connected = ErlyBerly.nodeAPI().connectedProperty().get();

        if(connected) {
            procController.refreshOnce();
        } else {
            procController.clearProcesses();
        }
    }

    private void initialiseProcessSorting() {
        InvalidationListener invalidationListener = new ProcSortUpdater();

        for (TableColumn<ProcInfo, ?> col : processView.getColumns()) {
            col.sortTypeProperty().addListener(invalidationListener);
        }

        processView.getSortOrder().addListener(invalidationListener);
    }

    private final class ProcSortUpdater implements InvalidationListener {
        @Override
        public void invalidated(Observable ob) {
            ProcSort procSort = null;

            if(!processView.getSortOrder().isEmpty()) {
                TableColumn<ProcInfo, ?> tableColumn = processView.getSortOrder().get(0);

                procSort = new ProcSort(tableColumn.getId(), tableColumn.getSortType());
            }
            procController.procSortProperty().set(procSort);
        }
    }
}