content: /**
   * 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);
          }
      }
  }
  