/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: ExecDialog.java
 *
 * Copyright (c) 2003, Static Free Software. All rights reserved.
 *
 * Electric(tm) 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.
 *
 * Electric(tm) 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.sun.electric.tool.user.dialogs;

import com.sun.electric.tool.user.ActivityLogger;
import com.sun.electric.tool.user.Exec;

import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.text.DefaultCaret;

/**
 * A Dialog for running and interacting with an external process. Usage:
 * <p>Create a new ExecDialog
 * <p>Call the startProcess() method.
 */
public class ExecDialog extends EDialog implements Exec.FinishedListener {

    /** Reads from process, writes to text area */
    private static class ProcessOutput extends OutputStream {

        private JTextArea area;
        private boolean processLineFeed;
        private int prevChar;

        private ProcessOutput(JTextArea area) {
            DefaultCaret caret = (DefaultCaret) area.getCaret();
            caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
            this.area = area;
            processLineFeed = true;
            if (System.getProperty("line.separator") == "\r")
                processLineFeed = false;
            prevChar = 0;
        }

        public synchronized void write(int b) throws IOException {
            byte [] bytes = new byte[1];
            bytes[0] = (byte)b;
            String str = new String(bytes);

            if (prevChar == '\r' && b != '\n' && processLineFeed) {
                try {
                    int start = area.getLineStartOffset(area.getLineCount() - 1);
                    int end = area.getLineEndOffset(area.getLineCount() - 1);
                    area.replaceRange(str, start, end);
                } catch (javax.swing.text.BadLocationException e) {
                    e.printStackTrace(System.out);
                    ActivityLogger.logException(e);                
                }
            } else {
                area.append(str);
            }

            prevChar = b;
        }

    }

    private ProcessOutput outStream;
    private ProcessOutput errStream;
    private Exec exec;
    private List<Exec.FinishedListener> finishedListenersToAdd;

    /** Creates new form ExecDialog */
    public ExecDialog(java.awt.Frame parent, boolean modal) {
        super(parent, modal);
        initComponents();
        exec = null;
        finishedListenersToAdd = new ArrayList<Exec.FinishedListener>();
		finishInitialization();
    }

    /** Do this before calling startProcess(). Listeners are only added before
     * the process starts. There is no need to remove yourself unless the
     * process has not finished yet, and you do not want to be notified.
     * @param l
     */
    public synchronized void addFinishedListener(Exec.FinishedListener l) {
        finishedListenersToAdd.add(l);
    }

    public synchronized void removeFinishedListener(Exec.FinishedListener l) {
        finishedListenersToAdd.remove(l);
    }

    /**
     * Start a process within an interactive dialog.
     * @param command the command to run (NOT a shell command!!)
     * @param envVars environment variables to use in the form name=value. If null, parent process vars are inherited
     * @param dir the working dir. If null, the parent process' working dir is used
     */
    public synchronized void startProcess(String command, String [] envVars, File dir) {
        if (exec != null) {
            System.out.println("ERROR: ExecDialog can only execute one process at a time.");
            return;
        }
        outStream = new ProcessOutput(outputTextArea);
        errStream = new ProcessOutput(outputTextArea);
        exec = new Exec(command, envVars, dir, outStream, errStream);
        exec.addFinishedListener(this);
        for (Exec.FinishedListener fl : finishedListenersToAdd) {
            exec.addFinishedListener(fl);
        }
        finishedListenersToAdd.clear();
        setTitle("External Process");
        statusLabel.setText("Running '"+command+"'...");
        setVisible(true);
        exec.start();
    }

    /**
     * Start a process within an interactive dialog.
     * @param command the command to run (NOT a shell command!!)
     * @param envVars environment variables to use in the form name=value. If null, parent process vars are inherited
     * @param dir the working dir. If null, the parent process' working dir is used
     */
    public synchronized void startProcess(String [] command, String [] envVars, File dir) {
        if (exec != null) {
            System.out.println("ERROR: ExecDialog can only execute one process at a time.");
            return;
        }
        outStream = new ProcessOutput(outputTextArea);
        errStream = new ProcessOutput(outputTextArea);
        exec = new Exec(command, envVars, dir, outStream, errStream);
        exec.addFinishedListener(this);
        for (Exec.FinishedListener l : finishedListenersToAdd) {
            exec.addFinishedListener(l);
        }
        finishedListenersToAdd.clear();
        setTitle("External Process");
        statusLabel.setText("Running "+command[0]+"...");
        setVisible(true);
        exec.start();
    }

    /**
     * Called by Exec when it is done. Satifies the Exec.FinishedListener Interface.
     * @param e a finished event.
     */
    public void processFinished(Exec.FinishedEvent e) {
        endProcess(e);
    }

    /**
     * Write one line to the process. Also writes that line to the output text area.
     * @param line the line to write
     */
    private synchronized void writeln(String line) {
        if (exec != null) {
            outputTextArea.append(">>> " +line + "\n");
            exec.writeln(line);
        }
    }

    /**
     * Clean up after getting a Exec.FinishedEvent.
     * @param e the event
     */
    private synchronized void endProcess(Exec.FinishedEvent e) {
        exec.removeFinishedListener(this);
        exec = null;
        String str;
        if (e.getExitValue() != 0) {
            JOptionPane.showMessageDialog(this, exec, "Exec '"+e.getExec()+"' failed: return value: "+e.getExitValue(), JOptionPane.ERROR_MESSAGE);
            str = "Process FAILED [exit="+e.getExitValue()+"]: '"+e.getExec()+"'\n";
        } else
            str = "Process Done [exit="+e.getExitValue()+"]: '"+e.getExec()+"'\n";
        statusLabel.setText(str);
        outputTextArea.append("*****" + str);
    }

    /** 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.
     */
    private void initComponents() {//GEN-BEGIN:initComponents
        java.awt.GridBagConstraints gridBagConstraints;

        mainPanel = new javax.swing.JPanel();
        inputTextField = new javax.swing.JTextField();
        jScrollPane1 = new javax.swing.JScrollPane();
        outputTextArea = new javax.swing.JTextArea();
        statusLabel = new javax.swing.JLabel();

        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                closeDialog(evt);
            }
        });

        mainPanel.setLayout(new java.awt.GridBagLayout());

        inputTextField.setColumns(8);
        inputTextField.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                inputTextFieldActionPerformed(evt);
            }
        });

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(0, 4, 4, 4);
        gridBagConstraints.weightx = 1.0;
        mainPanel.add(inputTextField, gridBagConstraints);

        outputTextArea.setColumns(40);
        outputTextArea.setRows(20);
        jScrollPane1.setViewportView(outputTextArea);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        mainPanel.add(jScrollPane1, gridBagConstraints);

        statusLabel.setText("Status:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4);
        mainPanel.add(statusLabel, gridBagConstraints);

        getContentPane().add(mainPanel, java.awt.BorderLayout.CENTER);

        pack();
    }//GEN-END:initComponents

    private void inputTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_inputTextFieldActionPerformed
        writeln(inputTextField.getText());
        inputTextField.setText("");
    }//GEN-LAST:event_inputTextFieldActionPerformed
    
    /** Closes the dialog */
    private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog
        setVisible(false);
        if (exec != null) {
            // need to remove self as listener, otherwise will get call back via processFinished()
            exec.removeFinishedListener(this);
            exec.destroyProcess();
        }
        dispose();
    }//GEN-LAST:event_closeDialog

    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JTextField inputTextField;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JPanel mainPanel;
    private javax.swing.JTextArea outputTextArea;
    private javax.swing.JLabel statusLabel;
    // End of variables declaration//GEN-END:variables
    
}
