/*******************************************************************************
 * Copyright (c) 2016, 2018 Red Hat.
 * 
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/

package org.eclipse.linuxtools.internal.docker.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.linuxtools.docker.core.Activator;
import org.eclipse.linuxtools.docker.core.DockerCommandNotFoundException;
import org.eclipse.linuxtools.docker.core.DockerException;

/**
 * A utility class to run Java {@link Process} such as {@code docker-machine} or
 * {@code docker-compose}.
 */
public class ProcessLauncher {

	public static final String COMMAND_LINE = "org.eclipse.linuxtools.docker.compose.commandLine";

	/**
	 * Checks that the given {@code cmdName} exists in the given {@code path}
	 * 
	 * @param path
	 *            the system {@code PATH} to use to look for the command
	 * @param cmdName
	 *            the name of the command in the given {@code cmdDir}
	 * @return <code>true</code> if the command was found, <code>false</code>
	 *         otherwise.
	 */
	public boolean checkPathToCommand(final String path, final String cmdName) {
		return Stream.of(path.split(File.pathSeparator))
				.map(pathFragment -> new File(pathFragment, cmdName))
				.anyMatch(fullPath -> fullPath.exists());
	}

	/**
	 * Entry method to run a command.
	 * 
	 * @param baseCmdDir
	 *            the base directory containing the command to run
	 * @param cmdName
	 *            the name of the command to run
	 * @throws DockerException
	 *             if the {@code baseCmdDir} did not contain the specific
	 *             {@code cmdName}
	 */
	public FluentProcessBuilder processBuilder(final String baseCmdDir,
			final String cmdName, final String[] args) throws DockerException {
		return new FluentProcessBuilder(baseCmdDir, cmdName, args);
	}

	public static class FluentProcessBuilder {

		/** the {@link ProcessBuilder} to use to run the command. */
		private final ProcessBuilder processBuilder;

		/**
		 * Constructor.
		 * 
		 * @param baseCmdDir
		 *            the base directory containing the command to run
		 * @param cmdName
		 *            the name of the command to run
		 * @throws DockerException
		 *             if the {@code baseCmdDir} did not contain the specific
		 *             {@code cmdName}
		 */
		private FluentProcessBuilder(final String baseCmdDir,
				final String cmdName, final String[] args)
				throws DockerException {
			// check that the 'cmdName' can be found in the given
			// 'baseCmdDir'
			final boolean commandExists = new ProcessLauncher()
					.checkPathToCommand(baseCmdDir, cmdName);
			if (!commandExists) {
				throw new DockerCommandNotFoundException(
						ProcessMessages.getFormattedString("Command_Not_Found", //$NON-NLS-1$
								baseCmdDir, cmdName));
			}
			final String[] command = new String[args.length + 1];
			command[0] = Paths.get(baseCmdDir, cmdName).toString();
			System.arraycopy(args, 0, command, 1, args.length);
			this.processBuilder = new ProcessBuilder(command);
		}

		public FluentProcessBuilder extraPath(final String... extraPaths) {
			final Map<String, String> environment = processBuilder
					.environment();
			final StringBuilder path = new StringBuilder();
			for (String extraPath : extraPaths) {
				path.append(File.pathSeparator).append(extraPath);
			}
			final String newEnvPath = environment.get("PATH") //$NON-NLS-1$
					+ path.toString();
			environment.put("PATH", newEnvPath); //$NON-NLS-1$
			return this;
		}

		public FluentProcessBuilder workingDir(final String workingDir) {
			this.processBuilder.directory(new File(workingDir));
			return this;
		}

		public FluentProcessBuilder redirectErrorStream(
				boolean redirectErrorStream) {
			this.processBuilder.redirectErrorStream(redirectErrorStream);
			return this;
		}

		public String getCommand() {
			return this.processBuilder.command().stream()
					.collect(Collectors.joining(" "));
		}

		/**
		 * Starts the process from the current settings.
		 * 
		 * @return the {@link Process} that was started by this process builder
		 * @throws DockerException
		 *             if an error occurred
		 */
		public Process start() throws DockerException {
			try {
				return this.processBuilder.start();
			} catch (Throwable e) {
				throw new DockerException(ProcessMessages
						.getFormattedString("Process_Start_Exception", //$NON-NLS-1$
								processBuilder.command().stream()
										.collect(Collectors.joining(" ")),
								e.getMessage()),
						e);
			}
		}

		/**
		 * Starts the {@link Process} from the current settings and returns the
		 * output.
		 * 
		 * @return the process output once it has completed.
		 * @throws DockerException
		 *             if an error occurred
		 */
		public String[] startAndGetResult() throws DockerException {
			final Process process = start();
			return extractResult(process);
		}

		private String[] extractResult(final Process process) {
			try {
			process.waitFor();
			if (process.exitValue() == 0) {
				final List<String> result = new ArrayList<>();
				try (final InputStream inputStream = process.getInputStream();
						final BufferedReader buff = new BufferedReader(
								new InputStreamReader(inputStream))) {
					String line;
					while ((line = buff.readLine()) != null) {
						result.add(line);
					}
				}
				return result.toArray(new String[0]);
			} else {
					final StringBuilder errorMessage = new StringBuilder();
				try (final InputStream errorStream = process.getErrorStream();
						final BufferedReader buff = new BufferedReader(
								new InputStreamReader(errorStream))) {
					String line;
					while ((line = buff.readLine()) != null) {
						errorMessage.append(line).append('\n'); // $NON-NLS-1$
					}
				}
				Activator.log(new Status(IStatus.WARNING, Activator.PLUGIN_ID,
						ProcessMessages.getFormattedString("Process_Error", //$NON-NLS-1$
											this.getCommand(),
								process.exitValue(), errorMessage.toString())));
			}
			} catch (IOException | InterruptedException e) {
				Activator.log(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
						ProcessMessages.getFormattedString("Process_Exception", //$NON-NLS-1$
								this.getCommand(), e.getMessage()),
						e));
			}
			return new String[0];
		}
	}

}