/*
 * @(#)SshScript.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.ssh;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * The SshScript class provides a simple way of running a set of batch scripted commands against a SSH server.  When
 * communicating with a SSH server in interactive mode (non-batch) the user must wait until the completion of the previous
 * command before entering a new command.  In interactive mode the completion of the previous command is generally indicated
 * by the display of the shell prompt which allows the user to enter the next command.  The SshScript class is especially
 * useful in automating SSH procedures such as user login and command execution.
 * <p/>
 * For simple scripting tasks see the {@link com.jscape.inet.ssh.SshSession} class.
 * <p/>
 * <p/>
 * Example Usage:<P>
 * <pre>
 * // create new Ssh instance
 * Ssh Ssh = new Ssh(hostname,username,password);
 *
 * // create new SshScript instance
 * SshScript script = new SshScript(Ssh);
 *
 * // create task to perform a directory listing
 *
 * SshTask dirListing = new SshTask("$","ls -al","$");
 *
 * // add task to script
 * script.addTask(dirListing);
 *
 * // connect to SSH server and execute SshScript
 * ssh.connect();
 *
 * // disconnect from SSH server
 * ssh.disconnect();
 * </pre>
 * <p/>
 *
 * @see com.jscape.inet.ssh.SshSession
 */

public class SshScript
        implements SshListener,
                   SshTask.Listener,
                   Serializable {

    private static final long serialVersionUID = 453189999916319349L;

    private String endOfLine;
    private LinkedList<SshTask> tasks;
    private int nextTaskIndex;
    private transient List<SshScriptListener> listeners;
    private transient OutputStream out;
    private transient SshTask currentTask;
    private transient boolean stopped;

    /**
     * Constructs a new SshScript instance.
     * When adding a new SshTask using addTask method the SshTask#setTerminator method is invoked with
     * the terminator value.  This is a convenient method of setting the terminator globally for all SshTask
     * added to a SshScript instead of setting it for each SshTask.
     *
     * @param ssh        - Ssh instance this SshScript is bound to.
     * @param endOfLine the line terminator to use when executing command for a SshTask
     *
     * @see Ssh
     * @see SshTask
     * @see #addTask
     */
    public SshScript(Ssh ssh, String endOfLine) {
        this.endOfLine = endOfLine;

        this.tasks = new LinkedList<>();
        this.listeners = new CopyOnWriteArrayList<>();

        this.out = ssh.isConnected() ? ssh.getOutputStream() : null;

        ssh.addSshListener(this);
    }

    /**
     * Constructs a new SshScript instance.
     *
     * @param ssh - the Ssh instance this SshScript is bound to.
     *
     * @see Ssh
     */
    public SshScript(Ssh ssh) {
        this(ssh, null);
    }

    /**
     * Adds a SshTask to this SshScript.
     *
     * @param task the SshTask to add
     *
     * @see SshTask
     */
    public synchronized void addTask(SshTask task) {
        assertNotNull(task);

        boolean completed = isComplete();

        this.tasks.add(task);

        if (connected() && completed) {
            activateNextTask();
        }
    }

    /**
     * Gets the named SshTask based on the name attribute of a SshTask.
     *
     * @param name the name of the SshTask to retrieve.
     *
     * @return the named SshTask if found, null otherwise
     *
     * @see SshTask
     */
    public synchronized SshTask getTask(String name) {
        for (SshTask task : this.tasks) {
            if (task.getName().equals(name)) {
                return task;
            }
        }

        return null;
    }

    /**
     * Checks if there are any SshTask bound to this SshScript
     *
     * @return true if there are SshTask, false otherwise
     */
    public synchronized boolean isEmpty() {
        return this.tasks.isEmpty();
    }

    /**
     * Checks whether this SshScript has completed execution of all SshTask.
     *
     * @return true if all tasks executed, false otherwise
     */
    public synchronized boolean isComplete() {
        return !tasksAvailable() && (this.currentTask == null || this.currentTask.isComplete());
    }

    /**
     * Clears all tasks from the script.
     */
    public synchronized void clear() {
        this.tasks.clear();
        this.nextTaskIndex = 0;
    }

    /**
     * Adds a <code>SshScriptListener</code> to this <code>SshScript</code>.
     *
     * @param listener the <code>SshScriptListener</code> instance to add
     *
     * @see SshScriptListener
     */
    public void addSshScriptListener(SshScriptListener listener) {
        assertNotNull(listener);
        this.listeners.add(listener);
    }

    /**
     * Removes a <code>SshScriptListener</code> from this SshScript
     *
     * @param listener the <code>SshScriptListener</code> instance to remove
     *
     * @see SshScriptListener
     */
    public void removeSshScriptListener(SshScriptListener listener) {
        this.listeners.remove(listener);
    }

    @Override
    public synchronized void connected(SshConnectedEvent event) {
        this.out = ((Ssh) event.getSource()).getOutputStream();
        this.stopped = false;
        activateNextTask();
    }

    @Override
    public synchronized void disconnected(SshDisconnectedEvent event) {
        this.out = null;

        if (this.currentTask != null && !this.stopped) {
            this.currentTask.onEof();
        }
    }

    @Override
    public synchronized void dataReceived(SshDataReceivedEvent event) {
        if (this.currentTask != null && !this.stopped) {
            this.currentTask.onData(event.getDataChar());
        }
    }

    @Override
    public synchronized void onTaskCompleted(SshTask task) {
        if (task != this.currentTask || this.stopped) {
            return;
        }

        raiseEvent(new SshTaskEndEvent(this, this.currentTask));
        activateNextTask();
    }

    @Override
    public synchronized void onTaskCancelled(SshTask task) {
        if (task != this.currentTask) {
            return;
        }

        this.stopped = true;
        raiseEvent(new SshTaskFailedEvent(this, this.currentTask));
    }

    @Override
    public synchronized void onTaskError(SshTask task, Exception error) {
        if (task != this.currentTask) {
            return;
        }

        this.stopped = true;
        raiseEvent(new SshTaskFailedEvent(this, this.currentTask));
    }

    @Override
    public synchronized void onTaskTimeout(SshTask task) {
        if (task != this.currentTask) {
            return;
        }

        this.stopped = true;
        raiseEvent(new SshTaskTimeoutEvent(this, this.currentTask));
    }

    private void activateNextTask() {
        if (tasksAvailable() && connected()) {
            SshTask previousTask = this.currentTask;
            this.currentTask = this.tasks.get(this.nextTaskIndex);
            ++this.nextTaskIndex;
            raiseEvent(new SshTaskStartEvent(this, this.currentTask));
            this.currentTask.activate(this, this.out, previousTask != null ? previousTask.getResponse() : "", this.endOfLine);
        } else {
            this.currentTask = null;
        }
    }

    private boolean connected() {
        return this.out != null;
    }

    private boolean tasksAvailable() {
        return this.nextTaskIndex < this.tasks.size();
    }

    private void raiseEvent(SshScriptEvent event) {
        for (SshScriptListener listener : this.listeners) {
            event.accept(listener);
        }
    }

    private void readObject(ObjectInputStream stream)
            throws IOException,
                   ClassNotFoundException {
        stream.defaultReadObject();
        this.listeners = new CopyOnWriteArrayList<>();
    }
}
