/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package jp.sourceforge.dvibrowser.dvicore.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import jp.sourceforge.dvibrowser.dvicore.DviException;


public class CommandShell
{
  public static final int RETCODE_SUCCESS = 0;
  
  private final ScheduledExecutorService exe = Executors.newSingleThreadScheduledExecutor
    (new DaemonThreadFactory());
  
  private static final Logger LOGGER = Logger.getLogger(CommandShell.class.getName());
  private ArrayList<String> commandLine = new ArrayList<String>();

  public void setCommandLine(String ... commandLine)
  {
    ArrayList<String> list = new ArrayList<String>();
    for (String arg : commandLine) {
      list.add(arg);
    }
    this.commandLine = list;
  }

  public void setCommandLine(Collection<String> cmdLine)
  {
    ArrayList<String> list = new ArrayList<String>();
    list.addAll(cmdLine);
    this.commandLine = list;
  }
  
  public Collection<String> getCommandLine()
  {
    return Collections.unmodifiableCollection(commandLine);
  }

  private ArrayList<String> envs = null;
  public void setEnvironment(Collection<String> envs)
  {
    if (envs == null) {
      this.envs = null;
      return;
    } else {
      ArrayList<String> list = new ArrayList<String>();
      list.addAll(envs);
      this.envs = list;
    }
  }

  public void setEnvironment(String ... envs)
  {
    ArrayList<String> list = new ArrayList<String>();
    for (String arg : envs) {
      list.add(arg);
    }
    this.envs = list;
  }
  
  public Collection<String> getEnvironment()
  {
    if (envs == null) return null;
    return Collections.unmodifiableCollection(envs);
  }

  private File dir = null;
  public void setWorkingDirectory(File dir)
  {
    this.dir = dir;
  }
  
  public File getWorkingDirectory()
  {
    return dir;
  }
  
  private TimeUnit timeUnit;
  private long timeout = 0;
  public void setTimeout(long timeout, TimeUnit timeUnit)
  {
    if (timeout < 0) {
      throw new IllegalArgumentException("timeout is negative");
    }
    this.timeout = timeout;
    this.timeUnit = timeUnit;
  }

  private CommandShellHandler handler = null;
  public void setHandler(CommandShellHandler handler)
  {
    this.handler = handler;
  }
  
  public CommandShellHandler getHandler()
  {
    return handler;
  }

  protected void checkVars()
  {
    if (commandLine == null)
      throw new IllegalArgumentException
        ("commandLine can't be null");
    if (commandLine.size() < 1)
      throw new IllegalArgumentException
        ("commandLine can't be empty");
  }

  private volatile Process p = null;
  private Thread stdoutThread = null;
  private Thread stderrThread = null;
  private Thread stdinThread = null;
  private volatile Throwable stdoutThrowable = null;
  private volatile Throwable stderrThrowable = null;
  private volatile Throwable stdinThrowable = null;

  public int execute()
  throws IOException, InterruptedException, DviException
  {
    int result = -1;

    p = null;
    checkVars();

    try {
      final String commandLineStr = DviUtils.join(" ", commandLine);
      LOGGER.fine("Running command: " + commandLineStr);
      p = Runtime.getRuntime().exec(
          commandLine.toArray(new String[commandLine.size()]),
          getEnvironmentAsArray(), dir);
      processStreams();
      ScheduledFuture<?> future = null;
      if (timeUnit != null && timeout > 0) {
        LOGGER.fine("Starting timer: timeout=" + timeout + " timeUnit=" + timeUnit);
        future = exe.schedule(new Runnable() {
          public void run() {
            LOGGER.warning("Command timed out: " + commandLineStr);
            p.destroy();
          }
        }, timeout, timeUnit);
      }
      LOGGER.fine("waiting for the process to terminate.");
      result = p.waitFor();
      LOGGER.fine("process exit with retcode " + result);
      if (future != null) {
        future.cancel(false);
      }
    } catch (InterruptedException ex) {
      if (p != null) {
        p.destroy();
      }
      throw ex;
    } catch (IOException ex) {
      if (p != null) {
        p.destroy();
      }
      throw ex;
    } finally {
      reapThread(stderrThread, "stderr");
      stderrThread = null;
      reapThread(stdoutThread, "stdout");
      stdoutThread = null;
      reapThread(stdinThread, "stdin");
      stdinThread = null;
      
      if (p != null) {
        DviUtils.silentClose(p.getErrorStream());
        DviUtils.silentClose(p.getInputStream());
        DviUtils.silentClose(p.getOutputStream());
      }
    }
    
    if (stdinThrowable != null) {
      throw new DviException(stdinThrowable);
    }
    if (stdoutThrowable != null) {
      throw new DviException(stdoutThrowable);
    }
    if (stderrThrowable != null) {
      throw new DviException(stderrThrowable);
    }
    
    p = null;

    return result;
  }

  private String[] getEnvironmentAsArray() {
    if (envs == null) return null;
    return envs.toArray(new String[envs.size()]);
  }

  private void reapThread(Thread thread, String name)
  {
    try {
      LOGGER.fine("waiting for the " + name + " thread");
      if (thread != null)
        thread.join();
    } catch (InterruptedException ex) {
      DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
    }
  }

  protected void processStreams()
  throws IOException
  {
    if (handler != null) {
      stdinThread = new Thread(new Runnable() {
        public void run() {
          OutputStream os = p.getOutputStream();
          try {
            handler.handleStdin(os);
          } catch (Throwable ex) {
            DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
            stdinThrowable = ex;
            p.destroy();
          } finally {
            DviUtils.silentClose(os);
          }
        }
      });
      stderrThread = new Thread(new Runnable() {
        public void run() {
          InputStream is = p.getErrorStream();
          try {
            handler.handleStderr(is);
          } catch (Throwable ex) {
            DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
            stderrThrowable = ex;
            p.destroy();
          } finally {
            DviUtils.silentClose(is);
          }
        }
      });
      stdoutThread = new Thread(new Runnable() {
        public void run() {
          InputStream is = p.getInputStream();
          try {
            handler.handleStdout(is);
          } catch (Throwable ex) {
            DviUtils.logStackTrace(LOGGER, Level.WARNING, ex);
            stdoutThrowable = ex;
            p.destroy();
          } finally {
            DviUtils.silentClose(is);
          }
        }
      });

      stdinThread .start();
      stderrThread.start();
      stdoutThread.start();
    } else {
      DviUtils.silentClose(p.getOutputStream());
      DviUtils.silentClose(p.getInputStream());
      DviUtils.silentClose(p.getErrorStream());
    }
  }
}
