/*******************************************************************************
 * Copyright (c) 2014, 2017 Rapicorp, Inc and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Rapicorp, Inc - application to collect native packages from an existing install
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.touchpoint.natives;

import java.io.*;
import java.net.URI;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.app.IApplication;
import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.equinox.internal.p2.touchpoint.natives.actions.ActionConstants;
import org.eclipse.equinox.internal.p2.touchpoint.natives.actions.CheckAndPromptNativePackage;
import org.eclipse.equinox.p2.core.*;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.osgi.util.NLS;
import org.osgi.framework.ServiceReference;

public class NativePackageExtractionApplication implements IApplication {
	// Keys used in the file created by the application
	private static final String PROP_LAUNCHER_NAME = "launcherName"; //$NON-NLS-1$
	private static final String PROP_ARCH = "arch"; //$NON-NLS-1$
	private static final String PROP_DEPENDS = "depends"; //$NON-NLS-1$

	// Internal constants
	private static final String DEFAULT_VERSION_CONSTRAINT = "ge"; //$NON-NLS-1$
	private static final String _ACTION_ID = "_action_id_"; //$NON-NLS-1$
	private static final String PROP_P2_PROFILE = "eclipse.p2.profile"; //$NON-NLS-1$
	private static final Integer EXIT_ERROR = 13;

	// Constants for arguments
	private static final String OPTION_TO_ANALYZE = "-toAnalyze"; //$NON-NLS-1$
	private static final String OPTION_RESULT_FILE = "-output"; //$NON-NLS-1$

	// Values provided as a parameter to the application
	private File installation;
	private File resultFile;

	// Values derived
	private IProvisioningAgent targetAgent;
	private String profileId;

	// Data collected by the application
	private Properties extractedData = new Properties();

	private Properties installCommandsProperties = new Properties();

	private boolean stackTrace = false;

	@Override
	public Object start(IApplicationContext context) throws Exception {
		try {
			processArguments((String[]) context.getArguments().get("application.args")); //$NON-NLS-1$
			initializeServices();
			NativeTouchpoint.loadInstallCommandsProperties(installCommandsProperties, "debian"); //$NON-NLS-1$
			NativeTouchpoint.loadInstallCommandsProperties(installCommandsProperties, "fedora"); //$NON-NLS-1$
			NativeTouchpoint.loadInstallCommandsProperties(installCommandsProperties, "windows"); //$NON-NLS-1$
			collectData();
			persistInformation();
		} catch (CoreException e) {
			deeplyPrint(e.getStatus(), System.err, 0);
			return EXIT_ERROR;
		}
		return IApplication.EXIT_OK;
	}

	private void processArguments(String[] args) throws CoreException {
		if (args == null || args.length == 0) {
			throw new CoreException(new Status(IStatus.ERROR, Activator.ID,
					NLS.bind(Messages.NativePackageExtractionApplication_MissingParameters, OPTION_TO_ANALYZE,
							OPTION_RESULT_FILE)));
		}
		for (int i = 0; i < args.length; i++) {
			// check for args without parameters (i.e., a flag arg)
			String opt = args[i];

			if (OPTION_TO_ANALYZE.equals(opt)) {
				installation = new File(getRequiredArgument(args, ++i));
				if (!installation.exists())
					throw new CoreException(new Status(IStatus.ERROR, Activator.ID,
							Messages.NativePackageExtractionApplication_FolderNotFound
									+ installation.getAbsolutePath()));
				continue;
			}

			if (OPTION_RESULT_FILE.equals(opt)) {
				resultFile = new File(getRequiredArgument(args, ++i));
				continue;
			}
		}
	}

	private static String getRequiredArgument(String[] args, int argIdx) throws CoreException {
		if (argIdx < args.length) {
			String arg = args[argIdx];
			if (!arg.startsWith("-")) //$NON-NLS-1$
				return arg;
		}
		throw new ProvisionException(
				NLS.bind(Messages.NativePackageExtractionApplication_MissingValue, args[argIdx - 1]));
	}

	private void collectData() {
		IProfileRegistry registry = targetAgent.getService(IProfileRegistry.class);
		IProfile p = registry.getProfile(profileId);
		collectArchitecture(p);
		collectLauncherName(p);
		collectDebianDependencies(p);
	}

	private void persistInformation() throws CoreException {
		try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(resultFile))) {
			extractedData.store(os, "Data extracted from eclipse located at " + installation); //$NON-NLS-1$
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR, Activator.ID,
					Messages.NativePackageExtractionApplication_PersistencePb + resultFile.getAbsolutePath(), e));
		}
	}

	private void collectLauncherName(IProfile p) {
		String launcherName = p.getProperty("eclipse.touchpoint.launcherName"); //$NON-NLS-1$
		if (launcherName == null)
			launcherName = ""; //$NON-NLS-1$
		extractedData.put(PROP_LAUNCHER_NAME, launcherName);
	}

	private void collectArchitecture(IProfile p) {
		String environments = p.getProperty(IProfile.PROP_ENVIRONMENTS);
		if (environments != null) {
			for (StringTokenizer tokenizer = new StringTokenizer(environments, ","); tokenizer.hasMoreElements();) { //$NON-NLS-1$
				String entry = tokenizer.nextToken();
				int i = entry.indexOf('=');
				String key = entry.substring(0, i).trim();
				String value = entry.substring(i + 1).trim();
				if (!key.equals("osgi.arch")) //$NON-NLS-1$
					continue;

				if ("x86_64".equals(value)) { //$NON-NLS-1$
					extractedData.put(PROP_ARCH, "amd64"); //$NON-NLS-1$
					return;
				}
				if ("x86".equals(value)) { //$NON-NLS-1$
					extractedData.put(PROP_ARCH, "x86"); //$NON-NLS-1$
					return;
				}
			}
		}
		extractedData.put(PROP_ARCH, ""); //$NON-NLS-1$
	}

	private void collectDebianDependencies(IProfile p) {
		String depends = ""; //$NON-NLS-1$
		IQueryResult<IInstallableUnit> allIUs = p.available(QueryUtil.ALL_UNITS, new NullProgressMonitor());
		Iterator<IInstallableUnit> a = allIUs.iterator();
		while (a.hasNext()) {
			IInstallableUnit iu = a.next();
			Collection<ITouchpointData> tpdata = iu.getTouchpointData();
			for (ITouchpointData data : tpdata) {
				Map<String, ITouchpointInstruction> allInstructions = data.getInstructions();
				for (ITouchpointInstruction instruction : allInstructions.values()) {
					StringTokenizer tokenizer = new StringTokenizer(instruction.getBody(), ";"); //$NON-NLS-1$
					while (tokenizer.hasMoreTokens()) {
						Map<String, String> parsedInstructions = parseInstruction(tokenizer.nextToken());
						if (parsedInstructions != null
								&& parsedInstructions.get(_ACTION_ID).endsWith(CheckAndPromptNativePackage.ID)) {
							if ("debian".equals(parsedInstructions.get(ActionConstants.PARM_LINUX_DISTRO))) { //$NON-NLS-1$
								depends += formatAsDependsEntry(
										parsedInstructions.get(ActionConstants.PARM_LINUX_PACKAGE_NAME),
										parsedInstructions.get(ActionConstants.PARM_LINUX_PACKAGE_VERSION),
										parsedInstructions.get(ActionConstants.PARM_LINUX_VERSION_COMPARATOR)) + ',';
							}
						}
					}
				}
			}
		}
		// pre-prend a comma, and remove the last one
		if (depends.length() > 0)
			depends = ',' + depends.substring(0, depends.length() - 1);

		extractedData.put(PROP_DEPENDS, depends);
	}

	private String formatAsDependsEntry(String packageId, String version, String versionComparator) {
		String result = packageId;
		if (versionComparator == null)
			versionComparator = DEFAULT_VERSION_CONSTRAINT;
		if (version != null) {
			result += '(' + getUserFriendlyComparator(versionComparator) + ' ' + version + ')';
		}
		return result;
	}

	private String getUserFriendlyComparator(String comparator) {
		return installCommandsProperties.getProperty(comparator, ""); //$NON-NLS-1$
	}

	// Code copied from the InstructionParser class
	private Map<String, String> parseInstruction(String statement) {
		Map<String, String> instructions = new HashMap<>();

		int openBracket = statement.indexOf('(');
		int closeBracket = statement.lastIndexOf(')');
		if (openBracket == -1 || closeBracket == -1 || openBracket > closeBracket)
			return null;
		instructions.put(_ACTION_ID, statement.substring(0, openBracket).trim());

		String nameValuePairs = statement.substring(openBracket + 1, closeBracket);
		if (nameValuePairs.length() == 0)
			return instructions;

		StringTokenizer tokenizer = new StringTokenizer(nameValuePairs, ","); //$NON-NLS-1$
		while (tokenizer.hasMoreTokens()) {
			String nameValuePair = tokenizer.nextToken();
			int colonIndex = nameValuePair.indexOf(":"); //$NON-NLS-1$
			if (colonIndex == -1)
				return null;
			String name = nameValuePair.substring(0, colonIndex).trim();
			String value = nameValuePair.substring(colonIndex + 1).trim();
			instructions.put(name, value);
		}
		return instructions;
	}

	private void initializeServices() throws ProvisionException {
		ServiceReference<IProvisioningAgentProvider> agentProviderRef = Activator.getContext()
				.getServiceReference(IProvisioningAgentProvider.class);
		IProvisioningAgentProvider provider = Activator.getContext().getService(agentProviderRef);

		URI p2DataArea = new File(installation, "p2").toURI(); //$NON-NLS-1$
		targetAgent = provider.createAgent(p2DataArea);
		targetAgent.registerService(IProvisioningAgent.INSTALLER_AGENT, provider.createAgent(null));

		Activator.getContext().ungetService(agentProviderRef);
		if (profileId == null) {
			if (installation != null) {
				File configIni = new File(installation, "configuration/config.ini"); //$NON-NLS-1$
				Properties ciProps = new Properties();
				try (InputStream in = new BufferedInputStream(new FileInputStream(configIni))) {
					ciProps.load(in);
					profileId = ciProps.getProperty(PROP_P2_PROFILE);
				} catch (IOException e) {
					// Ignore
				}
				if (profileId == null)
					profileId = installation.toString();
			}
		}
		if (profileId != null)
			targetAgent.registerService(PROP_P2_PROFILE, profileId);
	}

	private static void appendLevelPrefix(PrintStream strm, int level) {
		for (int idx = 0; idx < level; ++idx)
			strm.print(' ');
	}

	private void deeplyPrint(CoreException ce, PrintStream strm, int level) {
		appendLevelPrefix(strm, level);
		if (stackTrace)
			ce.printStackTrace(strm);
		deeplyPrint(ce.getStatus(), strm, level);
	}

	private void deeplyPrint(IStatus status, PrintStream strm, int level) {
		appendLevelPrefix(strm, level);
		String msg = status.getMessage();
		strm.println(msg);
		Throwable cause = status.getException();
		if (cause != null) {
			strm.print("Caused by: "); //$NON-NLS-1$
			if (stackTrace || !(msg.equals(cause.getMessage()) || msg.equals(cause.toString())))
				deeplyPrint(cause, strm, level);
		}

		if (status.isMultiStatus()) {
			IStatus[] children = status.getChildren();
			for (IStatus child : children) {
				deeplyPrint(child, strm, level + 1);
			}
		}
	}

	private void deeplyPrint(Throwable t, PrintStream strm, int level) {
		if (t instanceof CoreException)
			deeplyPrint((CoreException) t, strm, level);
		else {
			appendLevelPrefix(strm, level);
			if (stackTrace)
				t.printStackTrace(strm);
			else {
				strm.println(t.toString());
				Throwable cause = t.getCause();
				if (cause != null) {
					strm.print("Caused by: "); //$NON-NLS-1$
					deeplyPrint(cause, strm, level);
				}
			}
		}
	}

	@Override
	public void stop() {
		// We don't handle application stopping
	}

}
