/**
 * An interface to help organizes a multi-file gcode workflow.
 */

/*
    Copywrite 2016 Will Winder

    This file is part of Universal Gcode Sender (UGS).

    UGS 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.

    UGS 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 UGS.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.willwinder.ugs.nbm.workflow;

import com.willwinder.ugs.nbp.lib.services.LocalizingService;
import com.willwinder.ugs.nbp.lib.lookup.CentralLookup;
import com.willwinder.universalgcodesender.i18n.Localization;
import com.willwinder.universalgcodesender.listeners.UGSEventListener;
import com.willwinder.universalgcodesender.model.BackendAPI;
import com.willwinder.universalgcodesender.model.UGSEvent;
import com.willwinder.universalgcodesender.model.UGSEvent.ControlState;
import com.willwinder.universalgcodesender.uielements.components.GcodeFileTypeFilter;
import com.willwinder.universalgcodesender.utils.Settings;
import java.io.File;
import java.util.Arrays;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import org.netbeans.api.settings.ConvertAsProperties;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.util.Exceptions;
import org.openide.windows.TopComponent;
import org.openide.util.NbBundle.Messages;

@ConvertAsProperties(
        dtd = "-//com.willwinder.ugs.nbm.workflow//WorkflowWindow//EN",
        autostore = false
)
@TopComponent.Description(
        preferredID = "WorkflowWindowTopComponent",
        //iconBase="SET/PATH/TO/ICON/HERE", 
        persistenceType = TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(mode = "editor", openAtStartup = false)
@Messages({
})

@ActionID(category = LocalizingService.WorkflowWindowCategory, id = LocalizingService.WorkflowWindowActionId)
@ActionReference(path = LocalizingService.WorkflowWindowWindowPath)
@TopComponent.OpenActionRegistration(
        displayName = "<Not localized:WorkflowWindow>",
        preferredID = "WorkflowWindowTopComponent"
)

/**
 * UGSEventListener - this is how a plugin can listen to UGS lifecycle events.
 * ListSelectionListener - listen for table selections.
 */
public final class WorkflowWindowTopComponent extends TopComponent implements UGSEventListener, ListSelectionListener {

    // These are the UGS backend objects for interacting with the backend.
    private final Settings settings;
    private final BackendAPI backend;


    // This is used to identify when a stream has completed.
    private boolean wasSending;

    // This is used in most functions, so cache it here.
    DefaultTableModel model;

    /**
     * Initialize the WorkflowWindow, register with the UGS Backend and set some
     * of the required JTable settings.
     */
    public WorkflowWindowTopComponent() {
        setName(LocalizingService.WorkflowWindowTitle);
        setToolTipText(LocalizingService.WorkflowWindowTooltip);

        initComponents();

        // This is how to access the UGS backend and register the listener.
        // CentralLookup is used to get singleton instances of the UGS Settings
        // and BackendAPI objects.
        settings = CentralLookup.getDefault().lookup(Settings.class);
        backend = CentralLookup.getDefault().lookup(BackendAPI.class);
        backend.addUGSEventListener(this);

        // Only allow contiguous ranges of selections and register as a listener.
        this.fileTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
        ListSelectionModel cellSelectionModel = this.fileTable.getSelectionModel();
        cellSelectionModel.addListSelectionListener(this);
    }

    /**
     * Events from backend. Take specific actions based on the control state.
     * FILE_CHANGED: Add the file to the workflow, always do this if the workflow page is loaded.
     * 
     * STATE_CHANGED: Attempt to detect the file stream completion.
     * 
     * @param cse 
     */
    @Override
    public void UGSEvent(UGSEvent cse) {
        if (cse.isStateChangeEvent()) {
            if (wasSending && cse.getControlState() == ControlState.COMM_IDLE)
               this.completeFile(backend.getGcodeFile());
            wasSending = backend.isSendingFile();
        }
        if (cse.isFileChangeEvent()) {
            this.addFileToWorkflow(backend.getGcodeFile());
        }
    }

