/*******************************************************************************
 * Copyright (c) 2013, 2020 Ericsson AB 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:
 *     Ericsson AB - initial API and implementation
 *     Ericsson AB (Hamdan Msheik) - bug- 432167
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.sdk.scheduler;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.Platform;
import org.eclipse.equinox.internal.p2.touchpoint.eclipse.AgentFromInstall;
import org.eclipse.equinox.p2.core.IProvisioningAgent;

public class PreviousConfigurationFinder {

	private static final Pattern path = Pattern.compile("(.+?)_{1}?([0-9\\.]+)_{1}?(\\d+)(_*?([^_].*)|$)"); //$NON-NLS-1$

	public static class Identifier implements Comparable<Identifier> {
		private static final String DELIM = ". _-"; //$NON-NLS-1$
		private int major, minor, service;

		public Identifier(int major, int minor, int service) {
			super();
			this.major = major;
			this.minor = minor;
			this.service = service;
		}

		/**
		 * @throws NumberFormatException if cannot parse the major and minor version
		 *                               components
		 */
		Identifier(String versionString) {
			super();
			StringTokenizer tokenizer = new StringTokenizer(versionString, DELIM);

			// major
			if (tokenizer.hasMoreTokens())
				major = Integer.parseInt(tokenizer.nextToken());

			// minor
			if (tokenizer.hasMoreTokens())
				minor = Integer.parseInt(tokenizer.nextToken());

			try {
				// service
				if (tokenizer.hasMoreTokens())
					service = Integer.parseInt(tokenizer.nextToken());
			} catch (NumberFormatException nfe) {
				// ignore the service qualifier in that case and default to 0
				// this will allow us to tolerate other non-conventional version numbers
			}
		}

		/**
		 * Returns true if this id is considered to be greater than or equal to the
		 * given baseline. e.g. 1.2.9 >= 1.3.1 -> false 1.3.0 >= 1.3.1 -> false 1.3.1 >=
		 * 1.3.1 -> true 1.3.2 >= 1.3.1 -> true 2.0.0 >= 1.3.1 -> true
		 */
		boolean isGreaterEqualTo(Identifier minimum) {
			if (major < minimum.major)
				return false;
			if (major > minimum.major)
				return true;
			// major numbers are equivalent so check minor
			if (minor < minimum.minor)
				return false;
			if (minor > minimum.minor)
				return true;
			// minor numbers are equivalent so check service
			return service >= minimum.service;
		}

		@Override
		public boolean equals(Object other) {
			if (!(other instanceof Identifier))
				return false;
			Identifier o = (Identifier) other;
			if (major == o.major && minor == o.minor && service == o.service)
				return true;
			return false;
		}

		@Override
		public String toString() {
			return "" + major + '.' + minor + '.' + service; //$NON-NLS-1$
		}

		@Override
		public int compareTo(Identifier o) {

			if (o != null) {
				if (major < o.major) {
					return -1;
				} else if (major > o.major) {
					return 1;
				} else if (minor < o.minor) {
					return -1;
				} else if (minor > o.minor) {
					return 1;
				} else if (service < o.service) {
					return -1;
				} else if (service > o.service) {
					return 1;
				} else {
					return 0;
				}
			}

			return 1;
		}
	}

	public static class ConfigurationDescriptor {
		String productId;
		Identifier version;
		String installPathHashcode;
		File configFolder;
		String os_ws_arch;

		public ConfigurationDescriptor(String productId, Identifier version, String installPathHashcode,
				String platformConfig, File configFolder) {
			this.productId = productId;
			this.version = version;
			this.installPathHashcode = installPathHashcode;
			this.configFolder = configFolder;
			this.os_ws_arch = platformConfig;
		}

		public String getProductId() {
			return productId;
		}

		public Identifier getVersion() {
			return version;
		}

		public String getInstallPathHashcode() {
			return installPathHashcode;
		}

		public File getConfig() {
			return configFolder;
		}

		public String getPlatformConfig() {
			return os_ws_arch;
		}
	}

	public static class ConfigurationDescriptorComparator implements Comparator<ConfigurationDescriptor> {

		// compare ConfigurationDescriptor according to their versions and when equals
		// according to their lastModified field
		@Override
		public int compare(ConfigurationDescriptor o1, ConfigurationDescriptor o2) {
			int result = -1;
			if (o1 != null && o2 != null) {
				if (o1.getVersion().compareTo(o2.getVersion()) != 0) {
					result = o1.getVersion().compareTo(o2.getVersion());
				} else {
					if (o1.getConfig().lastModified() > o2.getConfig().lastModified()) {
						result = 1;
					} else if (o1.getConfig().lastModified() < o2.getConfig().lastModified()) {
						result = -1;
					} else
						result = 0;
				}
			} else if (o1 == null) {
				result = -1;
			} else if (o2 == null) {
				result = 1;
			}
			return result;
		}

	}

	private File currentConfig;

	public PreviousConfigurationFinder(File currentConfiguration) {
		currentConfig = currentConfiguration;
	}

	public ConfigurationDescriptor extractConfigurationData(File candidate) {
		Matcher m = path.matcher(candidate.getName());
		if (!m.matches())
			return null;
		return new ConfigurationDescriptor(m.group(1), new Identifier(m.group(2)), m.group(3), m.group(5),
				candidate.getAbsoluteFile());
	}

	public IProvisioningAgent findPreviousInstalls(File searchRoot, File installFolder) {
		List<ConfigurationDescriptor> potentialConfigurations = readPreviousConfigurations(searchRoot);
		ConfigurationDescriptor runningConfiguration = getConfigdataFromProductFile(installFolder);
		if (runningConfiguration == null)
			return null;
		ConfigurationDescriptor match = findMostRelevantConfigurationFromInstallHashDir(potentialConfigurations,
				runningConfiguration);
		if (match == null)
			match = findMostRelevantConfigurationFromProductId(potentialConfigurations, runningConfiguration);
		if (match == null)
			match = findSpecifiedConfiguration(searchRoot);
		if (match == null)
			return null;
		return AgentFromInstall.createAgentFrom(AutomaticUpdatePlugin.getDefault().getAgentProvider(), null,
				new File(match.getConfig(), "configuration"), null); //$NON-NLS-1$

	}

	public ConfigurationDescriptor findSpecifiedConfiguration(File searchRoot) {
		final String prefixesAsString = System.getProperty("p2.forcedMigrationLocation"); //$NON-NLS-1$
		if (prefixesAsString == null)
			return null;

		String[] prefixes = prefixesAsString.split(","); //$NON-NLS-1$
		for (String prefix : prefixes) {
			final String p = prefix;
			File[] match = searchRoot.listFiles((FileFilter) candidate -> {
				if (!candidate.isDirectory())
					return false;
				if (currentConfig.equals(candidate))
					return false;
				return candidate.getName().contains(p);
			});
			if (match.length != 0)
				return new ConfigurationDescriptor("unknown", new Identifier("0.0.0"), "unknown", "unknown", match[0]); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
		}
		return null;
	}

	private ConfigurationDescriptor getConfigdataFromProductFile(File installFolder) {
		Object[] productFileInfo = loadEclipseProductFile(installFolder);
		// Contrarily to the runtime, when the .eclipseproduct can't be found, we don't
		// fallback to org.eclipse.platform.
		if (productFileInfo.length == 0)
			return null;
		return new ConfigurationDescriptor((String) productFileInfo[0], (Identifier) productFileInfo[1],
				getInstallDirHash(installFolder),
				Platform.getOS() + '_' + Platform.getWS() + '_' + Platform.getOSArch(), null);
	}

	public ConfigurationDescriptor findMostRelevantConfigurationFromInstallHashDir(
			List<ConfigurationDescriptor> configurations, ConfigurationDescriptor configToMatch) {
		ConfigurationDescriptor bestMatch = null;
		int numberOfcriteriaMet = 0;
		for (ConfigurationDescriptor candidate : configurations) {
			int criteriaMet = 0;
			if (!candidate.getInstallPathHashcode().equals(configToMatch.getInstallPathHashcode())) {
				continue;
			}
			criteriaMet++;

			if (configToMatch.getProductId().equals(candidate.getProductId()) && //
					configToMatch.getPlatformConfig().equals(candidate.getPlatformConfig()) && //
					(!candidate.getVersion().isGreaterEqualTo(configToMatch.getVersion()))) {
				// We have a match
				criteriaMet++;
			}

			if (criteriaMet == 0)
				continue;
			if (criteriaMet > numberOfcriteriaMet) {
				bestMatch = candidate;
				numberOfcriteriaMet = criteriaMet;
			} else if (criteriaMet == numberOfcriteriaMet) {
				if (bestMatch.getVersion().equals(candidate.getVersion())) {
					if (bestMatch.getConfig().lastModified() < candidate.getConfig().lastModified()) {
						bestMatch = candidate;
					}
				} else {
					if (candidate.getVersion().isGreaterEqualTo(bestMatch.getVersion()))
						bestMatch = candidate;
				}
			}
		}
		return bestMatch;
	}

	// Out of a set of configuration, find the one with the most similar product
	// info.
	public ConfigurationDescriptor findMostRelevantConfigurationFromProductId(
			List<ConfigurationDescriptor> configurations, ConfigurationDescriptor configToMatch) {
		ConfigurationDescriptor bestMatch = null;

		List<ConfigurationDescriptor> candidates = new ArrayList<>();
		List<ConfigurationDescriptor> candidatesWithUnkonwArchitecture = new ArrayList<>();
		for (ConfigurationDescriptor candidate : configurations) {
			if (configToMatch.getProductId().equals(candidate.getProductId())
					&& configToMatch.getVersion().isGreaterEqualTo(candidate.getVersion())) {
				if (configToMatch.getPlatformConfig().equals(candidate.getPlatformConfig())) {
					candidates.add(candidate);
				} else { // candidate.getPlatformConfig() returns null in legacy installation prior to
							// 4.x.x releases
					candidatesWithUnkonwArchitecture.add(candidate);
				}
			}
		}

		if (!candidates.isEmpty()) {
			candidates.sort(new ConfigurationDescriptorComparator());
			bestMatch = candidates.get(candidates.size() - 1);
		}

		if (bestMatch == null) {
			if (!candidatesWithUnkonwArchitecture.isEmpty()) {
				candidatesWithUnkonwArchitecture.sort(new ConfigurationDescriptorComparator());
				bestMatch = candidatesWithUnkonwArchitecture.get(candidatesWithUnkonwArchitecture.size() - 1);
			}
		}

		return bestMatch;
	}

	// Load the .eclipseproduct file in the base of the installation. This logic is
	// very similar to the one found in the launcher
	private Object[] loadEclipseProductFile(File installDir) {
		final String ECLIPSE = "eclipse"; //$NON-NLS-1$
		final String PRODUCT_SITE_MARKER = ".eclipseproduct"; //$NON-NLS-1$
		final String PRODUCT_SITE_ID = "id"; //$NON-NLS-1$
		final String PRODUCT_SITE_VERSION = "version"; //$NON-NLS-1$

		File eclipseProduct = new File(installDir, PRODUCT_SITE_MARKER);
		String appId = null;
		Identifier appVersion = null;
		if (eclipseProduct.exists()) {
			Properties props = new Properties();
			try (FileInputStream is = new FileInputStream(eclipseProduct)) {
				props.load(is);
				appId = props.getProperty(PRODUCT_SITE_ID);
				if (appId == null || appId.trim().length() == 0)
					appId = ECLIPSE;
				String version = props.getProperty(PRODUCT_SITE_VERSION);
				if (version == null || version.trim().length() == 0)
					appVersion = new Identifier(0, 0, 0);
				else
					appVersion = new Identifier(version);
			} catch (IOException e) {
				return new String[0];
			}
		} else {
			return new String[0];
		}
		return new Object[] { appId, appVersion };
	}

	// Iterate through a folder to look for potential configuration folders and
	// reify them.
	public List<ConfigurationDescriptor> readPreviousConfigurations(File configurationFolder) {
		File[] candidates = configurationFolder.listFiles();
		List<ConfigurationDescriptor> configurations = new ArrayList<>(candidates.length);
		for (File candidate : candidates) {
			if (!candidate.isDirectory())
				continue;
			if (candidate.equals(currentConfig))
				continue;
			ConfigurationDescriptor tmp = extractConfigurationData(candidate);
			if (tmp != null)
				configurations.add(tmp);
		}
		return configurations;
	}

	// This code computes the hashCode of the install location.
	// This is a simplified version of the code that the launcher executes.
	private String getInstallDirHash(File installFolder) {
		try {
			return Integer.toString(installFolder.getCanonicalPath().hashCode());
		} catch (IOException e) {
			return ""; //$NON-NLS-1$
		}
	}
}
