/*
 * GRBL Firmware Settings. Dynamically load and save all GRBL settings.
 */
/*
    Copywrite 2013-2017 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.universalgcodesender.uielements;

import com.willwinder.universalgcodesender.uielements.components.TableCellListener;
import com.willwinder.universalgcodesender.GrblUtils;
import com.willwinder.universalgcodesender.i18n.Localization;
import com.willwinder.universalgcodesender.listeners.ControllerListener;
import com.willwinder.universalgcodesender.listeners.ControllerStatus;
import com.willwinder.universalgcodesender.model.BackendAPI;
import com.willwinder.universalgcodesender.model.Position;
import com.willwinder.universalgcodesender.model.UGSEvent;
import com.willwinder.universalgcodesender.types.GcodeCommand;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;

/**
 *
 * @author wwinder
 */
public class GrblFirmwareSettingsDialog extends javax.swing.JDialog implements ControllerListener {
    private static final String ERROR = "Error";
    // Controller object to fetch settings from.
    private final BackendAPI grblController;
    private final TableCellListener tcl;
    private int numberOfSettings = 0;
    //private List<String> commandList;
    private boolean loadingSettings;
    
    private String[] commands = null;
    
    private static final int COL_INDEX_SETTING     = 0;
    private static final int COL_INDEX_VALUE       = 1;
    private static final int COL_INDEX_DESCRIPTION = 2;
    
    
    private static final String settingNumRegex = "\\$(\\d*)";
    private static final String settingValueRegex = " +\\= +(\\d*\\.?\\d*)";
    private static final String commentRegex = "\\(.*\\)";
    
    private final Pattern settingNumPattern;
    private final Pattern settingValuePattern;
    private final Pattern commentPattern;

    // These guys are used to save initial settings and determine when they can
    // be restored.
    private boolean initialSingleStepMode;
    private boolean statusUpdatesEnabled;
    private boolean savingSettings;
    