    /**
     * Call when a file's work has been completed to progress to the next step
     * of the work flow.
     * @param gcodeFile the file which is completing.
     */
    public void completeFile(File gcodeFile) {
        if (gcodeFile == null) return;

        // Make sure the file is loaded in the table.
        int fileIndex = findFileIndex(gcodeFile);
        if (fileIndex < 0) return;

        // Mark that it has been completed.
        model.setValueAt(true, fileIndex, 2);

        fileIndex++;
        String message;

        // Make sure there is another command left.
        if (fileIndex < fileTable.getRowCount()) {
            String nextTool = (String) model.getValueAt(fileIndex, 1);
            String messageTemplate =
                    "Finished sending '%s'.\n"
                  + "The next file uses tool '%s'\n"
                  + "Load tool and move machine to its zero location\n"
                  + "and click send to continue this workflow.";
            message = String.format(messageTemplate, gcodeFile.getName(), nextTool);

            // Select the next row, this will trigger a selection event.
            fileTable.setRowSelectionInterval(fileIndex, fileIndex);
            
        // Use a different message if we're finished.
        } else {
            message = "Finished sending the last file!";
        }

        // Display a notification.
        java.awt.EventQueue.invokeLater(() -> {
            JOptionPane.showMessageDialog(new JFrame(), message, 
                    "Workflow Event", JOptionPane.PLAIN_MESSAGE);
        });

    }

    /**
     * ListSelectionListener - load files when they are selected.
     * @param e 
     */
    @Override
    public void valueChanged(ListSelectionEvent e) {
        int[] selectedRow = fileTable.getSelectedRows();
        // Only load files when there is a single selection.
        if (selectedRow.length == 1) {
            // Pull the file out of the table and set it in the backend.
            String file = (String) model.getValueAt(selectedRow[0], 0);
            try {
                backend.setGcodeFile(new File(file));
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            }
        }

    }

    /**
     * Add a file to the table.
     * @param gcodeFile 
     */
    public void addFileToWorkflow(File gcodeFile) {
        if (gcodeFile == null) {
            return;
        }

        int fileIndex = findFileIndex(gcodeFile);
        // Don't re-add a file.
        if (fileIndex >= 0) {
            return;
        }

        model.addRow(new Object[]{
                gcodeFile.getAbsolutePath(),
                "default",
                false
            });

        // Fire off the selection event to load the file.
        int lastRow = fileTable.getRowCount() - 1;
        fileTable.setRowSelectionInterval(lastRow, lastRow);
    }


    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        addButton = new javax.swing.JButton();
        removeButton = new javax.swing.JButton();
        tablePanel = new javax.swing.JPanel();
        upButton = new javax.swing.JButton();
        tableScrollPane = new javax.swing.JScrollPane();
        fileTable = new javax.swing.JTable();
        downButton = new javax.swing.JButton();

