/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.aries.subsystem.core.archive;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.apache.aries.subsystem.core.internal.BasicSubsystem;
import org.apache.aries.util.manifest.ManifestProcessor;
import org.osgi.framework.Constants;
import org.osgi.resource.Resource;
import org.osgi.service.resolver.ResolutionException;
import org.osgi.service.subsystem.Subsystem;
import org.osgi.service.subsystem.SubsystemConstants;

public class DeploymentManifest {
	public static class Builder {
		private Map<String, Header<?>> headers = new HashMap<String, Header<?>>();
		
		public DeploymentManifest build() {
			return new DeploymentManifest(headers);
		}
		
		public Builder autostart(boolean value) {
			header(new GenericHeader(ARIESSUBSYSTEM_AUTOSTART, Boolean.toString(value)));
			return this;
		}
		
		public Builder content(Resource resource, boolean referenced) {
			DeployedContentHeader header = (DeployedContentHeader)headers.get(DeploymentManifest.DEPLOYED_CONTENT);
			if (header == null) {
				DeployedContentHeader.Clause clause = new DeployedContentHeader.Clause(resource, referenced);
				header(new DeployedContentHeader(Collections.singleton(clause)));
				return this;
			}
			DeployedContentHeader.Clause clause = header.getClause(resource);
			if (clause == null) {
				clause = new DeployedContentHeader.Clause(resource, referenced);
				List<DeployedContentHeader.Clause> clauses = new ArrayList<DeployedContentHeader.Clause>(header.getClauses().size() + 1);
				clauses.addAll(header.getClauses());
				clauses.add(clause);
				header(new DeployedContentHeader(clauses));
				return this;
			}
			
			Collection<DeployedContentHeader.Clause> clauses = new ArrayList<DeployedContentHeader.Clause>(header.getClauses());
			for (Iterator<DeployedContentHeader.Clause> i = clauses.iterator(); i.hasNext();)
				if (clause.equals(i.next())) {
					i.remove();
					break;
				}
			clauses.add(new DeployedContentHeader.Clause(resource, referenced));
			header(new DeployedContentHeader(clauses));
			return this;
		}
		
		public Builder header(Header<?> value) {
			if (value != null)
				headers.put(value.getName(), value);
			return this;
		}
		
		public Builder id(long value) {
			header(new GenericHeader(ARIESSUBSYSTEM_ID, Long.toString(value)));
			return this;
		}
		
		public Builder lastId(long value) {
			header(new GenericHeader(ARIESSUBSYSTEM_LASTID, Long.toString(value)));
			return this;
		}
		
		public Builder location(String value) {
			if (value != null)
				header(new AriesSubsystemLocationHeader(value));
			return this;
		}
		
		public Builder manifest(DeploymentManifest value) {
			if (value != null)
				for (Entry<String, Header<?>> entry : value.getHeaders().entrySet())
					header(entry.getValue());
			return this;
		}
		
		public Builder manifest(SubsystemManifest value) {
			if (value != null)
				for (Entry<String, Header<?>> entry : value.getHeaders().entrySet())
					header(entry.getValue());
			return this;
		}
		
		public Builder parent(BasicSubsystem value, boolean referenceCount) {
			AriesSubsystemParentsHeader.Clause clause = new AriesSubsystemParentsHeader.Clause(value, referenceCount);
			AriesSubsystemParentsHeader header = (AriesSubsystemParentsHeader)headers.get(ARIESSUBSYSTEM_PARENTS);
			if (header == null)
				header(new AriesSubsystemParentsHeader(Collections.singleton(clause)));
			else {
				Collection<AriesSubsystemParentsHeader.Clause> clauses = new ArrayList<AriesSubsystemParentsHeader.Clause>(header.getClauses().size() + 1);
				clauses.addAll(header.getClauses());
				clauses.add(clause);
				header(new AriesSubsystemParentsHeader(clauses));
			}
			return this;
		}
		
		public Builder region(String value) {
			if (value != null)
				header(new GenericHeader(ARIESSUBSYSTEM_REGION, value));
			return this;
		}
		
