/*******************************************************************************
 * Copyright (c) 2008, 2017 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Code 9 - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.reconciler.dropins;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.core.helpers.Tracing;
import org.eclipse.equinox.internal.p2.extensionlocation.ExtensionLocationArtifactRepository;
import org.eclipse.equinox.internal.p2.extensionlocation.ExtensionLocationMetadataRepository;
import org.eclipse.equinox.internal.p2.update.Site;
import org.eclipse.equinox.internal.provisional.p2.directorywatcher.RepositoryListener;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.IRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.osgi.util.NLS;

public class DropinsRepositoryListener extends RepositoryListener {
	private static final String PREFIX = "[reconciler] [dropins] "; //$NON-NLS-1$
	private static final String PLUGINS = "plugins"; //$NON-NLS-1$
	private static final String FEATURES = "features"; //$NON-NLS-1$
	private static final String JAR = ".jar"; //$NON-NLS-1$
	private static final String LINK = ".link"; //$NON-NLS-1$
	private static final String ZIP = ".zip"; //$NON-NLS-1$
	private static final String LINKS_PATH = "path"; //$NON-NLS-1$
	private static final String LINK_IS_OPTIONAL = "optional"; //$NON-NLS-1$
	private static final String DROPIN_ARTIFACT_REPOSITORIES = "dropin.artifactRepositories"; //$NON-NLS-1$
	private static final String DROPIN_METADATA_REPOSITORIES = "dropin.metadataRepositories"; //$NON-NLS-1$
	private static final String PIPE = "|"; //$NON-NLS-1$
	private final IProvisioningAgent agent;
	private List<IMetadataRepository> metadataRepositories = new ArrayList<>();
	private List<IArtifactRepository> artifactRepositories = new ArrayList<>();

	static class LinkedRepository {
		LinkedRepository(File location) {
			super();
			if (location == null)
				throw new IllegalArgumentException("Repository location cannot be null."); //$NON-NLS-1$
			this.location = location;
		}

		boolean exists() {
			return location.exists();
		}

		File getLocation() {
			return location;
		}

		boolean isOptional() {
			return optional;
		}

		void setOptional(boolean optional) {
			this.optional = optional;
		}

		private File location;
		private boolean optional = false;
	}

	public DropinsRepositoryListener(IProvisioningAgent agent, String repositoryName, Map<String, String> properties) {
		super(repositoryName, properties);
		this.agent = agent;
	}

	@Override
	public boolean isInterested(File file) {
		return true;
	}

	@Override
	public boolean added(File file) {
		if (super.added(file)) {
			if (Tracing.DEBUG_RECONCILER)
				Tracing.debug(PREFIX + "Interesting feature or bundle added: " + file); //$NON-NLS-1$
			return true;
		}
		addRepository(file);
		return true;
	}

	@Override
	public boolean changed(File file) {
		if (super.changed(file)) {
			if (Tracing.DEBUG_RECONCILER)
				Tracing.debug(PREFIX + "Interesting feature or bundle changed: " + file); //$NON-NLS-1$
			return true;
		}
		addRepository(file);
		return true;
	}

	private void addRepository(File file) {
		URI repoLocation = createRepositoryLocation(file);
		if (repoLocation == null)
			return;
		Map<String, String> properties = new HashMap<>();
		// if the file pointed to a link file, keep track of the attribute
		// so we can add it to the repo later
		if (file.isFile() && file.getName().endsWith(LINK)) {
			URI linkLocation = getLinkRepository(file, false);
			if (linkLocation != null)
				properties.put(Site.PROP_LINK_FILE, file.getAbsolutePath());
		}
		getMetadataRepository(repoLocation, properties);
		getArtifactRepository(repoLocation, properties);
	}

	/*
	 * Return the file pointed to by the given link file. Return null if there is a problem
	 * reading the file or resolving the location.
	 */
	static LinkedRepository getLinkedRepository(File file) {
		Properties links = new Properties();
		try {
			try (InputStream input = new BufferedInputStream(new FileInputStream(file))) {
				links.load(input);
			}
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.error_reading_link, file.getAbsolutePath()), e));
			return null;
		}
		String path = links.getProperty(LINKS_PATH);
		if (path == null) {
			return null;
		}

		// parse out link information
		if (path.startsWith("r ")) { //$NON-NLS-1$
			path = path.substring(2).trim();
		} else if (path.startsWith("rw ")) { //$NON-NLS-1$
			path = path.substring(3).trim();
		} else {
			path = path.trim();
		}
		path = Activator.substituteVariables(path);
		File linkedFile = new File(path);
		if (!linkedFile.isAbsolute()) {
			// link support is relative to the install root
			File root = Activator.getEclipseHome();
			if (root != null)
				linkedFile = new File(root, path);
		}
		try {
			LinkedRepository result = new LinkedRepository(linkedFile.getCanonicalFile());
			// Check if the link target is marked as optional.
			// If link is optional, then the link target may not exist.
			// So IF link is marked as optional AND does not exist, simply ignore it.
			String optional = links.getProperty(LINK_IS_OPTIONAL);
			result.setOptional(Boolean.parseBoolean(optional));
			return result;
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, NLS.bind(Messages.error_resolving_link, linkedFile.getAbsolutePath(), file.getAbsolutePath()), e));
			return null;
		}
	}

	private URI createRepositoryLocation(File file) {
		try {
			file = file.getCanonicalFile();
			String fileName = file.getName();
			if (fileName.endsWith(LINK))
				return getLinkRepository(file, true);

			if (file.isDirectory()) {
				// Check if the directory is either the plugins directory of an extension location
				// or the features directory and the plugins folder is not present.
				// This extra check on the features directory is done to avoid adding the parent URL twice
				if (file.getName().equals(PLUGINS)) {
					File parentFile = file.getParentFile();
					return (parentFile != null) ? parentFile.toURI() : null;
				}
				if (file.getName().equals(FEATURES)) {
					File parentFile = file.getParentFile();
					if (parentFile == null || new File(parentFile, PLUGINS).isDirectory())
						return null;
					return parentFile.toURI();
				}
				return file.toURI();
			}

			//TODO: Should we remove this? We only should support directly runnable repos
			if (fileName.endsWith(ZIP) || fileName.endsWith(JAR))
				return new URI("jar:" + file.toURI() + "!/"); //$NON-NLS-1$ //$NON-NLS-2$

			// last resort -- we'll try to interpret the file as a link
			return getLinkRepository(file, false);
		} catch (URISyntaxException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while building repository location from file: " + file.getAbsolutePath(), e)); //$NON-NLS-1$
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while building repository location from file: " + file.getAbsolutePath(), e)); //$NON-NLS-1$
		}
		return null;
	}

	private URI getLinkRepository(File file, boolean logMissingLink) {
		LinkedRepository repo = getLinkedRepository(file);
		if (repo == null) {
			if (logMissingLink)
				LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Unable to determine link location from file: " + file.getAbsolutePath())); //$NON-NLS-1$
			return null;
		}
		return repo.isOptional() && !repo.exists() ? null : repo.getLocation().toURI();
	}

	public void getMetadataRepository(URI repoURL, Map<String, String> properties) {
		try {
			IMetadataRepository repository = null;
			try {
				ExtensionLocationMetadataRepository.validate(repoURL, null);
				repository = Activator.createExtensionLocationMetadataRepository(repoURL, "dropins metadata repo: " + repoURL, properties); //$NON-NLS-1$
			} catch (ProvisionException e) {
				repository = Activator.loadMetadataRepository(repoURL, null);
			}
			metadataRepositories.add(repository);
			debugRepository(repository);
		} catch (ProvisionException ex) {
			LogHelper.log(ex);
		}
	}

	private void debugRepository(IMetadataRepository repository) {
		if (!Tracing.DEBUG_RECONCILER)
			return;
		Tracing.debug(PREFIX + "Repository created " + repository.getLocation()); //$NON-NLS-1$
		// Print out a list of all the IUs in the repository
		IQueryResult<IInstallableUnit> result = repository.query(QueryUtil.createIUAnyQuery(), new NullProgressMonitor());
		for (IInstallableUnit iInstallableUnit : result)
			Tracing.debug(PREFIX + "\t" + iInstallableUnit); //$NON-NLS-1$
	}

	public void getArtifactRepository(URI repoURL, Map<String, String> properties) {
		try {
			IArtifactRepository repository = null;
			try {
				ExtensionLocationArtifactRepository.validate(repoURL, null);
				repository = Activator.createExtensionLocationArtifactRepository(repoURL, "dropins artifact repo: " + repoURL, properties); //$NON-NLS-1$
				// fall through here and call the load which then adds the repo to the manager's list
			} catch (ProvisionException ex) {
				repository = Activator.loadArtifactRepository(repoURL, null);
			}
			artifactRepositories.add(repository);
		} catch (ProvisionException ex) {
			LogHelper.log(ex);
		}
	}

	@Override
	public void stopPoll() {
		synchronizeDropinMetadataRepositories();
		synchronizeDropinArtifactRepositories();
		super.stopPoll();
	}

	private void synchronizeDropinMetadataRepositories() {
		List<String> currentRepositories = new ArrayList<>();
		for (IMetadataRepository repository : metadataRepositories) {
			currentRepositories.add(repository.getLocation().toString());
		}
		List<String> previousRepositories = getListRepositoryProperty(getMetadataRepository(), DROPIN_METADATA_REPOSITORIES);
		for (String repository : previousRepositories) {
			if (!currentRepositories.contains(repository))
				removeMetadataRepository(repository);
		}
		setListRepositoryProperty(getMetadataRepository(), DROPIN_METADATA_REPOSITORIES, currentRepositories);
	}

	private void removeMetadataRepository(String urlString) {
		IMetadataRepositoryManager manager = agent.getService(IMetadataRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException(Messages.metadata_repo_manager_not_registered);
		try {
			manager.removeRepository(new URI(urlString));
		} catch (URISyntaxException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while creating URL from: " + urlString, e)); //$NON-NLS-1$
		}
	}

	private void synchronizeDropinArtifactRepositories() {
		List<String> currentRepositories = new ArrayList<>();
		for (IArtifactRepository repository : artifactRepositories) {
			currentRepositories.add(repository.getLocation().toString());
		}
		List<String> previousRepositories = getListRepositoryProperty(getArtifactRepository(), DROPIN_ARTIFACT_REPOSITORIES);
		for (String repository : previousRepositories) {
			if (!currentRepositories.contains(repository))
				removeArtifactRepository(repository);
		}
		setListRepositoryProperty(getArtifactRepository(), DROPIN_ARTIFACT_REPOSITORIES, currentRepositories);
	}

	public void removeArtifactRepository(String urlString) {
		IArtifactRepositoryManager manager = agent.getService(IArtifactRepositoryManager.class);
		if (manager == null)
			throw new IllegalStateException(Messages.artifact_repo_manager_not_registered);
		try {
			manager.removeRepository(new URI(urlString));
		} catch (URISyntaxException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Error occurred while creating URL from: " + urlString, e)); //$NON-NLS-1$
		}
	}

	private List<String> getListRepositoryProperty(IRepository<?> repository, String key) {
		List<String> listProperty = new ArrayList<>();
		String dropinRepositories = repository.getProperties().get(key);
		if (dropinRepositories != null) {
			StringTokenizer tokenizer = new StringTokenizer(dropinRepositories, PIPE);
			while (tokenizer.hasMoreTokens()) {
				listProperty.add(tokenizer.nextToken());
			}
		}
		return listProperty;
	}

	private void setListRepositoryProperty(IRepository<?> repository, String key, List<String> listProperty) {
		StringBuilder buffer = new StringBuilder();
		for (Iterator<String> it = listProperty.iterator(); it.hasNext();) {
			String repositoryString = it.next();
			buffer.append(repositoryString);
			if (it.hasNext())
				buffer.append(PIPE);
		}
		String value = (buffer.length() == 0) ? null : buffer.toString();
		repository.setProperty(key, value);
	}

	public Collection<IMetadataRepository> getMetadataRepositories() {
		List<IMetadataRepository> result = new ArrayList<>(metadataRepositories);
		result.add(getMetadataRepository());
		return result;
	}

}
