content: /**
   *  Copyright (c) 2013-2014 Angelo ZERR.
   *  All rights reserved. This program and the accompanying materials
   *  are made available under the terms of the Eclipse Public License v1.0
   *  which accompanies this distribution, and is available at
   *  http://www.eclipse.org/legal/epl-v10.html
   *
   *  Contributors:
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   */
  package tern.server.nodejs.npm;
  
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.LinkedList;
  import java.util.List;
  
  import tern.TernException;
  import tern.server.ITernModule;
  import tern.server.nodejs.process.NodejsProcessException;
  
  public class NPMProcess {
  
  	/**
  	 * The node.js base dir.
  	 */
  	private final String npmPath;
  
  	/**
  	 * The project dir where .tern-project is hosted.
  	 */
  	private final File projectDir;
  
  	/**
  	 * node.js process.
  	 */
  	private Process process;
  
  	/**
  	 * StdOut thread.
  	 */
  	private Thread outThread;
  
  	/**
  	 * StdErr thread.
  	 */
  	private Thread errThread;
  
  	private boolean hasError;
  
  	/**
  	 * Process listeners.
  	 */
  	private final List<INPMProcessListener> listeners;
  
  	public NPMProcess(String npmPath, File projectDir) throws TernException {
  		this.npmPath = npmPath;
  		this.projectDir = projectDir;
  		this.listeners = new ArrayList<INPMProcessListener>();
  		this.hasError = false;
  	}
  
  	/**
  	 * Start process.
  	 * 
  	 * @throws IOException
  	 * @throws InterruptedException
  	 */
  	public void install(ITernModule module) throws NodejsProcessException {
  		if (isStarted()) {
  			notifyErrorProcess("Nodejs tern Server is already started.");
  			throw new NodejsProcessException(
  					"Nodejs tern Server is already started.");
  		}
  
  		try {
  			List<String> commands = createCommands(module);
  			ProcessBuilder builder = new ProcessBuilder(commands);
  			// builder.redirectErrorStream(true);
  			builder.directory(getProjectDir());
  			notifyCreateProcess(commands, projectDir);
  
  			this.process = builder.start();
  
  			outThread = new Thread(new StdOut());
  			outThread.setDaemon(true);
  			outThread.start();
  
  			errThread = new Thread(new StdErr());
  			errThread.setDaemon(true);
  			errThread.start();
  
  		} catch (Throwable e) {
  			notifyErrorProcess(e.getMessage());
  			notifyErrorProcess("");
  			throw new NodejsProcessException(e);
  		}
  	}
  	
  	
  	
  	/**
  	 * Create process commands to start tern with node.js
  	 * 
  	 * @return
  	 * @throws IOException
  	 */
  	/*protected List<String> createCommands() {
  		List<String> commands = new LinkedList<String>();
  		if (nodejsBaseDir == null) {
  			// for osx, path of node.js should be setted?
  			if (new File("/usr/local/bin/node").exists()) {
  				commands.add("/usr/local/bin/node");
  			}
  			if (new File("/opt/local/bin/node").exists()) {
  				commands.add("/opt/local/bin/node");
  			} else {
  				commands.add("npm");
  			}
  		} else {
  			commands.add(nodejsBaseDir.getPath());
  		}
  		return commands;
  	}*/
  	
  	private List<String> createCommands(ITernModule module) {		
  		List<String> cmds = new ArrayList<String>();
  		cmds.add(npmPath);
  		cmds.add("install");
  		cmds.add(new StringBuilder("tern-").append(module.getName()).toString());
  		return cmds;
  	}
  
  	/**
  	 * Return the project dir of the tern repository;
  	 * 
  	 * @return
  	 */
  	public File getProjectDir() {
  		return projectDir;
  	}
  
  	/**
  	 * StdOut of the node.js process.
  	 */
  	private class StdOut implements Runnable {
  
  		@Override
  		public void run() {
  			try {
  
  				long startTime = System.nanoTime();
  				Integer port = null;
  				String line = null;
  				InputStream is = process.getInputStream();
  				InputStreamReader isr = new InputStreamReader(is);
  				BufferedReader br = new BufferedReader(isr);
  				try {
  					while ((line = br.readLine()) != null) {
  						/*if (port == null) {
  							// port was not getted, try to get it.
  							if (line.startsWith("Listening on port ")) {
  								port = Integer.parseInt(line.substring(
  										"Listening on port ".length(),
  										line.length()));
  
  								// port is getted, notify that process is
  								// started.
  								// setPort(port);
  
  								//synchronized (lock) {
  								//	lock.notifyAll();
  								//}
  								notifyStartProcess(startTime);
  							}
  						} else {*/
  							// notify data
  							notifyDataProcess(line);
  						//}
  					}
  				} catch (IOException e) {
  					e.printStackTrace();
  				}
  
  				if (process != null) {
  					process.waitFor();
  				}
  				notifyStopProcess();
  				kill();
  			} catch (InterruptedException e) {
  				Thread.currentThread().interrupt();
  			}
  		}
  	};
  
  	/**
  	 * StdErr of the node.js process.
  	 */
  	private class StdErr implements Runnable {
  		@Override
  		public void run() {
  			String line = null;
  			InputStream is = process.getErrorStream();
  			InputStreamReader isr = new InputStreamReader(is);
  			BufferedReader br = new BufferedReader(isr);
  			try {
  				while ((line = br.readLine()) != null) {
  					notifyErrorProcess(line);
  				}
  			} catch (IOException e) {
  				e.printStackTrace();
  			}
  		}
  	}
  
  	/**
  	 * Return true id process is started and false otherwise.
  	 * 
  	 * @return
  	 */
  	public boolean isStarted() {
  		return process != null;
  	}
  
  	/**
  	 * Kill the process.
  	 */
  	public void kill() {
  		if (process != null) {
  			process.destroy();
  			process = null;
  		}
  		if (outThread != null) {
  			outThread.interrupt();
  			outThread = null;
  		}
  		if (errThread != null) {
  			errThread.interrupt();
  			errThread = null;
  		}
  	}
  
  	public void addProcessListener(INPMProcessListener listener) {
  		synchronized (listeners) {
  			listeners.add(listener);
  		}
  	}
  
  	/**
  	 * Remove the given process listener.
  	 * 
  	 * @param listener
  	 */
  	public void removeProcessListener(INPMProcessListener listener) {
  		synchronized (listeners) {
  			listeners.remove(listener);
  		}
  	}
  
  	/**
  	 * Notify start process.
  	 */
  	private void notifyCreateProcess(List<String> commands, File projectDir) {
  		synchronized (listeners) {
  			for (INPMProcessListener listener : listeners) {
  				listener.onCreate(this, commands, projectDir);
  			}
  		}
  	}
  
  	/**
  	 * Notify start process.
  	 * 
  	 * @param startTime
  	 *            time when node.js process is started.
  	 */
  	private void notifyStartProcess(long startTime) {
  		// this.elapsedSartTime =
  		// NodejsTernHelper.getElapsedTimeInMs(startTime);
  		synchronized (listeners) {
  			for (INPMProcessListener listener : listeners) {
  				listener.onStart(this);
  			}
  		}
  	}
  
  	/**
  	 * Notify stop process.
  	 */
  	private void notifyStopProcess() {
  		synchronized (listeners) {
  			for (INPMProcessListener listener : listeners) {
  				listener.onStop(this);
  			}
  		}
  	}
  
  	/**
  	 * Notify data process.
  	 * 
  	 * @param line
  	 */
  	private void notifyDataProcess(String line) {
  		synchronized (listeners) {
  			for (INPMProcessListener listener : listeners) {
  				listener.onData(this, line);
  			}
  		}
  	}
  
  	/**
  	 * Notify error process.
  	 */
  	private void notifyErrorProcess(String line) {
  		this.hasError = true;
  		synchronized (listeners) {
  			for (INPMProcessListener listener : listeners) {
  				listener.onError(NPMProcess.this, line);
  			}
  		}
  	}
  }
  