/*
 * Copyright 2012-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.loader.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;

import org.springframework.util.ReflectionUtils;

/**
 * Utility used to run a process.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @since 1.1.0
 */
public class RunProcess {

	private static final Method INHERIT_IO_METHOD = ReflectionUtils
			.findMethod(ProcessBuilder.class, "inheritIO");

	private static final long JUST_ENDED_LIMIT = 500;

	private File workingDirectory;

	private final String[] command;

	private volatile Process process;

	private volatile long endTime;

	/**
	 * Creates new {@link RunProcess} instance for the specified command.
	 * @param command the program to execute and its arguments
	 */
	public RunProcess(String... command) {
		this(null, command);
	}

	/**
	 * Creates new {@link RunProcess} instance for the specified working directory and
	 * command.
	 * @param workingDirectory the working directory of the child process or {@code null}
	 * to run in the working directory of the current Java process
	 * @param command the program to execute and its arguments
	 */
	public RunProcess(File workingDirectory, String... command) {
		this.workingDirectory = workingDirectory;
		this.command = command;
	}

	public int run(boolean waitForProcess, String... args) throws IOException {
		return run(waitForProcess, Arrays.asList(args));
	}

	protected int run(boolean waitForProcess, Collection<String> args)
			throws IOException {
		ProcessBuilder builder = new ProcessBuilder(this.command);
		builder.directory(this.workingDirectory);
		builder.command().addAll(args);
		builder.redirectErrorStream(true);
		boolean inheritedIO = inheritIO(builder);
		try {
			Process process = builder.start();
			this.process = process;
			if (!inheritedIO) {
				redirectOutput(process);
			}
			SignalUtils.attachSignalHandler(this::handleSigInt);
			if (waitForProcess) {
				try {
					return process.waitFor();
				}
				catch (InterruptedException ex) {
					Thread.currentThread().interrupt();
					return 1;
				}
			}
			return 5;
		}
		finally {
			if (waitForProcess) {
				this.endTime = System.currentTimeMillis();
				this.process = null;
			}
		}
	}

	private boolean inheritIO(ProcessBuilder builder) {
		if (isInheritIOBroken()) {
			return false;
		}
		try {
			INHERIT_IO_METHOD.invoke(builder);
			return true;
		}
		catch (Exception ex) {
			return false;
		}
	}

	// There's a bug in the Windows VM (https://bugs.openjdk.java.net/browse/JDK-8023130)
	// that means we need to avoid inheritIO
	private static boolean isInheritIOBroken() {
		if (!System.getProperty("os.name", "none").toLowerCase().contains("windows")) {
			return false;
		}
		String runtime = System.getProperty("java.runtime.version");
		if (!runtime.startsWith("1.7")) {
			return false;
		}
		String[] tokens = runtime.split("_");
		if (tokens.length < 2) {
			return true; // No idea actually, shouldn't happen
		}
		try {
			Integer build = Integer.valueOf(tokens[1].split("[^0-9]")[0]);
			if (build < 60) {
				return true;
			}
		}
		catch (Exception ex) {
			return true;
		}
		return false;
	}

	private void redirectOutput(Process process) {
		final BufferedReader reader = new BufferedReader(
				new InputStreamReader(process.getInputStream()));
		new Thread(() -> {
			try {
				String line = reader.readLine();
				while (line != null) {
					System.out.println(line);
					line = reader.readLine();
					System.out.flush();
				}
				reader.close();
			}
			catch (Exception ex) {
				// Ignore
			}
		}).start();
	}

	/**
	 * Return the running process.
	 * @return the process or {@code null}
	 */
	public Process getRunningProcess() {
		return this.process;
	}

	/**
	 * Return if the process was stopped.
	 * @return {@code true} if stopped
	 */
	public boolean handleSigInt() {
		// if the process has just ended, probably due to this SIGINT, consider handled.
		if (hasJustEnded()) {
			return true;
		}
		return doKill();
	}

	/**
	 * Kill this process.
	 */
	public void kill() {
		doKill();
	}

	private boolean doKill() {
		// destroy the running process
		Process process = this.process;
		if (process != null) {
			try {
				process.destroy();
				process.waitFor();
				this.process = null;
				return true;
			}
			catch (InterruptedException ex) {
				Thread.currentThread().interrupt();
			}
		}
		return false;
	}

	public boolean hasJustEnded() {
		return System.currentTimeMillis() < (this.endTime + JUST_ENDED_LIMIT);
	}

}