		public Builder region(org.eclipse.equinox.region.Region value) {
			if (value != null)
				region(value.getName());
			return this;
		}
		
		public Builder state(Subsystem.State value) {
			if (value != null)
				header(new GenericHeader(ARIESSUBSYSTEM_STATE, value.toString()));
			return this;
		}
	}
	
	public static final String DEPLOYED_CONTENT = SubsystemConstants.DEPLOYED_CONTENT;
	public static final String DEPLOYMENT_MANIFESTVERSION = SubsystemConstants.DEPLOYMENT_MANIFESTVERSION;
	public static final String EXPORT_PACKAGE = Constants.EXPORT_PACKAGE;
	public static final String IMPORT_PACKAGE = Constants.IMPORT_PACKAGE;
	public static final String PROVIDE_CAPABILITY = Constants.PROVIDE_CAPABILITY;
	public static final String PROVISION_RESOURCE = SubsystemConstants.PROVISION_RESOURCE;
	public static final String REQUIRE_BUNDLE = Constants.REQUIRE_BUNDLE;
	public static final String REQUIRE_CAPABILITY = Constants.REQUIRE_CAPABILITY;
	public static final String SUBSYSTEM_EXPORTSERVICE = SubsystemConstants.SUBSYSTEM_EXPORTSERVICE;
	public static final String SUBSYSTEM_IMPORTSERVICE = SubsystemConstants.SUBSYSTEM_IMPORTSERVICE;
	public static final String SUBSYSTEM_SYMBOLICNAME = SubsystemConstants.SUBSYSTEM_SYMBOLICNAME;
	public static final String SUBSYSTEM_VERSION = SubsystemConstants.SUBSYSTEM_VERSION;
	
	public static final String ARIESSUBSYSTEM_AUTOSTART = "AriesSubsystem-Autostart";
	public static final String ARIESSUBSYSTEM_ID = "AriesSubsystem-Id";
	public static final String ARIESSUBSYSTEM_LASTID = "AriesSubsystem-LastId";
	public static final String ARIESSUBSYSTEM_LOCATION = AriesSubsystemLocationHeader.NAME;
	public static final String ARIESSUBSYSTEM_PARENTS = "AriesSubsystem-Parents";
	public static final String ARIESSUBSYSTEM_REGION = "AriesSubsystem-Region";
	public static final String ARIESSUBSYSTEM_STATE = "AriesSubsystem-State";
	
	private final Map<String, Header<?>> headers;
	
	public DeploymentManifest(java.util.jar.Manifest manifest) {
		headers = new HashMap<String, Header<?>>();
		for (Entry<Object, Object> entry : manifest.getMainAttributes().entrySet()) {
			String key = String.valueOf(entry.getKey());
			if (key.equals(SubsystemManifest.SUBSYSTEM_SYMBOLICNAME))
				continue;
			headers.put(key, HeaderFactory.createHeader(key, String.valueOf(entry.getValue())));
		}
	}
	
	public DeploymentManifest(File file) throws IOException {
		this(new FileInputStream(file));
	}
	
	public DeploymentManifest(InputStream in) throws IOException {
		Manifest manifest = ManifestProcessor.parseManifest(in);
		Attributes attributes = manifest.getMainAttributes();
		Map<String, Header<?>> headers = new HashMap<String, Header<?>>(attributes.size() + 4); // Plus the # of potentially derived headers.
		for (Entry<Object, Object> entry : attributes.entrySet()) {
			String key = String.valueOf(entry.getKey());
			headers.put(key, HeaderFactory.createHeader(key, String.valueOf(entry.getValue())));
		}
		this.headers = Collections.unmodifiableMap(headers);
	}
	
