/*******************************************************************************
 *  Copyright (c) 2007, 2020 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
 *     Prashant Deva - Bug 194674 [prov] Provide write access to metadata repository
 *    Christoph Läubrich - Bug 481443 - CLassCastException While Downloading Repository that loads fine in RCP target
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata.repository;

import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.metadata.*;
import org.eclipse.equinox.internal.p2.metadata.index.*;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent;
import org.eclipse.equinox.p2.core.*;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.KeyWithLocale;
import org.eclipse.equinox.p2.metadata.index.IIndex;
import org.eclipse.equinox.p2.metadata.index.IIndexProvider;
import org.eclipse.equinox.p2.query.IQuery;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.repository.*;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.repository.metadata.spi.AbstractMetadataRepository;

/**
 * A metadata repository that resides in the local file system.  If the repository
 * location is a directory, this implementation will traverse the directory structure
 * and combine any metadata repository files that are found.
 */
public class LocalMetadataRepository extends AbstractMetadataRepository implements IIndexProvider<IInstallableUnit> {

	static final private String CONTENT_FILENAME = "content"; //$NON-NLS-1$
	static final private String REPOSITORY_TYPE = LocalMetadataRepository.class.getName();
	static final private Integer REPOSITORY_VERSION = 1;
	static final private String JAR_EXTENSION = ".jar"; //$NON-NLS-1$
	static final private String XML_EXTENSION = ".xml"; //$NON-NLS-1$

	protected IUMap units = new IUMap();
	protected HashSet<IRepositoryReference> repositories = new HashSet<>();
	private IIndex<IInstallableUnit> idIndex;
	private IIndex<IInstallableUnit> capabilityIndex;
	private TranslationSupport translationSupport;
	private boolean snapshotNeeded = false;
	private boolean disableSave = false;

	private static File getActualLocation(URI location, String extension) {
		File spec = URIUtil.toFile(location);
		String path = spec.getAbsolutePath();
		if (path.endsWith(CONTENT_FILENAME + extension)) {
			//todo this is the old code that doesn't look right
			//			return new File(spec + extension);
			return spec;
		}
		if (path.endsWith("/")) //$NON-NLS-1$
			path += CONTENT_FILENAME;
		else
			path += "/" + CONTENT_FILENAME; //$NON-NLS-1$
		return new File(path + extension);
	}

	public static File getActualLocation(URI location) {
		return getActualLocation(location, XML_EXTENSION);
	}

	/**
	 * This no argument constructor is called when restoring an existing repository.
	 */
	public LocalMetadataRepository(IProvisioningAgent agent) {
		super(agent);
	}

	/**
	 * This constructor is used when creating a new local repository.
	 * @param location The location of the repository
	 * @param name The name of the repository
	 */
	public LocalMetadataRepository(IProvisioningAgent agent, URI location, String name, Map<String, String> properties) {
		super(agent, name == null ? (location != null ? location.toString() : "") : name, REPOSITORY_TYPE, REPOSITORY_VERSION.toString(), location, null, null, properties); //$NON-NLS-1$
		if (!location.getScheme().equals("file")) //$NON-NLS-1$
			throw new IllegalArgumentException("Invalid local repository location: " + location); //$NON-NLS-1$
		//when creating a repository, we must ensure it exists on disk so a subsequent load will succeed
		save();
	}

	@Override
	public synchronized void addInstallableUnits(Collection<IInstallableUnit> installableUnits) {
		if (installableUnits == null || installableUnits.isEmpty())
			return;
		if (snapshotNeeded) {
			units = units.clone();
			idIndex = null; // Backed by units
			snapshotNeeded = false;
		}
		units.addAll(installableUnits);
		capabilityIndex = null; // Generated, not backed by units
		save();
	}

	@Override
	public void addReferences(Collection<? extends IRepositoryReference> references) {
		assertModifiable();
		// only write out the repository if we made changes
		if (repositories.addAll(references))
			save();
	}

	@Override
	public Collection<IRepositoryReference> getReferences() {
		return Collections.unmodifiableCollection(repositories);
	}

	@Override
	public synchronized IIndex<IInstallableUnit> getIndex(String memberName) {
		if (InstallableUnit.MEMBER_ID.equals(memberName)) {
			snapshotNeeded = true;
			if (idIndex == null)
				idIndex = new IdIndex(units);
			return idIndex;
		}

		if (InstallableUnit.MEMBER_PROVIDED_CAPABILITIES.equals(memberName)) {
			snapshotNeeded = true;
			if (capabilityIndex == null)
				capabilityIndex = new CapabilityIndex(units.iterator());
			return capabilityIndex;
		}
		return null;
	}

	@Override
	public synchronized Object getManagedProperty(Object client, String memberName, Object key) {
		if (!(client instanceof IInstallableUnit))
			return null;
		IInstallableUnit iu = (IInstallableUnit) client;
		if (InstallableUnit.MEMBER_TRANSLATED_PROPERTIES.equals(memberName)) {
			if (translationSupport == null)
				translationSupport = new TranslationSupport(this);
			return key instanceof KeyWithLocale ? translationSupport.getIUProperty(iu, (KeyWithLocale) key) : translationSupport.getIUProperty(iu, key.toString());
		}
		return null;
	}

	@Override
	public void initialize(RepositoryState state) {
		synchronized (this) {
			setName(state.Name);
			setType(state.Type);
			setVersion(state.Version.toString());
			setProvider(state.Provider);
			setDescription(state.Description);
			setLocation(state.Location);
			setProperties(state.Properties);
			this.units.addAll(state.Units);
			this.repositories.addAll(Arrays.asList(state.Repositories));
		}
		publishRepositoryReferences();
	}

