/*******************************************************************************
 * Copyright (c) 2015, 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.vagrant.core;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.linuxtools.vagrant.core.EnumVMStatus;
import org.eclipse.linuxtools.vagrant.core.IVagrantBox;
import org.eclipse.linuxtools.vagrant.core.IVagrantBoxListener;
import org.eclipse.linuxtools.vagrant.core.IVagrantConnection;
import org.eclipse.linuxtools.vagrant.core.IVagrantVM;
import org.eclipse.linuxtools.vagrant.core.IVagrantVMListener;
import org.osgi.framework.Version;

public class VagrantConnection implements IVagrantConnection, Closeable {

	private static final String JSCH_ID = "org.eclipse.jsch.core"; //$NON-NLS-1$
	private static final String KEY = "PRIVATEKEY"; //$NON-NLS-1$
	private static final String VG = "vagrant"; //$NON-NLS-1$
	private final Object imageLock = new Object();
	private final Object containerLock = new Object();

	private List<IVagrantVM> vms;
	private boolean containersLoaded = false;
	private List<IVagrantBox> boxes;
	private boolean boxesLoaded = false;
	private Set<String> trackedKeys = new HashSet<>();

	ListenerList<IVagrantVMListener> vmListeners;
	ListenerList<IVagrantBoxListener> boxListeners;

	public VagrantConnection() {
		// Add the box/vm refresh manager to watch the containers list
		VagrantBoxRefreshManager vbrm = VagrantBoxRefreshManager.getInstance();
		VagrantVMRefreshManager vvrm = VagrantVMRefreshManager.getInstance();
		addBoxListener(vbrm);
		addVMListener(vvrm);
	}

	@Override
	public void close() {
	}

	@Override
	public void addVMListener(IVagrantVMListener listener) {
		if (vmListeners == null)
			vmListeners = new ListenerList<>(ListenerList.IDENTITY);
		vmListeners.add(listener);
	}

	@Override
	public void removeVMListener(IVagrantVMListener listener) {
		if (vmListeners != null)
			vmListeners.remove(listener);
	}

	public void notifyContainerListeners(List<IVagrantVM> list) {
		if (vmListeners != null) {
			for (IVagrantVMListener listener : vmListeners) {
				listener.listChanged(this, list);
			}
		}
	}

	@Override
	public void addBoxListener(IVagrantBoxListener listener) {
		if (boxListeners == null)
			boxListeners = new ListenerList<>(ListenerList.IDENTITY);
		boxListeners.add(listener);
	}

	@Override
	public void removeBoxListener(IVagrantBoxListener listener) {
		if (boxListeners != null)
			boxListeners.remove(listener);
	}

	public void notifyBoxListeners(List<IVagrantBox> list) {
		if (boxListeners != null) {
			for (IVagrantBoxListener listener : boxListeners) {
				listener.listChanged(this, list);
			}
		}
	}

	@Override
	public List<IVagrantVM> getVMs(boolean force) {
		if (force || !isVMsLoaded()) {
			refreshVMs();
		}
		return this.vms;
	}

	protected void refreshVMs() {
		String[] res = call(new String[] { "global-status" }); //$NON-NLS-1$
		List<String> vmIDs = new LinkedList<>();
		List<String> vmDirs = new LinkedList<>();
		final List<IVagrantVM> containers = new LinkedList<>();
		for (int i = 0; i < res.length; i++) {
			String[] items = res[i].split("\\s+"); //$NON-NLS-1$
			if (items.length == 5 && i >= 2) {
				vmIDs.add(items[0]);
				vmDirs.add(items[items.length - 1]);
			}
		}

		List<String> completed = new ArrayList<>();
		if (!vmIDs.isEmpty()) {
			Iterator<String> vmIterator = vmIDs.iterator();
			Iterator<String> vmDirIterator = vmDirs.iterator();
			while (vmIterator.hasNext()) {
				final String vmid = vmIterator.next();
				final String vmDir = vmDirIterator.next();
				new Thread("Checking ssh-config for vm " + vmid) { //$NON-NLS-1$
					@Override
					public void run() {
						try {
							VagrantVM ret = createVagrantVM(vmid, vmDir);
							if (ret != null) {
								containers.add(ret);
							}
						} finally {
							// Ensure this gets called no matter what
							completed.add(vmid);
						}
					}
				}.start();
			}
		}

		// Should set a max timeout?

		// Wait for completed to have same count as vmIDs
		while (completed.size() < vmIDs.size()) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException ie) {
				// Ignore
			}
		}

		Collections.sort(containers,
				(o1, o2) -> o1.name().compareTo(o2.name()));

		this.containersLoaded = true;
		synchronized (containerLock) {
			this.vms = containers;
		}
		removeKeysFromInnactiveVMs();
		notifyContainerListeners(this.vms);
	}

	private VagrantVM createVagrantVM(String vmid, String vmDir) {

		Map<String, String> env = EnvironmentsManager.getSingleton()
				.getEnvironment(new File(vmDir));

		List<String> args = new LinkedList<>(
				Arrays.asList(new String[] { "ssh-config" })); //$NON-NLS-1$
		args.add(vmid);

		List<String> sshConfig = null;

		// Run and handle ssh-config for this vm
		String[] res = call(args.toArray(new String[0]), new File(vmDir), env);
		for (int i = 0; i < res.length; i++) {
			String[] items = res[i].trim().split(" "); //$NON-NLS-1$
			if (items[0].equals("HostName")) { //$NON-NLS-1$
				List<String> tmp = new ArrayList<>();
				tmp.add(items[1]);
				sshConfig = tmp;
			} else if (items[0].equals("User") || items[0].equals("Port")) { //$NON-NLS-1$ //$NON-NLS-2$
				sshConfig.add(items[1]);
			} else if (items[0].equals("IdentityFile")) {
				sshConfig.add(items[1].replace("\"", "")); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		// Run and handle status for this vm
		VagrantVM vm = null;
		args = new LinkedList<>(
				Arrays.asList(new String[] { "--machine-readable", "status" })); //$NON-NLS-1$ //$NON-NLS-2$
		args.add(vmid);
		res = call(args.toArray(new String[0]), new File(vmDir), env);
		String name, provider, state, state_desc;
		name = provider = state = state_desc = ""; //$NON-NLS-1$
		for (int i = 0; i < res.length; i++) {
			String[] items = res[i].split(","); //$NON-NLS-1$
			if (items[2].equals("provider-name")) { //$NON-NLS-1$
				name = items[1];
				provider = items[3];
			} else if (items[2].equals("state")) { //$NON-NLS-1$
				state = items[3];
			} else if (items[2].equals("state-human-long")) { //$NON-NLS-1$
				state_desc = items[3];
				if (sshConfig == null || sshConfig.isEmpty()) {
					// VM exists but ssh is not configured
					vm = new VagrantVM(vmid, name, provider, state, state_desc,
							new File(vmDir), null, null, 0, null);
				} else {
					vm = new VagrantVM(vmid, name, provider, state, state_desc,
							new File(vmDir), sshConfig.get(0), sshConfig.get(1),
							Integer.parseInt(sshConfig.get(2)),
							sshConfig.get(3));
				}
			}
		}
		return vm;
	}

	/**
	 * If a key stored in preferences comes from a non-running VM
	 * or came from a Vagrant VM (tracked) but is not longer
	 * associated with one, then it is safe to remove it.
	 */
	private void removeKeysFromInnactiveVMs() {
		// org.eclipse.jsch.internal.core.IConstants.KEY_PRIVATEKEY
		String newKeys = ""; //$NON-NLS-1$
		String keys = InstanceScope.INSTANCE.getNode(JSCH_ID).get(KEY, ""); //$NON-NLS-1$
		if (keys.isEmpty()) {
			keys = DefaultScope.INSTANCE.getNode(JSCH_ID).get(KEY, ""); //$NON-NLS-1$
		}
		boolean vmFound = false;
		for (String key : keys.split(",")) { //$NON-NLS-1$
			for (IVagrantVM vm : vms) {
				if (key.equals(vm.identityFile())) {
					vmFound = true;
					if (!EnumVMStatus.RUNNING.equals(EnumVMStatus.fromStatusMessage(vm.state()))) {
						newKeys = removeFromKeys(keys, key);
						removeFromTrackedKeys(key);
						break;
					}
				}
			}
			if (!vmFound && isTrackedKey(key)) {
				newKeys = removeFromKeys(keys, key);
				removeFromTrackedKeys(key);
			}
		}
		if (!newKeys.isEmpty() && !newKeys.equals(keys)) {
			InstanceScope.INSTANCE.getNode(JSCH_ID).put(KEY, newKeys);
		}
	}

	@Override
	public void addToTrackedKeys(String key) {
		trackedKeys.add(key);
	}

	private void removeFromTrackedKeys(String key) {
		trackedKeys.remove(key);
	}

	private String removeFromKeys(String keys, String key) {
		StringBuilder res = new StringBuilder();
		for (String k : keys.split(",")) {
			if (!key.equals(k)) {
				res.append(","); //$NON-NLS-1$
				res.append(k);
			}
		}
		return res.substring(1);
	}

	public boolean isTrackedKey(String key) {
		return trackedKeys.contains(key);
	}

	@Override
	public List<IVagrantVM> getVMs() {
		return getVMs(false);
	}

	@Override
	public boolean isVMsLoaded() {
		return containersLoaded;
	}

	@Override
	public boolean isBoxesLoaded() {
		return boxesLoaded;
	}

	@Override
	public List<IVagrantBox> getBoxes() {
		return getBoxes(false);
	}

	@Override
	public List<IVagrantBox> getBoxes(boolean force) {
		if (force || !isBoxesLoaded()) {
			String [] res = call(new String[] { "--machine-readable", "box", "list" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			List<IVagrantBox> images = new LinkedList<>();
			String name = ""; //$NON-NLS-1$
			String provider = ""; //$NON-NLS-1$
			String version = "0"; //$NON-NLS-1$
			for (int i = 0; i < res.length; i++) {
				String[] items = res[i].split(","); //$NON-NLS-1$
				if (items[2].equals("box-name")) { //$NON-NLS-1$
					name = items[3];
				} else if (items[2].equals("box-provider")) { //$NON-NLS-1$
					provider = items[3];
				} else if (items[2].equals("box-version")) { //$NON-NLS-1$
					version = items[3];
					images.add(new VagrantBox(name, provider, Version.parseVersion(version)));
					name = ""; //$NON-NLS-1$
					provider = ""; //$NON-NLS-1$
					version = "0"; //$NON-NLS-1$
				}
			}
			this.boxesLoaded = true;
			synchronized (imageLock) {
				this.boxes = images;
			}
			notifyBoxListeners(this.boxes);
		}
		return this.boxes;
	}

	@Override
	public void init(File vagrantDir) {
		call(new String [] {"init"}, vagrantDir); //$NON-NLS-1$
	}

	@Override
	public void up(File vagrantDir, String provider) {
		up(vagrantDir, provider,
				EnvironmentsManager.getSingleton().getEnvironment(vagrantDir));
	}


	private void up(File vagrantDir, String provider,
			Map<String, String> environment) {
		if (provider != null) {
			rtCall(new String[] { "up", "--provider", provider }, //$NON-NLS-1$ //$NON-NLS-2$
					vagrantDir, environment);
		} else {
			rtCall(new String[] { "up" }, vagrantDir, environment); //$NON-NLS-1$
		}
	}

	@Override
	public void addBox(String name, String location, boolean progress) {
		if (progress) {
			rtCall(new String[] { "box", "add", name, location }, null, null); //$NON-NLS-1$ //$NON-NLS-2$
		} else {
			call(new String [] {"--machine-readable", "box", "add", name, location}); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
	}

	@Override
	public void destroyVM(IVagrantVM vm) {
		call(new String[] { "destroy", "-f", vm.id() }, vm.directory(), //$NON-NLS-1$ //$NON-NLS-2$
				EnvironmentsManager.getSingleton()
						.getEnvironment(vm.directory()));
	}

	@Override
	public void haltVM(IVagrantVM vm) {
		call(new String[] { "--machine-readable", "halt", vm.id() }, //$NON-NLS-1$ //$NON-NLS-2$
				vm.directory(), EnvironmentsManager.getSingleton()
						.getEnvironment(vm.directory()));
	}

	@Override
	public void startVM(IVagrantVM vm) {
		up(vm.directory(), vm.provider(), EnvironmentsManager.getSingleton()
				.getEnvironment(vm.directory()));
	}

	@Override
	public void removeBox(String name) {
		call(new String[] { "--machine-readable", "box", "remove", name }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	@Override
	public void packageVM(IVagrantVM vm, String name) {
		rtCall(new String[] { "package", vm.id(), "--output", name}, vm.directory(), null); //$NON-NLS-1$ //$NON-NLS-2$
	}

	@Override
	public String getName() {
		return Messages.VagrantConnection_sys_vagrant_conn;
	}

	private static String[] call(String[] args) {
		return call(args, null);
	}

	private static String[] call(String[] args, File vagrantDir) {
		return call(args, vagrantDir, null);
	}

	private static String[] call(String[] args, File vagrantDir,
			Map<String, String> env) {
		final String[] envp = EnvironmentsManager.convertEnvironment(env);

		List<String> result = new ArrayList<>();
		try {
			List<String> cmd = new ArrayList<>();
			cmd.add(VG);
			cmd.addAll(Arrays.asList(args));
			Process p = Runtime.getRuntime().exec(cmd.toArray(new String[0]),
					envp, vagrantDir);
			BufferedReader buff = new BufferedReader(
					new InputStreamReader(p.getInputStream()));
			if (p.waitFor() == 0) {
				String line;
				while ((line = buff.readLine()) != null) {
					result.add(line);
				}
			} else {
				return new String[0];
			}
		} catch (IOException e) {
		} catch (InterruptedException e) {
		}
		return result.toArray(new String[0]);
	}

	private static void rtCall(String[] args, File vagrantDir,
			Map<String, String> environment) {

		// org.eclipse.core.externaltools.internal.IExternalToolConstants
		final String EXTERNAL_TOOLS = "org.eclipse.ui.externaltools.ProgramLaunchConfigurationType"; //$NON-NLS-1$
		final String UI_PLUGIN_ID = "org.eclipse.ui.externaltools"; //$NON-NLS-1$
		final String ATTR_LOCATION = UI_PLUGIN_ID + ".ATTR_LOCATION"; //$NON-NLS-1$
		final String ATTR_TOOL_ARGUMENTS = UI_PLUGIN_ID + ".ATTR_TOOL_ARGUMENTS"; //$NON-NLS-1$
		final String ATTR_WORKING_DIRECTORY = UI_PLUGIN_ID + ".ATTR_WORKING_DIRECTORY"; //$NON-NLS-1$

		String arguments = Arrays.asList(args).stream().map(u -> u.toString())
				.collect(Collectors.joining(" ")); //$NON-NLS-1$
		ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
		ILaunchConfigurationType type = manager.getLaunchConfigurationType(EXTERNAL_TOOLS);
		try {
			// TODO: worth handling 'vagrant' (not on PATH) as an alias ?
			String vagrantPath = findVagrantPath();
			ILaunchConfigurationWorkingCopy wc = type.newInstance(null, VG);
			wc.setAttribute(ATTR_LOCATION, vagrantPath);
			wc.setAttribute(ATTR_TOOL_ARGUMENTS, arguments);
			wc.setAttribute(ATTR_WORKING_DIRECTORY,
					vagrantDir != null ? vagrantDir.getAbsolutePath() : null);
			wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES,
					environment);
			wc.launch(ILaunchManager.RUN_MODE, new NullProgressMonitor());
		} catch (CoreException e1) {
			Activator.log(e1);
		}
	}

	/**
	 * Find the location of 'vagrant' on the system by looking under the
	 * environment PATH.
	 *
	 * @return The location of 'vagrant' as a string if it exists under
	 * the PATH, or null if it could not be found.
	 */
	public static String findVagrantPath() {
		final String vgName = Platform.OS_WIN32.equals(Platform.getOS())
				? VG + ".exe" : VG; //$NON-NLS-1$

		String userDefinedVagrantPath = getUserDefinedVagrantPath();
		if (userDefinedVagrantPath != null) {
			Path vgPath = Paths.get(userDefinedVagrantPath, vgName);
			if (vgPath.toFile().exists()) {
				return vgPath.toString();
			}
		}

		final String envPath = System.getenv("PATH"); //$NON-NLS-1$
		if (envPath != null) {
			for (String dir : envPath.split(File.pathSeparator)) {
				// On Windows, we might get back a String with quotes so we must
				// remove them (Bug 538217)
				if ('"' == dir.charAt(0) // $NON-NLS-1$
						&& '"' == dir.charAt(dir.length() - 1)) { // $NON-NLS-1$
					dir = dir.substring(1, dir.length() - 1);
				}
				Path vgPath = Paths.get(dir, vgName);
				if (vgPath.toFile().exists()) {
					return vgPath.toString();
				}
			}
		}

		return null;
	}

	public static String getUserDefinedVagrantPath() {
		String userDefinedVagrantPath = InstanceScope.INSTANCE
				.getNode("org.eclipse.linuxtools.vagrant.ui") //$NON-NLS-1$
				.get("vagrantPath", null); //$NON-NLS-1$
		String defaultDefinedVagrantPath = DefaultScope.INSTANCE
				.getNode("org.eclipse.linuxtools.vagrant.ui") //$NON-NLS-1$
				.get("vagrantPath", null); //$NON-NLS-1$
		return userDefinedVagrantPath != null ? userDefinedVagrantPath
				: defaultDefinedVagrantPath;
	}

}