	public DeploymentManifest(
			DeploymentManifest deploymentManifest, 
			SubsystemManifest subsystemManifest, 
			boolean autostart, 
			long id, 
			long lastId, 
			String location,
			boolean overwrite,
			boolean acceptDependencies) throws ResolutionException, IOException, URISyntaxException {
		Map<String, Header<?>> headers;
		if (deploymentManifest == null // We're generating a new deployment manifest.
				|| (deploymentManifest != null && overwrite)) { // A deployment manifest already exists but overwriting it with subsystem manifest content is desired.
			headers = computeHeaders(subsystemManifest);
		}
		else {
			headers = new HashMap<String, Header<?>>(deploymentManifest.getHeaders());
		}
		// TODO DEPLOYMENT_MANIFESTVERSION
		headers.put(ARIESSUBSYSTEM_AUTOSTART, new GenericHeader(ARIESSUBSYSTEM_AUTOSTART, Boolean.toString(autostart)));
		headers.put(ARIESSUBSYSTEM_ID, new GenericHeader(ARIESSUBSYSTEM_ID, Long.toString(id)));
		headers.put(ARIESSUBSYSTEM_LOCATION, new AriesSubsystemLocationHeader(location));
		headers.put(ARIESSUBSYSTEM_LASTID, new GenericHeader(ARIESSUBSYSTEM_LASTID, Long.toString(lastId)));
		this.headers = Collections.unmodifiableMap(headers);
	}
	
	private DeploymentManifest(Map<String, Header<?>> headers) {
		Map<String, Header<?>> map = new HashMap<String, Header<?>>(headers);
		this.headers = Collections.unmodifiableMap(map);
	}
	
	public DeployedContentHeader getDeployedContentHeader() {
		return (DeployedContentHeader)getHeaders().get(DEPLOYED_CONTENT);
	}
	
	public ExportPackageHeader getExportPackageHeader() {
		return (ExportPackageHeader)getHeaders().get(EXPORT_PACKAGE);
	}
	
	public Map<String, Header<?>> getHeaders() {
		return headers;
	}
	
	public AriesSubsystemParentsHeader getAriesSubsystemParentsHeader() {
		return (AriesSubsystemParentsHeader)getHeaders().get(ARIESSUBSYSTEM_PARENTS);
	}
	
	public ImportPackageHeader getImportPackageHeader() {
		return (ImportPackageHeader)getHeaders().get(IMPORT_PACKAGE);
	}
	
	public ProvideCapabilityHeader getProvideCapabilityHeader() {
		return (ProvideCapabilityHeader)getHeaders().get(PROVIDE_CAPABILITY);
	}
	
	public ProvisionResourceHeader getProvisionResourceHeader() {
		return (ProvisionResourceHeader)getHeaders().get(PROVISION_RESOURCE);
	}
	
	public RequireBundleHeader getRequireBundleHeader() {
		return (RequireBundleHeader)getHeaders().get(REQUIRE_BUNDLE);
	}
	
	public RequireCapabilityHeader getRequireCapabilityHeader() {
		return (RequireCapabilityHeader)getHeaders().get(REQUIRE_CAPABILITY);
	}
	
	public SubsystemExportServiceHeader getSubsystemExportServiceHeader() {
		return (SubsystemExportServiceHeader)getHeaders().get(SUBSYSTEM_EXPORTSERVICE);
	}
	
	public SubsystemImportServiceHeader getSubsystemImportServiceHeader() {
		return (SubsystemImportServiceHeader)getHeaders().get(SUBSYSTEM_IMPORTSERVICE);
	}
	
	public void write(OutputStream out) throws IOException {
		Manifest manifest = new Manifest();
		Attributes attributes = manifest.getMainAttributes();
		// The manifest won't write anything unless the following header is present.
		attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
		for (Entry<String, Header<?>> entry : headers.entrySet()) {
			attributes.putValue(entry.getKey(), entry.getValue().getValue());
		}
		manifest.write(out);
	}
	
	private Map<String, Header<?>> computeHeaders(SubsystemManifest manifest) {
		return new HashMap<String, Header<?>>(manifest.getHeaders());
	}

    @Override
    public int hashCode() {
    	return 31 * 17 + headers.hashCode();
    }

    @Override
    public boolean equals(Object o) {
    	if (o == this) {
    		return true;
    	}
    	if (!(o instanceof SubsystemManifest)) {
    		return false;
    	}
    	DeploymentManifest that = (DeploymentManifest)o;
    	return that.headers.equals(this.headers);
    }
}