    /**
     * Creates new form GrblFirmwareSettingsDialog
     */
    public GrblFirmwareSettingsDialog(Frame parent, boolean modal, BackendAPI backend) throws Exception {
        super(parent, modal);
        initComponents();
        initLocalization();
        setLocationRelativeTo(parent);

        if (backend == null) {
            throw new Exception("There is no controller. Are you connected?");
        }
        
        this.grblController = backend;
        this.grblController.addControllerListener(this);
        
        this.loadingSettings = false;
        
        // Compile regular expressions.
        this.settingNumPattern   = Pattern.compile(settingNumRegex);
        this.settingValuePattern = Pattern.compile(settingValueRegex);
        this.commentPattern      = Pattern.compile(commentRegex);
        
        initSettings();
        
        Action action = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                TableCellListener tcl = (TableCellListener)e.getSource();
                handleSettingChange(tcl.getRow(), tcl.getColumn(), tcl.getOldValue(), tcl.getNewValue());
            }
        };

        tcl = new TableCellListener(this.settingsTable, action);
    }
    
    private void initSettings() throws Exception {
        this.loadingSettings = true;
        this.grblController.sendGcodeCommand(GrblUtils.GRBL_VIEW_SETTINGS_COMMAND);
    }
    
    private void checkDoneSavingSettings() {
                
        if (this.savingSettings) {
            // If the controller is done sending (we just received the final OK)
            // then reset the original user settings.
            if (!this.grblController.isActive()) {
                // Reset controller to previous settings.
                //These should not be re-enabled until all ok arrive
                this.grblController.getController().setSingleStepMode(initialSingleStepMode);
                this.grblController.getController().setStatusUpdatesEnabled(statusUpdatesEnabled);
                this.savingSettings = false;
            }
        }
    }
    
    /**
     * We are interested in settings.
     */
    @Override
    public void messageForConsole(MessageType type, String msg) {
        // Initially we are in load-settings mode, looking for results to "$$".
        if (this.loadingSettings) {
            if ("ok".equals(msg)) {
                this.loadingSettings = false;
            } else if (this.isSettingString(msg)) {
                this.addSetting(msg);
                // Increment for each setting.
                this.numberOfSettings++;
            }
        }
        
        // Later we can be in save-settings mode.
        if (this.savingSettings) {
            this.checkDoneSavingSettings();
        }
    }
    
    private boolean isSettingString(String msg) {
        if (msg.startsWith("$$")) {
            return false;
        }
        else if (msg.startsWith("$")) {
            return true;
        }
        return false;
    }
    
    private void addSetting(String msg) {
        String setting;
        String value;
        String comment;
        
        Matcher matcher = this.settingNumPattern.matcher(msg);
        if (matcher.find()) {
            setting = matcher.group();
        } else { return; }
        
        matcher = this.settingValuePattern.matcher(msg);
        if (matcher.find()) {
            value = matcher.group(1);
        } else { return; }
        
        matcher = this.commentPattern.matcher(msg);
        if (matcher.find()) {
            comment = matcher.group();
        } else { return; }
        
        addSetting(setting, value, comment);
    }
    
    private void addSetting(String setting, String value, String description) {
        ((DefaultTableModel)this.settingsTable.getModel()).addRow(new String[]{
            setting,
            value,
            description});
    }
    
    private void updateSetting(int row, String setting, String value, String description) {
        this.settingsTable.getModel().setValueAt(setting, row, COL_INDEX_SETTING);
        this.settingsTable.getModel().setValueAt(value, row, COL_INDEX_VALUE);
        this.settingsTable.getModel().setValueAt(description, row, COL_INDEX_DESCRIPTION);
    }

    private void handleSettingChange(int row, int column, Object oldValue, Object newValue) {
                
        // TODO: Initialize the command array the first time a column is edited.
        if ((this.commands == null) && (this.numberOfSettings > 0)) {
            this.commands = new String[numberOfSettings];
        }
        
        // If the user somehow edited a command (thus creating the array) before
        // all the commands were loaded this situation arises.
        if (row > this.commands.length) {
            String before[] = this.commands;
            this.commands = new String[numberOfSettings];
            System.arraycopy(before, 0, this.commands, 0, before.length);
        }
        
        String setting = this.settingsTable.getModel().getValueAt(row, COL_INDEX_SETTING).toString();
        String command = setting + "=" + newValue;
        
        this.commands[row] = command;
    }
      
    private void initLocalization() {
        closeButton.setText(Localization.getString("close"));
        saveButton.setText(Localization.getString("save"));
        TableColumnModel tcm = settingsTable.getTableHeader().getColumnModel();
        tcm.getColumn(0).setHeaderValue(Localization.getString("setting"));
        tcm.getColumn(1).setHeaderValue(Localization.getString("value"));
        tcm.getColumn(2).setHeaderValue(Localization.getString("description"));
        settingsTable.getTableHeader().repaint();
    }
    
    /**
     * 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.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        saveButton = new javax.swing.JButton();
        closeButton = new javax.swing.JButton();
        settingsTableScrollPane = new javax.swing.JScrollPane();
        settingsTable = new javax.swing.JTable();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);

        saveButton.setText("Save");
        saveButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveButtonActionPerformed(evt);
            }
        });

        closeButton.setText("Close");
        closeButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                closeButtonActionPerformed(evt);
            }
        });

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

            },
            new String [] {
                "Setting", "Value", "Description"
            }
        ) {
            private Class[] types = new Class [] {
                java.lang.String.class, java.lang.String.class, java.lang.String.class
            };
            private 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];
            }
        });
        settingsTable.getTableHeader().setReorderingAllowed(false);
        settingsTableScrollPane.setViewportView(settingsTable);
        settingsTable.getColumnModel().getColumn(0).setMinWidth(50);
        settingsTable.getColumnModel().getColumn(0).setMaxWidth(50);
        settingsTable.getColumnModel().getColumn(1).setPreferredWidth(85);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(saveButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addComponent(closeButton)
                .addContainerGap())
            .addComponent(settingsTableScrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 510, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(settingsTableScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 245, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(saveButton)
                    .addComponent(closeButton))
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed
        if (this.savingSettings) {
            JOptionPane.showMessageDialog(new JFrame(),
                "Cannot close dialog until settings have finished being saved."
                + "\nIf we got here by mistake open a bug report on github.",
                ERROR, JOptionPane.ERROR_MESSAGE);
        } else {
            this.setVisible(false);
        }
    }//GEN-LAST:event_closeButtonActionPerformed

    private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed
        // Make sure current cell has been committed by selecting this.
        this.settingsTable.editCellAt(-1, -1);

        // If nothing has been edited, don't do anything.
        if (this.commands == null) return;

        // Cannot update firmware if the controller is busy.
        if (this.grblController.isActive()) {
            JOptionPane.showMessageDialog(new JFrame(),
                "Cannot update firmware while it is busy.",
                ERROR, JOptionPane.ERROR_MESSAGE);
            return;
        }
        
        initialSingleStepMode = this.grblController.getController().getSingleStepMode();
        statusUpdatesEnabled = this.grblController.getController().getStatusUpdatesEnabled();
        
        // Single step mode is required for commands which modify GRBL's EEPROM.
        this.grblController.getController().setSingleStepMode(true);
        this.grblController.getController().setStatusUpdatesEnabled(false);
        this.savingSettings = true;

        // Search command array for commands and send them.
        try {            
            String command;
            for (int i=0; i < this.commands.length; i++) {
                command = this.commands[i];
                if (command != null) {
                    // If GRBL is feeling especially quick, we may need to keep
                    // setting these guys.
                    this.grblController.getController().setSingleStepMode(true);
                    this.grblController.getController().setStatusUpdatesEnabled(false);

                    this.grblController.sendGcodeCommand(command);
                    this.commands[i] = null;
                }
            }
            this.commands = null;
        } catch (Exception ex) {
            JOptionPane.showMessageDialog(new JFrame(),
                "Error from firmware while saving settings: " + ex.getMessage(),
                ERROR, JOptionPane.ERROR_MESSAGE);
        }
    }//GEN-LAST:event_saveButtonActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton closeButton;
    private javax.swing.JButton saveButton;
    private javax.swing.JTable settingsTable;
    private javax.swing.JScrollPane settingsTableScrollPane;
    // End of variables declaration//GEN-END:variables
    
    @Override
    public void controlStateChange(UGSEvent.ControlState state) {
    }

    @Override
    public void fileStreamComplete(String filename, boolean success) {
    }
    
    @Override
    public void commandComplete(GcodeCommand command) {
    }
    
    @Override
    public void commandSkipped(GcodeCommand command) {
    }

    @Override
    public void commandSent(GcodeCommand command) {
    }

    @Override
    public void commandComment(String comment) {
    }

    @Override
    public void probeCoordinates(Position p) {

    }

    @Override
    public void statusStringListener(ControllerStatus status) {
    }

    @Override
    public void postProcessData(int numRows) {
    }
}