	/**
	 * Broadcast discovery events for all repositories referenced by this repository.
	 */
	public void publishRepositoryReferences() {
		IProvisioningEventBus bus = getProvisioningAgent().getService(IProvisioningEventBus.class);
		if (bus == null)
			return;

		List<IRepositoryReference> repositoriesSnapshot = createRepositoriesSnapshot();
		for (IRepositoryReference reference : repositoriesSnapshot) {
			boolean isEnabled = (reference.getOptions() & IRepository.ENABLED) != 0;
			bus.publishEvent(new RepositoryEvent(reference.getLocation(), reference.getType(), RepositoryEvent.DISCOVERED, isEnabled));
		}
	}

	private synchronized List<IRepositoryReference> createRepositoriesSnapshot() {
		if (repositories.isEmpty())
			return Collections.emptyList();
		return new ArrayList<>(repositories);
	}

	// use this method to setup any transient fields etc after the object has been restored from a stream
	public synchronized void initializeAfterLoad(URI aLocation) {
		setLocation(aLocation);
	}

	@Override
	public boolean isModifiable() {
		return true;
	}

	@Override
	public IQueryResult<IInstallableUnit> query(IQuery<IInstallableUnit> query, IProgressMonitor monitor) {
		return IndexProvider.query(this, query, monitor);
	}

	@Override
	public synchronized Iterator<IInstallableUnit> everything() {
		snapshotNeeded = true;
		return units.iterator();
	}

	@Override
	public synchronized void removeAll() {
		if (snapshotNeeded) {
			units = new IUMap();
			idIndex = null; // Backed by units
			snapshotNeeded = false;
		} else
			units.clear();
		capabilityIndex = null; // Generated, not backed by units.
		save();
	}

	@Override
	public synchronized boolean removeInstallableUnits(Collection<IInstallableUnit> installableUnits) {
		boolean changed = false;
		if (installableUnits != null && !installableUnits.isEmpty()) {
			changed = true;
			if (snapshotNeeded) {
				units = units.clone();
				idIndex = null; // Backed by units
				snapshotNeeded = false;
			}
			units.removeAll(installableUnits);
			capabilityIndex = null; // Generated, not backed by units.
		}
		if (changed)
			save();
		return changed;
	}

	// caller should be synchronized
	/**
	 * Marking protected so we can test.  This is internal, so it shouldn't matter, but I'll
	 * mark it as no override just to be clear.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 */
	protected void save() {
		if (disableSave)
			return;
		File file = getActualLocation(getLocation());
		File jarFile = getActualLocation(getLocation(), JAR_EXTENSION);
		boolean compress = "true".equalsIgnoreCase(getProperty(PROP_COMPRESSED)); //$NON-NLS-1$
		try {
			OutputStream output = null;
			if (!compress) {
				if (jarFile.exists()) {
					jarFile.delete();
				}
				if (!file.exists()) {
					if (!file.getParentFile().exists())
						file.getParentFile().mkdirs();
					file.createNewFile();
				}
				output = new FileOutputStream(file);
			} else {
				if (file.exists()) {
					file.delete();
				}
				if (!jarFile.exists()) {
					if (!jarFile.getParentFile().exists())
						jarFile.getParentFile().mkdirs();
					jarFile.createNewFile();
				}
				JarEntry jarEntry = new JarEntry(file.getName());
				output = new JarOutputStream(new FileOutputStream(jarFile));
				((JarOutputStream) output).putNextEntry(jarEntry);
			}
			super.setProperty(IRepository.PROP_TIMESTAMP, Long.toString(System.currentTimeMillis()), new NullProgressMonitor());
			new MetadataRepositoryIO(getProvisioningAgent()).write(this, output);
		} catch (IOException e) {
			LogHelper.log(new Status(IStatus.ERROR, Constants.ID, ProvisionException.REPOSITORY_FAILED_WRITE, "Error saving metadata repository: " + getLocation(), e)); //$NON-NLS-1$
		}
	}

	@Override
	public String setProperty(String key, String newValue, IProgressMonitor monitor) {
		try {
			String oldValue = null;
			synchronized (this) {
				oldValue = super.setProperty(key, newValue, monitor);
				if (oldValue == newValue || (oldValue != null && oldValue.equals(newValue)))
					return oldValue;
				save();
			}
			IMetadataRepositoryManager manager = getProvisioningAgent()
					.getService(IMetadataRepositoryManager.class);
			if (manager instanceof MetadataRepositoryManager) {
				// force repository manager to reload this repository because it caches
				// properties
				if (manager.removeRepository(getLocation()))
					((MetadataRepositoryManager) manager).addRepository(this);
			}
			return oldValue;
		} finally {
			if (monitor != null)
				monitor.done();
		}
	}

	@Override
	public IStatus executeBatch(IRunnableWithProgress runnable, IProgressMonitor monitor) {
		IStatus result = null;
		synchronized (this) {
			try {
				disableSave = true;
				runnable.run(monitor);
			} catch (OperationCanceledException oce) {
				return new Status(IStatus.CANCEL, Constants.ID, oce.getMessage(), oce);
			} catch (Throwable e) {
				result = new Status(IStatus.ERROR, Constants.ID, e.getMessage(), e);
			} finally {
				disableSave = false;
				try {
					save();
				} catch (Exception e) {
					if (result != null)
						result = new MultiStatus(Constants.ID, IStatus.ERROR, new IStatus[] {result}, e.getMessage(), e);
					else
						result = new Status(IStatus.ERROR, Constants.ID, e.getMessage(), e);
				}
			}
		}
		if (result == null)
			result = Status.OK_STATUS;
		return result;
	}

	@Override
	public void compress(IPool<IInstallableUnit> iuPool) {
		units.compress(iuPool);
	}
}
