/*
 * GRBL serial port interface class.
 */

/*
    Copyright 2012-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;

import static com.willwinder.universalgcodesender.AbstractCommunicator.SerialCommunicatorEvent.*;
import com.willwinder.universalgcodesender.types.GcodeCommand;
import com.willwinder.universalgcodesender.utils.CommUtils;
import com.willwinder.universalgcodesender.utils.GcodeStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.concurrent.LinkedBlockingDeque;

/**
 *
 * @author wwinder
 */
public abstract class BufferedCommunicator extends AbstractCommunicator {
    
    // Command streaming variables
    private Boolean sendPaused = false;
    private GcodeCommand nextCommand;                      // Cached command.
    private BufferedReader    rawCommandStream;            // Arbitrary number of commands
    private GcodeStreamReader commandStream;               // Arbitrary number of commands
    private LinkedBlockingDeque<String> commandBuffer;     // Manually specified commands
    private LinkedBlockingDeque<GcodeCommand> activeCommandList;  // Currently running commands
    private int sentBufferSize = 0;
    
    private Boolean singleStepModeEnabled = false;
    
    abstract public int getBufferSize();
    
    protected void setQueuesForTesting(LinkedBlockingDeque<String> cb, LinkedBlockingDeque<GcodeCommand> asl) {
        this.commandBuffer = cb;
        this.activeCommandList = asl;
    }
    
    public BufferedCommunicator() {
    }
    
    @Override
    public void setSingleStepMode(boolean enable) {
        this.singleStepModeEnabled = enable;
    }
    
    @Override
    public boolean getSingleStepMode() {
        return this.singleStepModeEnabled;
    }
    
    /**
     * Add command to the command queue outside file mode. This is the only way
     * to send a command to the comm port without being in file mode.
     * These commands will be sent prior to any queued stream, they should
     * typically be control commands calculated by the application.
     */
    @Override
    public void queueStringForComm(final String input) {        
        String commandString = input;
        
        // Add command to queue
        this.commandBuffer.add(commandString);
    }
    

    /**
     * Arbitrary length of commands to send to the communicator.
     * @param input 
     */
    @Override
    public void queueRawStreamForComm(final Reader input) {
        rawCommandStream = new BufferedReader(input);
    }

    /**
     * Arbitrary length of commands to send to the communicator.
     * @param input 
     */
    @Override
    public void queueStreamForComm(final GcodeStreamReader input) {
        commandStream = input;
    }
       
    /*
    // TODO: Figure out why this isn't working ...
    boolean isCommPortOpen() throws NoSuchPortException {
            CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(this.commPort.getName());
            String owner = portIdentifier.getCurrentOwner();
            String thisClass = this.getClass().getName();
            
            return portIdentifier.isCurrentlyOwned() && owner.equals(thisClass);                    
    }
    */
    
    /** File Stream Methods. **/
    @Override
    public void resetBuffersInternal() {
        if (activeCommandList != null) {
            activeCommandList.clear();
        }
    }

    @Override
    public String activeCommandSummary() {
        StringBuilder sb = new StringBuilder();
        String comma = "";

        for (GcodeCommand gc : activeCommandList) {
            sb.append(comma).append(gc.getCommandString());
            comma = ", ";
        }

        if (commandStream != null) {
            sb.append(comma)
                    .append(commandStream.getNumRowsRemaining())
                    .append(" streaming commands.");
        }

        return sb.toString();
    }
    
    @Override
    public boolean areActiveCommands() {
        return (this.activeCommandList.size() > 0)
                || (this.commandStream != null && this.commandStream.getNumRowsRemaining() > 0);
    }

    @Override
    public int numActiveCommands() {
        int streamingCount =
                commandStream == null ? 0 : commandStream.getNumRowsRemaining();
        return this.activeCommandList.size() + streamingCount;
    }
    
    // Helper for determining if commands should be throttled.
    private boolean allowMoreCommands() {
        if (this.singleStepModeEnabled) {
            return this.activeCommandList.isEmpty();
        }
        return true;
    }
    
    /**
     * THIS COMMAND CAN ONLY BE CALLED FROM streamCommands UNLESS
     * THE nextCommand OBJECT IS SYNCHRONIZED.
     * 
     * Returns the next command with the following priority:
     * 1. nextCommand object if set.
     * 2. Front of the commandBuffer collection.
     * 3. Next line in the commandStream.
     * @return 
     */
    private GcodeCommand getNextCommand() {
        if (nextCommand != null) {
            return nextCommand;
        }
        else if (!this.commandBuffer.isEmpty()) {
            nextCommand = new GcodeCommand(commandBuffer.pop());
        }
        else try {
            if (rawCommandStream != null && rawCommandStream.ready()) {
                nextCommand = new GcodeCommand(rawCommandStream.readLine());
            }
            else if (commandStream != null && commandStream.ready()) {
                nextCommand = commandStream.getNextCommand();
            }
        } catch (IOException ex) {
            // Fall through to null handling.
        }

        if (nextCommand != null) {
            nextCommand.setCommandNumber(getNextCommandId());
            if (nextCommand.getCommandString().endsWith("\n")) {
                nextCommand.setCommand(nextCommand.getCommandString().trim());
            }
            return nextCommand;
        }
        return null;
    }
   
