/*
 * @(#)TelnetTask.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */

package com.jscape.inet.telnet;

import java.io.PrintStream;
import java.io.Serializable;
import java.util.regex.Pattern;

/**
 * The TelnetTask class represents a command to be executed in batch (non-interactive) mode.  In order to execute a command
 * in batch mode the TelnetTask must know the following:
 * <p/>
 * Start Prompt - The start prompt is the data that is sent back by the Telnet server indicating that it is ready to
 * execute the command. e.g. user@localhost$
 * <p/>
 * Command - The command is the command to be executed. e.g. ls -al
 * <p/>
 * End Prompt - The end prompt is the data that is sent back by the Telnet server indicating that it has completed execution
 * of the command.  In most cases this is the same as the Start Prompt.  e.g. user@localhost$
 * <p/>
 * When creating a new TelnetTask you must at a MINIMUM provide both the Command and End Prompt.  In most cases a Start Prompt should be
 * provided although you may provide a <code>null</code> value in cases where you are not waiting for Telnet server data but want the
 * command to be executed immediately.
 */
public class TelnetTask
        extends TelnetAdapter
        implements Serializable {

    /**
     * TelnetTask is waiting to be activated
     */
    public static final int PENDING = 0;

    /**
     * TelnetTask is active and waiting for start prompt to execute command
     */
    public static final int ACTIVE = 1;

    /**
     * TelnetTask has received start prompt and is currently executing command
     */
    public static final int RUNNING = 2;

    /**
     * TelnetTask has finished execution of command and has received end prompt
     */
    public static final int COMPLETE = 3;

    /**
     * The TelnetScript responsible for controlling execution of this TelnetTask
     */
    private TelnetScript script = null;

    /**
     * The next TelnetTask to execute after this TelnetTask is complete
     */
    TelnetTask nextTask = null;

    /**
     * The previous TelnetTask executed before this TelnetTask
     */
    TelnetTask prevTask = null;

    /**
     * Holds value of property start prompt.
     */
    private String startPrompt;

    /**
     * Holds value of property command.
     */
    private String command;

    /**
     * Holds value of property end prompt.
     */
    private String endPrompt;

    /**
     * Holds value of buffer waiting for start prompt
     */
    private StringBuffer buffer = new StringBuffer();

    /**
     * Holds value of response to command executed
     */
    private StringBuffer response = new StringBuffer();

    /**
     * Holds value of property status.
     */
    private int status = PENDING;

    /**
     * Holds value of property name.
     */
    private String name;

    /**
     * Holds value used for line termination when sending commands
     */
    private String terminator = "\n";

    /**
     * Hold value used for end prompt timeout
     */
    private long endPromptTimeout = 120000L;

    /**
     * Hold value used for start prompt timeout
     */
    private long startPromptTimeout = 120000L;

    /**
     * Holds whether start prompt is a regular expression
     */
    private boolean startPromptRegex = false;

    /**
     * Holds whether end prompt is a regular expression
     */
    private boolean endPromptRegex = false;

    /**
     * Holds the start prompt pattern
     */
    private Pattern startPromptPattern = null;

    /**
     * Holds the end prompt pattern
     */
    private Pattern endPromptPattern = null;

    /**
     * Flag to track whether this task has timed out
     */
    private boolean timedOut;

    /**
     * Enables/disables debug mode for the task
     */
    private boolean debug = false;

    /**
     * Debug stream to print debug messages to
     */
    private PrintStream debugStream = System.out;

    /**
     * Number of characters to match against in regular expressions
     */
    private int regexMatchLength = 20;

    private TelnetTaskMonitor telnetTaskMonitor;

    /**
     * Constructs a new <code>TelnetTask</code> instance.
     *
     * @param name        a name to uniquely identify this <code>TelnetTask</code>.
     * @param startPrompt the shell prompt this task should wait for before executing command.
     * @param command     the command to execute
     * @param endPrompt   the end shell prompt this task should wait for to indicate that this task is finished.
     */
    public TelnetTask(String name, String startPrompt, String command, String endPrompt) {
        this(startPrompt, command, endPrompt);
        this.name = name;
    }

    /**
     * Constructs a new <code>TelnetTask</code> instance.
     *
     * @param startPrompt the start data this task should wait for before executing command.
     * @param command     the command to execute
     * @param endPrompt   the end data this task should wait for to indicate that this task is finished.
     */
    public TelnetTask(String startPrompt, String command, String endPrompt) {
        if (command == null || endPrompt == null) {
            throw new IllegalArgumentException("command and endPrompt must not be null");
        }
        this.startPrompt = startPrompt;
        this.command = command;
        this.endPrompt = endPrompt;
    }


    /**
     * Gets the next task executed after this <code>TelnetTask</code>.
     *
     * @return the next <code>TelnetTask</code> or <code>null</code> if none found
     */
    public TelnetTask getNextTask() {
        return nextTask;
    }

    /**
     * Gets the <code>TelnetTask</code> executed prior to this <code>TelnetTask</code>.
     *
     * @return the previous <code>TelnetTask</code> or <code>null</code> if no previous <code>TelnetTask</code> found
     */
    public TelnetTask getPreviousTask() {
        return prevTask;
    }

    /**
     * Sets the <code>TelnetScript</code> that this <code>TelnetTask</code> is bound to.
     *
     * @param script a <code>TelnetScript</code>
     *
     * @see TelnetScript
     */
    protected void setScript(TelnetScript script) {
        this.script = script;
    }

    /**
     * Gets the <code>TelnetScript</code> this <code>TelnetTask</code> is bound to.
     *
     * @return a <code>TelnetScript</code>
     *
     * @see TelnetScript
     */
    public TelnetScript getScript() {
        return script;
    }

    /**
     * Prints debug message to debug stream
     *
     * @param msg the message to print
     */
    private void debug(String msg) {
        if (debug) {
            debugStream.println(msg);
        }
    }

    /**
     * Enables/disables additional debugging for this task.
     *
     * @param debug true if debugging is enabled, false otherwise
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Sets debug stream to be used when printing debug messages.
     * Default debug stream is System.out
     *
     * @param debugStream a PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        this.debugStream = debugStream;
    }

    /**
     * Gets debug stream used when printing debug messages.
     * Default debug stream is System.out
     *
     * @return a PrintStream
     */
    public PrintStream getDebugStream() {
        return debugStream;
    }

    /**
     * Checks whether debugging is enabled for this task.
     *
     * @return true if enabled, false otherwise
     */
    public boolean getDebug() {
        return debug;
    }


    /**
     * Gets data received from Telnet server as fired by <code>TelnetListener</code>
     *
     * @param event a <code>TelnetDataReceivedEvent</code>
     *
     * @see TelnetDataReceivedEvent
     * @see TelnetListener
     */
    public void dataReceived(TelnetDataReceivedEvent event) {
        synchronized (this) {
            if (isActive() && startPrompt != null) {
                buffer.append(event.getData());

                if (startPromptRegex) {
                    if (startPromptPattern.matcher(getCompareString(buffer, regexMatchLength)).find()) {
                        debug("Found start prompt matching regex \"" + startPrompt + "\" for task \"" + getName() + "\"" + " in response \"" + buffer.toString() + "\"");
                        if (prevTask != null) {
                            if (prevTask.isComplete()) {
                                execute();
                            }
                        } else {
                            execute();
                        }
                    }
                } else {
                    // data is found
                    if (getCompareString(buffer, startPrompt.length()).endsWith(startPrompt)) {
                        debug("Found start prompt matching \"" + startPrompt + "\" for task \"" + getName() + "\"" + " in response \"" + buffer.toString() + "\"");
                        if (prevTask != null) {
                            if (prevTask.isComplete()) {
                                execute();
                            }
                        } else {
                            execute();
                        }
                    }
                }
            } else if (isRunning()) {
                response.append(event.getData());

                if (endPromptRegex) {
                    if (endPromptPattern.matcher(getCompareString(response, regexMatchLength)).find()) {
                        debug("Found end prompt matching regex \"" + endPrompt + "\" for task \"" + getName() + "\"" + " in response \"" + response.toString() + "\"");
                        setStatus(COMPLETE);
                        script.telnet.removeTelnetListener(this);
                    }
                } else {
                    if (getCompareString(response, endPrompt.length()).endsWith(endPrompt)) {
                        debug("Found end prompt matching \"" + endPrompt + "\" for task \"" + getName() + "\"" + " in response \"" + response.toString() + "\"");
                        setStatus(COMPLETE);
                        script.telnet.removeTelnetListener(this);
                    }
                }
            }
        }
    }

    /**
     * Gets a substring from a StringBuffer
     *
     * @param buffer          the StringBuffer
     * @param maxBufferLength the maximum number of characters to get starting from end of StringBuffer
     *
     * @return a String
     */
    private String getCompareString(StringBuffer buffer, int maxBufferLength) {
        int bufferLength = buffer.length();
        if (maxBufferLength > bufferLength) {
            return buffer.toString();
        } else {
            return buffer.substring(bufferLength - maxBufferLength);
        }
    }

    /**
     * Executes command as defined in constructor.  If command fails then a <code>TelnetTaskFailedEvent</code> will be generated and sent
     * to all registered <code>TelnetScriptListener</code>.
     *
     * @see TelnetScriptListener
     * @see TelnetTaskFailedEvent
     */
    protected void execute() {
        debug("Executing command \"" + command + "\" for task \"" + getName() + "\"");

        // start next task to listen
        if (nextTask != null) {
            nextTask.setStatus(ACTIVE);
            script.telnet.addTelnetListener(nextTask);
        }
        // change status
        setStatus(RUNNING);

        // execute command
        TelnetOutputStream out = (TelnetOutputStream) script.telnet.getOutputStream();
        try {
            out.println(command, terminator);
            out.flush();
            script.telnet.debug(command + terminator);
        } catch (Exception e) {
            script.fireTaskFailed(this, e);
        }
    }

    /**
     * Gets the command to execute
     *
     * @return the command to execute
     */
    public String getCommand() {
        return this.command;
    }

    /**
     * Gets the shell prompt this <code>TelnetTask</code> should wait for before executing command.
     *
     * @return the start shell prompt
     */
    public String getStartPrompt() {
        return this.startPrompt;
    }

    /**
     * Gets the end shell prompt this <code>TelnetTask</code> should wait for to indicate that this <code>TelnetTask</code> is complete.
     *
     * @return the end shell prompt
     */
    public String getEndPrompt() {
        return this.endPrompt;
    }

    /**
     * Gets completion status of this <code>TelnetTask</code>.
     *
     * @return the completion status of this <code>TelnetTask</code>
     *
     * @see #PENDING
     * @see #ACTIVE
     * @see #RUNNING
     * @see #COMPLETE
     */
    public int getStatus() {
        return this.status;
    }

    /**
     * Gets response to command executed.  Response will be empty if command has not executed.
     *
     * @return the response to command executed
     *
     * @see #isComplete
     */
    public String getResponse() {
        return response.toString();
    }

    /**
     * Sets completion status of this <code>TelnetTask</code>.
     *
     * @param status the completion status of this <code>TelnetTask</code>.
     *
     * @see #PENDING
     * @see #ACTIVE
     * @see #RUNNING
     * @see #COMPLETE
     */
    protected void setStatus(int status) {
        this.status = status;
        debug("Changed task status for task \"" + getName() + "\"" + " running command \"" + getCommand() + "\" to \"" + getStatusString(status) + "\"");
        if (status == RUNNING) {
            telnetTaskMonitor = new TelnetTaskMonitor(this);
            telnetTaskMonitor.start();
            script.fireTaskStart(this);
        } else if (status == ACTIVE) {
            TelnetTaskActiveMonitor monitor = new TelnetTaskActiveMonitor(this);
            monitor.start();
        } else if (status == COMPLETE) {
            script.fireTaskEnd(this);
        }
        debug("Change task status for task \"" + getName() + "\"" + " complete.");
    }

    /**
     * Gets status string based on task status
     *
     * @param status the status code
     *
     * @return the status
     *
     * @see #PENDING
     * @see #ACTIVE
     * @see #RUNNING
     * @see #COMPLETE
     */
    public String getStatusString(int status) {
        String tmp = "Unknown";
        switch (status) {
            case PENDING:
                tmp = "Pending";
                break;
            case ACTIVE:
                tmp = "Active";
                break;
            case RUNNING:
                tmp = "Running";
                break;
            case COMPLETE:
                tmp = "Complete";
                break;
            default:
                break;
        }
        return tmp;
    }


    /**
     * Checks if this <code>TelnetTask</code> is pending activation.
     *
     * @return <code>true</code> if pending activation, <code>false</code> otherwise
     */
    public boolean isPending() {
        if (status == PENDING) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if this <code>TelnetTask</code> is active.
     *
     * @return <code>true</code> if active, <code>false</code> otherwise
     */
    public boolean isActive() {
        if (status == ACTIVE) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * Checks if this <code>TelnetTask</code> is running.
     *
     * @return <code>true</code> if running, <code>false</code> otherwise
     */
    public boolean isRunning() {
        if (status == RUNNING) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Checks if this <code>TelnetTask</code> is complete.
     *
     * @return <code>true</code> if complete, <code>false</code> otherwise
     */
    public boolean isComplete() {
        if (status == COMPLETE) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Gets name of this <code>TelnetTask</code> as defined in constructor.
     *
     * @return name of this <code>TelnetTask</code>, <code>null</code> if not defined.
     */
    public String getName() {
        return this.name;
    }

    /**
     * Gets line terminator used when sending commands.
     * Default is LF (Line Feed).
     *
     * @return line terminator
     */
    public String getTerminator() {
        return terminator;
    }

    /**
     * Sets line terminator used when sending commands.
     * Default is LF (Line Feed).
     *
     * @param string line terminator
     */
    public void setTerminator(String string) {
        terminator = string;
    }

    /**
     * Cancels this task and subsequent tasks by setting it's status to complete.
     */
    public void cancel() {
        setStatus(COMPLETE);
        if (this.nextTask != null) {
            this.nextTask.cancel();
        }
        if (telnetTaskMonitor != null) {
            telnetTaskMonitor.interrupt();
        }
    }


    /**
     * Gets the maximum timeout between the time the task is in running state and the expected end prompt is received.
     *
     * @return the timeout in milliseconds
     */
    public long getEndPromptTimeout() {
        return endPromptTimeout;
    }


    /**
     * Gets the maximum timeout between the time the task is in active state and the expected start prompt is received.
     *
     * @return the timeout in milliseconds
     */
    public long getStartPromptTimeout() {
        return startPromptTimeout;
    }


    /**
     * Sets the maximum timeout between the time the task is in running state and the expected end prompt is received.
     *
     * @param endPromptTimeout the timeout in milliseconds
     */
    public void setEndPromptTimeout(long endPromptTimeout) {
        this.endPromptTimeout = endPromptTimeout;
    }

    /**
     * Sets the maximum timeout between the time the task is in active state and the expected start prompt is received.
     *
     * @param startPromptTimeout the timeout in milliseconds
     */
    public void setStartPromptTimeout(long startPromptTimeout) {
        this.startPromptTimeout = startPromptTimeout;
    }

    /**
     * Monitors a running TelnetTask instance.
     * <p/>
     * Throws a RuntimeException if task end prompt is not received within defined timeout.
     */
    class TelnetTaskMonitor
            extends Thread {
        private TelnetTask task = null;

        /**
         * Constructs a new TelnetTaskMonitor instance.
         *
         * @param task the TelnetTask to monitor
         */
        TelnetTaskMonitor(TelnetTask task) {
            this.task = task;
        }

        /**
         * Continues polling TelnetTask until task is complete or timeout is exceeded.
         */
        public void run() {
            long time = 0L;
            while (task.isRunning()) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    // todo: ignore?
                }
                time += 1000L;
                if (time >= task.getEndPromptTimeout()) {
                    task.timedOut = true;
                    task.cancel();
                    script.fireTaskTimeout(task);
                }
            }
        }
    }


    /**
     * Monitors an active TelnetTask instance.
     * <p/>
     * Throws a RuntimeException if start prompt is not received within defined timeout.
     */
    class TelnetTaskActiveMonitor
            extends Thread {
        private TelnetTask task = null;

        /**
         * Constructs a new TelnetTaskMonitor instance.
         *
         * @param task the TelnetTask to monitor
         */
        TelnetTaskActiveMonitor(TelnetTask task) {
            this.task = task;
        }

        /**
         * Continues polling TelnetTask until task switches state or timeout is exceeded.
         */
        public void run() {
            long time = 0L;
            while (task != null && task.isActive()) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    // todo: ignore?
                }
                time += 1000L;
                if (time >= task.getEndPromptTimeout()) {
                    try {
                        task.timedOut = true;
                        task.cancel();
                        script.fireTaskTimeout(task);
                    } catch (NullPointerException ne) {
                        //ignore since task has been set to null
                    }
                }
            }
        }
    }


    /**
     * Sets the number of characters to read from the end of the response buffer when using regular expressions
     * to detect start or end prompts.  Default value is 20.
     *
     * @param regexMatchLength
     */
    public void setRegexMatchLength(int regexMatchLength) {
        this.regexMatchLength = regexMatchLength;
    }

    /**
     * Gets the number of characters to read from the end of the response buffer when using regular expressions
     * to detect start or end prompts.  Default value is 20.
     *
     * @return match length
     */
    public int getRegexMatchLength() {
        return regexMatchLength;
    }

    /**
     * Checks if task has exceeded timeout
     *
     * @return true if timeout exceeded, false otherwise
     */
    public boolean isTimedOut() {
        return timedOut;
    }

    /**
     * Checks if end prompt is a regular expression
     *
     * @return true if is a regular expression, false otherwise
     */
    public boolean isEndPromptRegex() {
        return endPromptRegex;
    }

    /**
     * Specified if start prompt is a regular expression
     *
     * @return true if is a regular expression, false otherwise
     */
    public boolean isStartPromptRegex() {
        return startPromptRegex;
    }

    /**
     * Specifies if end prompt is a regular expression
     *
     * @param b true if end prompt is a regular expression, false otherwise
     *
     * @throws TelnetException if the end prompt is an invalid regular expression
     * @see #setRegexMatchLength
     */
    public void setEndPromptRegex(boolean b)
            throws TelnetException {
        if (b) {
            try {
                endPromptPattern = Pattern.compile(endPrompt);
            } catch (Exception mpe) {
                throw new TelnetException("Invalid end prompt regex: " + mpe.getMessage());
            }

        } else {
            endPromptPattern = null;
        }
        endPromptRegex = b;
    }

    /**
     * Specifies if start prompt is a regular expression
     *
     * @param b true if start prompt is a regular expression, false otherwise
     *
     * @throws TelnetException if the start prompt is an invalid regular expression
     * @see #setRegexMatchLength
     */
    public void setStartPromptRegex(boolean b)
            throws TelnetException {
        if (b) {
            try {
                startPromptPattern = Pattern.compile(startPrompt);
            } catch (Exception mpe) {
                throw new TelnetException("Invalid start prompt regex: " + mpe.getMessage());
            }

        } else {
            startPromptPattern = null;
        }
        startPromptRegex = b;
    }
}