        org.openide.awt.Mnemonics.setLocalizedText(addButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.addButton.text")); // NOI18N
        addButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                addButtonActionPerformed(evt);
            }
        });

        org.openide.awt.Mnemonics.setLocalizedText(removeButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.removeButton.text")); // NOI18N
        removeButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                removeButtonActionPerformed(evt);
            }
        });

        upButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/resources/Arrowhead-Up-01-32.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(upButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.upButton.text")); // NOI18N
        upButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                upButtonActionPerformed(evt);
            }
        });

        tableScrollPane.setMinimumSize(new java.awt.Dimension(100, 60));

        fileTable.setModel(new javax.swing.table.DefaultTableModel(
            new Object [][] {

            },
            new String [] {
                "Filename", "Toolname", "Finished"
            }
        ) {
            Class[] types = new Class [] {
                java.lang.String.class, java.lang.String.class, java.lang.Boolean.class
            };
            boolean[] canEdit = new boolean [] {
                false, true, false
            };

            public Class getColumnClass(int columnIndex) {
                return types [columnIndex];
            }

            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return canEdit [columnIndex];
            }
        });
        fileTable.setMinimumSize(new java.awt.Dimension(100, 60));
        tableScrollPane.setViewportView(fileTable);
        if (fileTable.getColumnModel().getColumnCount() > 0) {
            fileTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title0")); // NOI18N
            fileTable.getColumnModel().getColumn(1).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title1")); // NOI18N
            fileTable.getColumnModel().getColumn(2).setHeaderValue(org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.fileTable.columnModel.title3")); // NOI18N
        }

        downButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/resources/Arrowhead-Down-01-32.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(downButton, org.openide.util.NbBundle.getMessage(WorkflowWindowTopComponent.class, "WorkflowWindowTopComponent.downButton.text")); // NOI18N
        downButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                downButtonActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout tablePanelLayout = new javax.swing.GroupLayout(tablePanel);
        tablePanel.setLayout(tablePanelLayout);
        tablePanelLayout.setHorizontalGroup(
            tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(tablePanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(upButton)
                    .addComponent(downButton, javax.swing.GroupLayout.Alignment.TRAILING))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(tableScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)
                .addContainerGap())
        );
        tablePanelLayout.setVerticalGroup(
            tablePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(tablePanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(tableScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)
                .addContainerGap())
            .addGroup(tablePanelLayout.createSequentialGroup()
                .addComponent(upButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 29, Short.MAX_VALUE)
                .addComponent(downButton))
        );

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(addButton, javax.swing.GroupLayout.DEFAULT_SIZE, 202, Short.MAX_VALUE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(removeButton, javax.swing.GroupLayout.DEFAULT_SIZE, 211, Short.MAX_VALUE))
                    .addComponent(tablePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(addButton)
                    .addComponent(removeButton))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(tablePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addContainerGap())
        );
    }// </editor-fold>//GEN-END:initComponents

    private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed
        JFileChooser fileChooser = GcodeFileTypeFilter.getGcodeFileChooser(settings.getLastOpenedFilename());

        int returnVal = fileChooser.showOpenDialog(this);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
                File gcodeFile = fileChooser.getSelectedFile();
                settings.setLastOpenedFilename(gcodeFile.getParent());
                addFileToWorkflow(gcodeFile);
        }  

    }//GEN-LAST:event_addButtonActionPerformed

    private void removeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeButtonActionPerformed
        int[] selectedRows = fileTable.getSelectedRows();
        if (selectedRows.length == 0) return;

        Arrays.sort(selectedRows);
        for (int i = selectedRows.length - 1; i >= 0; i--) {
            int row = selectedRows[i];
            this.model.removeRow(row);
            this.model.fireTableRowsDeleted(row, row);
        }
    }//GEN-LAST:event_removeButtonActionPerformed

    private void upButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_upButtonActionPerformed
        int[] selectedRows = fileTable.getSelectedRows();

        // Exit early if nothing is selected.
        if (selectedRows.length == 0) return;

        Arrays.sort(selectedRows);

        // Exit early if the selected range can't move.
        if (selectedRows[0] == 0) return;

        for (int i = 0; i < selectedRows.length; i++) {
            selectedRows[i] = this.moveRow(selectedRows[i], -1);
        }
        int first = selectedRows[0];
        int last = selectedRows[selectedRows.length-1];
        fileTable.setRowSelectionInterval(first, last);
    }//GEN-LAST:event_upButtonActionPerformed

    private void downButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_downButtonActionPerformed
        int[] selectedRows = fileTable.getSelectedRows();

        // Exit early if nothing is selected.
        if (selectedRows.length == 0) return;

        Arrays.sort(selectedRows);

        // Exit early if the selected range can't move.
        if (selectedRows[selectedRows.length-1] == fileTable.getRowCount()) return;

        for (int i = selectedRows.length - 1; i >= 0; i--) {
            selectedRows[i] = this.moveRow(selectedRows[i], 1);
        }
        fileTable.setRowSelectionInterval(selectedRows[0], selectedRows[selectedRows.length-1]);
    }//GEN-LAST:event_downButtonActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton addButton;
    private javax.swing.JButton downButton;
    private javax.swing.JTable fileTable;
    private javax.swing.JButton removeButton;
    private javax.swing.JPanel tablePanel;
    private javax.swing.JScrollPane tableScrollPane;
    private javax.swing.JButton upButton;
    // End of variables declaration//GEN-END:variables

    /**
     * NetBeans module overrides.
     */

    @Override
    public void componentOpened() {
        this.wasSending = backend.isSendingFile();
        model = (DefaultTableModel)this.fileTable.getModel();
    }

    @Override
    public void componentClosed() {
        // TODO add custom code on component closing
    }

    /**
     * Helper functions.
     */
    
    /**
     * Look for the provided file in the file table.
     * @param gcodeFile
     * @return Row index of the provided file or -1 if not found.
     */
    private int findFileIndex(File gcodeFile) {
        if (gcodeFile == null) return -1;

        for (int i = 0; i < model.getRowCount(); i++) {
            String file = (String) model.getValueAt(i, 0);
            if (file != null && gcodeFile.getAbsolutePath().equals(file)) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Move a given row by some offset. If the offset would move the row outside
     * of the current table size, the row is not moved.
     * @param row row to move.
     * @param offset how far to move row.
     * @return location of row after move.
     */
    private int moveRow(int row, int offset) {
        int dest = row + offset;
        if (dest < 0 || dest >= model.getRowCount()) {
            return row;
        }

        model.moveRow(row, row, dest);
        return dest;
    }

    void writeProperties(java.util.Properties p) {
        // better to version settings since initial version as advocated at
        // http://wiki.apidesign.org/wiki/PropertyFiles
        p.setProperty("version", "1.0");
        // TODO store your settings
    }

    void readProperties(java.util.Properties p) {
        String version = p.getProperty("version");
        // TODO read your settings according to their version
    }
}
