/*******************************************************************************
 *  Copyright (c) 2007, 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
 *     Genuitec, LLC - added license support
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.metadata.repository.io;

import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.metadata.RequiredCapability;
import org.eclipse.equinox.internal.p2.metadata.RequiredPropertiesMatch;
import org.eclipse.equinox.internal.p2.metadata.repository.Constants;
import org.eclipse.equinox.internal.p2.persistence.XMLWriter;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.metadata.expression.*;

public class MetadataWriter extends XMLWriter implements XMLConstants {

	public MetadataWriter(OutputStream output, ProcessingInstruction[] piElements) {
		super(output, piElements);
		// TODO: add a processing instruction for the metadata version
	}

	/**
	 * Writes a list of {@link IInstallableUnit}.
	 * @param units An Iterator of {@link IInstallableUnit}.
	 * @param size The number of units to write
	 */
	public void writeInstallableUnits(Iterator<IInstallableUnit> units, int size) {
		if (!units.hasNext())
			return;
		start(INSTALLABLE_UNITS_ELEMENT);

		// The size is a bummer. Is it really needed? It forces the use of a collect
		attribute(COLLECTION_SIZE_ATTRIBUTE, size);
		while (units.hasNext())
			writeInstallableUnit(units.next());
		end(INSTALLABLE_UNITS_ELEMENT);
	}

	protected void writeInstallableUnit(IInstallableUnit resolvedIU) {
		IInstallableUnit iu = resolvedIU.unresolved();
		start(INSTALLABLE_UNIT_ELEMENT);
		attribute(ID_ATTRIBUTE, iu.getId());
		attribute(VERSION_ATTRIBUTE, iu.getVersion());
		attribute(SINGLETON_ATTRIBUTE, iu.isSingleton(), true);
		//		attribute(FRAGMENT_ATTRIBUTE, iu.isFragment(), false);

		boolean simpleRequirements = hasOnlySimpleRequirements(iu);
		if (!simpleRequirements)
			attribute(GENERATION_ATTRIBUTE, 2);

		if (iu instanceof IInstallableUnitFragment) {
			IInstallableUnitFragment fragment = (IInstallableUnitFragment) iu;
			writeHostRequirements(fragment.getHost());
		}

		if (iu instanceof IInstallableUnitPatch) {
			IInstallableUnitPatch patch = (IInstallableUnitPatch) iu;
			writeApplicabilityScope(patch.getApplicabilityScope());
			writeRequirementsChange(patch.getRequirementsChange());
			writeLifeCycle(patch.getLifeCycle());
		}

		writeUpdateDescriptor(resolvedIU, resolvedIU.getUpdateDescriptor());
		writeProperties(iu.getProperties());
		writeMetaRequirements(iu.getMetaRequirements());
		writeProvidedCapabilities(iu.getProvidedCapabilities());
		if (simpleRequirements && iu instanceof IInstallableUnitFragment) {
			Collection<IRequirement> mergedRequirementsAndFragmentHostForPre36Compatibility = new LinkedHashSet<>(iu.getRequirements());
			mergedRequirementsAndFragmentHostForPre36Compatibility.addAll(((IInstallableUnitFragment) iu).getHost());
			writeRequirements(mergedRequirementsAndFragmentHostForPre36Compatibility);
		} else {
			writeRequirements(iu.getRequirements());
		}
		writeTrimmedCdata(IU_FILTER_ELEMENT, iu.getFilter() == null ? null : iu.getFilter().getParameters()[0].toString());

		writeArtifactKeys(iu.getArtifacts());
		writeTouchpointType(iu.getTouchpointType());
		writeTouchpointData(iu.getTouchpointData());
		writeLicenses(iu.getLicenses());
		writeCopyright(iu.getCopyright());

		end(INSTALLABLE_UNIT_ELEMENT);
	}

	private boolean hasOnlySimpleRequirements(IInstallableUnit iu) {
		for (IRequirement r : iu.getRequirements())
			if (r.getMax() == 0 || !RequiredCapability.isVersionRangeRequirement(r.getMatches()))
				return false;

		if (iu.getUpdateDescriptor() != null) {
			for (IMatchExpression<IInstallableUnit> m : iu.getUpdateDescriptor().getIUsBeingUpdated()) {
				if (!RequiredCapability.isVersionRangeRequirement(m))
					return false;
			}
		}

		for (IRequirement r : iu.getMetaRequirements())
			if (r.getMax() == 0 || !RequiredCapability.isVersionRangeRequirement(r.getMatches()))
				return false;

		if (iu instanceof IInstallableUnitFragment) {
			for (IRequirement r : ((IInstallableUnitFragment) iu).getHost())
				if (!RequiredCapability.isVersionRangeRequirement(r.getMatches()))
					return false;
		}

		if (iu instanceof IInstallableUnitPatch) {
			IInstallableUnitPatch iuPatch = (IInstallableUnitPatch) iu;
			for (IRequirement[] rArr : iuPatch.getApplicabilityScope())
				for (IRequirement r : rArr)
					if (!RequiredCapability.isVersionRangeRequirement(r.getMatches()))
						return false;

			IRequirement lifeCycle = iuPatch.getLifeCycle();
			if (lifeCycle != null && !RequiredCapability.isVersionRangeRequirement(lifeCycle.getMatches()))
				return false;
		}
		return true;
	}

	protected void writeLifeCycle(IRequirement capability) {
		if (capability == null)
			return;
		start(LIFECYCLE);
		writeRequirement(capability);
		end(LIFECYCLE);
	}

	protected void writeHostRequirements(Collection<IRequirement> hostRequirements) {
		if (hostRequirements != null && hostRequirements.size() > 0) {
			start(HOST_REQUIREMENTS_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, hostRequirements.size());
			for (IRequirement req : hostRequirements) {
				writeRequirement(req);
			}
			end(HOST_REQUIREMENTS_ELEMENT);
		}
	}

	protected void writeProvidedCapabilities(Collection<IProvidedCapability> capabilities) {
		if (capabilities != null && capabilities.size() > 0) {
			start(PROVIDED_CAPABILITIES_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, capabilities.size());
			for (IProvidedCapability capability : capabilities) {
				writeProvidedCapability(capability);
			}
			end(PROVIDED_CAPABILITIES_ELEMENT);
		}
	}

	protected void writeProvidedCapability(IProvidedCapability capability) {
		start(PROVIDED_CAPABILITY_ELEMENT);

		attribute(NAMESPACE_ATTRIBUTE, capability.getNamespace());
		attribute(NAME_ATTRIBUTE, capability.getName());
		attribute(VERSION_ATTRIBUTE, capability.getVersion());

		Map<String, Object> props = new HashMap<>(capability.getProperties());
		props.remove(capability.getNamespace());
		props.remove(IProvidedCapability.PROPERTY_VERSION);

		if (!props.isEmpty()) {
			writeProperties(props);
		}

		end(PROVIDED_CAPABILITY_ELEMENT);
	}

	protected void writeMetaRequirements(Collection<IRequirement> metaRequirements) {
		if (metaRequirements != null && metaRequirements.size() > 0) {
			start(META_REQUIREMENTS_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, metaRequirements.size());
			for (IRequirement req : metaRequirements) {
				writeRequirement(req);
			}
			end(META_REQUIREMENTS_ELEMENT);
		}
	}

	protected void writeRequirements(Collection<IRequirement> requirements) {
		if (requirements != null && requirements.size() > 0) {
			start(REQUIREMENTS_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, requirements.size());
			for (IRequirement req : requirements) {
				writeRequirement(req);
			}
			end(REQUIREMENTS_ELEMENT);
		}
	}

	protected void writeUpdateDescriptor(IInstallableUnit iu, IUpdateDescriptor descriptor) {
		if (descriptor == null)
			return;

		if (descriptor.getIUsBeingUpdated().size() > 1)
			throw new IllegalStateException();
		IMatchExpression<IInstallableUnit> singleUD = descriptor.getIUsBeingUpdated().iterator().next();
		start(UPDATE_DESCRIPTOR_ELEMENT);
		if (RequiredCapability.isVersionRangeRequirement(singleUD)) {
			attribute(ID_ATTRIBUTE, RequiredCapability.extractName(singleUD));
			attribute(VERSION_RANGE_ATTRIBUTE, RequiredCapability.extractRange(singleUD));
		} else {
			writeMatchExpression(singleUD);
		}
		attribute(UPDATE_DESCRIPTOR_SEVERITY, descriptor.getSeverity());
		attribute(DESCRIPTION_ATTRIBUTE, descriptor.getDescription());
		end(UPDATE_DESCRIPTOR_ELEMENT);
	}

	protected void writeApplicabilityScope(IRequirement[][] capabilities) {
		start(APPLICABILITY_SCOPE);
		for (IRequirement[] capability : capabilities) {
			start(APPLY_ON);
			writeRequirements(Arrays.asList(capability));
			end(APPLY_ON);
		}
		end(APPLICABILITY_SCOPE);
	}

	protected void writeRequirementsChange(List<IRequirementChange> changes) {
		start(REQUIREMENT_CHANGES);
		for (IRequirementChange change : changes) {
			writeRequirementChange(change);
		}
		end(REQUIREMENT_CHANGES);
	}

	protected void writeRequirementChange(IRequirementChange change) {
		start(REQUIREMENT_CHANGE);
		if (change.applyOn() != null) {
			start(REQUIREMENT_FROM);
			writeRequirement(change.applyOn());
			end(REQUIREMENT_FROM);
		}
		if (change.newValue() != null) {
			start(REQUIREMENT_TO);
			writeRequirement(change.newValue());
			end(REQUIREMENT_TO);
		}
		end(REQUIREMENT_CHANGE);
	}

	protected void writeRequirement(IRequirement requirement) {
		IMatchExpression<IInstallableUnit> match = requirement.getMatches();

		// A (namespace, name, version-range) type of requirement
		if (requirement.getMax() > 0 && RequiredCapability.isVersionRangeRequirement(match)) {
			start(REQUIREMENT_ELEMENT);

			attribute(NAMESPACE_ATTRIBUTE, RequiredCapability.extractNamespace(match));
			attribute(NAME_ATTRIBUTE, RequiredCapability.extractName(match));
			attribute(VERSION_RANGE_ATTRIBUTE, RequiredCapability.extractRange(match));
			attribute(REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTE, requirement.getMin() == 0, false);
			attribute(REQUIRED_CAPABILITY_MULTIPLE_ATTRIBUTE, requirement.getMax() > 1, false);
		}
		// A (namespace, attributes-match) type of requirement
		else if (RequiredPropertiesMatch.isPropertiesMatchRequirement(match)) {
			start(REQUIREMENT_PROPERTIES_ELEMENT);

			attribute(NAMESPACE_ATTRIBUTE, RequiredPropertiesMatch.extractNamespace(match));
			attribute(MATCH_ATTRIBUTE, RequiredPropertiesMatch.extractPropertiesMatch(match));

			if (requirement.getMin() != 1) {
				attribute(MIN_ATTRIBUTE, requirement.getMin());
			}

			if (requirement.getMax() != 1) {
				attribute(MAX_ATTRIBUTE, requirement.getMax());
			}
		}
		// A general match expression type of requirement
		else {
			start(REQUIREMENT_ELEMENT);

			writeMatchExpression(match);

			if (requirement.getMin() != 1) {
				attribute(MIN_ATTRIBUTE, requirement.getMin());
			}

			if (requirement.getMax() != 1) {
				attribute(MAX_ATTRIBUTE, requirement.getMax());
			}
		}

		attribute(REQUIREMENT_GREED_ATTRIBUTE, requirement.isGreedy(), true);

		if (requirement.getFilter() != null) {
			writeTrimmedCdata(REQUIREMENT_FILTER_ELEMENT, requirement.getFilter().getParameters()[0].toString());
		}

		if (requirement.getDescription() != null) {
			writeTrimmedCdata(REQUIREMENT_DESCRIPTION_ELEMENT, requirement.getDescription());
		}

		end();
	}

	private void writeMatchExpression(IMatchExpression<IInstallableUnit> match) {
		attribute(MATCH_ATTRIBUTE, ExpressionUtil.getOperand(match));
		Object[] params = match.getParameters();
		if (params.length > 0) {
			IExpressionFactory factory = ExpressionUtil.getFactory();
			IExpression[] constantArray = new IExpression[params.length];
			for (int idx = 0; idx < params.length; ++idx)
				constantArray[idx] = factory.constant(params[idx]);
			attribute(MATCH_PARAMETERS_ATTRIBUTE, factory.array(constantArray));
		}
	}

	protected void writeArtifactKeys(Collection<IArtifactKey> artifactKeys) {
		if (artifactKeys != null && artifactKeys.size() > 0) {
			start(ARTIFACT_KEYS_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, artifactKeys.size());
			for (IArtifactKey artifactKey : artifactKeys) {
				start(ARTIFACT_KEY_ELEMENT);
				attribute(ARTIFACT_KEY_CLASSIFIER_ATTRIBUTE, artifactKey.getClassifier());
				attribute(ID_ATTRIBUTE, artifactKey.getId());
				attribute(VERSION_ATTRIBUTE, artifactKey.getVersion());
				end(ARTIFACT_KEY_ELEMENT);
			}
			end(ARTIFACT_KEYS_ELEMENT);
		}
	}

	protected void writeTouchpointType(ITouchpointType touchpointType) {
		start(TOUCHPOINT_TYPE_ELEMENT);
		attribute(ID_ATTRIBUTE, touchpointType.getId());
		attribute(VERSION_ATTRIBUTE, touchpointType.getVersion());
		end(TOUCHPOINT_TYPE_ELEMENT);
	}

	protected void writeTouchpointData(Collection<ITouchpointData> touchpointData) {
		if (touchpointData != null && touchpointData.size() > 0) {
			start(TOUCHPOINT_DATA_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, touchpointData.size());
			for (ITouchpointData nextData : touchpointData) {
				Map<String, ITouchpointInstruction> instructions = nextData.getInstructions();
				if (instructions.size() > 0) {
					start(TOUCHPOINT_DATA_INSTRUCTIONS_ELEMENT);
					attribute(COLLECTION_SIZE_ATTRIBUTE, instructions.size());
					for (Map.Entry<String, ITouchpointInstruction> entry : instructions.entrySet()) {
						start(TOUCHPOINT_DATA_INSTRUCTION_ELEMENT);
						attribute(TOUCHPOINT_DATA_INSTRUCTION_KEY_ATTRIBUTE, entry.getKey());
						ITouchpointInstruction instruction = entry.getValue();
						if (instruction.getImportAttribute() != null)
							attribute(TOUCHPOINT_DATA_INSTRUCTION_IMPORT_ATTRIBUTE, instruction.getImportAttribute());
						cdata(instruction.getBody(), true);
						end(TOUCHPOINT_DATA_INSTRUCTION_ELEMENT);
					}
					end(TOUCHPOINT_DATA_INSTRUCTIONS_ELEMENT);
				}
			}
			end(TOUCHPOINT_DATA_ELEMENT);
		}
	}

	private void writeTrimmedCdata(String element, String filter) {
		String trimmed;
		if (filter != null && (trimmed = filter.trim()).length() > 0) {
			start(element);
			cdata(trimmed);
			end(element);
		}
	}

	private void writeLicenses(Collection<ILicense> licenses) {
		if (licenses != null && licenses.size() > 0) {
			// In the future there may be more than one license, so we write this 
			// as a collection of one.
			// See bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=216911
			start(LICENSES_ELEMENT);
			attribute(COLLECTION_SIZE_ATTRIBUTE, licenses.size());
			for (ILicense license : licenses) {
				if (license == null)
					continue;
				start(LICENSE_ELEMENT);
				if (license.getLocation() != null) {
					attribute(URI_ATTRIBUTE, license.getLocation().toString());

					try {
						// we write the URL attribute for backwards compatibility with 3.4.x
						// this attribute should be removed if we make a breaking format change.
						attribute(URL_ATTRIBUTE, URIUtil.toURL(license.getLocation()).toExternalForm());
					} catch (MalformedURLException e) {
						attribute(URL_ATTRIBUTE, license.getLocation().toString());
					}
				}
				cdata(license.getBody(), true);
				end(LICENSE_ELEMENT);
			}
			end(LICENSES_ELEMENT);
		}
	}

	private void writeCopyright(ICopyright copyright) {
		if (copyright != null) {
			start(COPYRIGHT_ELEMENT);
			try {
				if (copyright.getLocation() != null) {
					attribute(URI_ATTRIBUTE, copyright.getLocation().toString());
					try {
						// we write the URL attribute for backwards compatibility with 3.4.x
						// this attribute should be removed if we make a breaking format change.
						attribute(URL_ATTRIBUTE, URIUtil.toURL(copyright.getLocation()).toExternalForm());
					} catch (MalformedURLException e) {
						attribute(URL_ATTRIBUTE, copyright.getLocation().toString());
					}
				}
			} catch (IllegalStateException ise) {
				LogHelper.log(new Status(IStatus.INFO, Constants.ID, "Error writing the copyright URL: " + copyright.getLocation())); //$NON-NLS-1$
			}
			cdata(copyright.getBody(), true);
			end(COPYRIGHT_ELEMENT);
		}
	}
}