    /**
     * Streams anything in the command buffer to the comm port.
     * Synchronized to prevent commands from sending out of order.
     */
    @Override
    synchronized public void streamCommands() {
        // If there are no commands to send, exit.
        if (this.getNextCommand() == null) {
            return;
        }
        
        // If streaming is paused, exit.
        if (this.sendPaused) {
            // Another NO-OP
            return;
        }
        
        // Send command if:
        // There is room in the buffer.
        // AND We are NOT in single step mode.
        // OR  We are in single command mode and there are no active commands.
        while (this.getNextCommand() != null &&
                CommUtils.checkRoomInBuffer(
                    this.sentBufferSize,
                    this.getNextCommand().getCommandString(),
                    this.getBufferSize())
                && allowMoreCommands()) {

            GcodeCommand command = this.getNextCommand();

            if (command.getCommandString().isEmpty()) {
                dispatchListenerEvents(COMMAND_SKIPPED, command);
                nextCommand = null;
                continue;
            }

            String commandString = command.getCommandString().trim();
            
            this.activeCommandList.add(command);
            this.sentBufferSize += (commandString.length() + 1);
        
            // Command already has a newline attached.
            this.sendMessageToConsoleListener(">>> " + commandString + "\n");
            
            try {
                this.sendingCommand(commandString);
                conn.sendStringToComm(commandString + "\n");
                dispatchListenerEvents(COMMAND_SENT, command);
                nextCommand = null;
            } catch (Exception e) {
                e.printStackTrace();
                System.exit(-1);
            }
        }
    }
    
    @Override
    public void pauseSend() {
        this.sendPaused = true;
    }
    
    @Override
    public void resumeSend() {
        this.sendPaused = false;
        this.streamCommands();
    }

    @Override
    public boolean isPaused() {
        return sendPaused;
    }
    
    @Override
    public void cancelSend() {
        this.nextCommand = null;
        this.commandBuffer.clear();
        this.commandStream = null;
        this.sendPaused = false;
    }
    
    /**
     * This is to allow the GRBL Ctrl-C soft reset command.
     */
    @Override
    public void softReset() {
        this.commandBuffer.clear();
        this.activeCommandList.clear();
        this.sentBufferSize = 0;
    }

    /**
     * Notifies the subclass that a command has been sent.
     * @param command The command being sent.
     */
    abstract protected void sendingCommand(String command);
    
    /**
     * Returns whether or not a command has been completed based on a response
     * from the controller.
     * @param response
     * @return true if a command has completed.
     */
    abstract protected boolean processedCommand(String response);

    /**
     * Returns whether or not a completed command had an error based on a
     * response from the controller.
     * @param response
     * @return true if a command has completed.
     */
    abstract protected boolean processedCommandIsError(String response);
    
    /** 
     * Processes message from GRBL. This should only be called from the
     * connection object.
     * @param response
     */
    @Override
    public void responseMessage(String response) {
        // Send this information back up to the Controller.
        dispatchListenerEvents(SerialCommunicatorEvent.RAW_RESPONSE, response);


        // Pause if there is an error.
        // FIXME: This prevents more commands from being sent, but does not
        //        issue a FEED HOLD event. The GUI will appear to hang if the
        //        controller does not detect the error and update the GUI.
        //
        // This is handled in two ways:
        //        1) In GUIBackend.java if we were streaming a feed hold command
        //           is sent to pause whatever command is currently running.
        //        2) In AbstractController.java commandComplete we reset
        //           sendPaused if this command wasn't part of a file stream.
        if (processedCommandIsError(response)) {
            this.sendPaused = true;
        }

        // Keep the data flow going in case of an "ok".
        if (processedCommand(response)) {
            // Pop the front of the active list.
            if (this.activeCommandList != null && this.activeCommandList.size() > 0) {
                GcodeCommand command = this.activeCommandList.pop();
                this.sentBufferSize -= (command.getCommandString().length() + 1);

                if (this.sendPaused == false) {
                    this.streamCommands();
                }
            }
        }
    }

    @Override
    public boolean openCommPort(String name, int baud) throws Exception {
        boolean ret = super.openCommPort(name, baud);
        
        if (ret) {
            this.commandBuffer = new LinkedBlockingDeque<>();
            this.activeCommandList = new LinkedBlockingDeque<>();
            this.sentBufferSize = 0;
        }
        return ret;
    }

    @Override
    public void closeCommPort() throws Exception {
        this.cancelSend();
        super.closeCommPort();
        
        this.sendPaused = false;
        this.commandBuffer = null;
        this.activeCommandList = null;
    }

    @Override
    public void sendByteImmediately(byte b) throws Exception {
        conn.sendByteImmediately(b);
    }
}
