/*
 * 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.internal;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.aries.subsystem.core.archive.DeploymentManifest;
import org.apache.aries.subsystem.core.internal.BundleResourceInstaller.BundleConstituent;
import org.apache.aries.util.io.IOUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Resource;
import org.osgi.service.coordinator.Coordination;
import org.osgi.service.subsystem.Subsystem;
import org.osgi.service.subsystem.Subsystem.State;
import org.osgi.service.subsystem.SubsystemException;

public class Subsystems {
	private BasicSubsystem root;
	private volatile SubsystemGraph graph;
	
	private final Map<Long, BasicSubsystem> idToSubsystem = new HashMap<Long, BasicSubsystem>();
	private final Map<String, BasicSubsystem> locationToSubsystem = new HashMap<String, BasicSubsystem>();
	private final ResourceReferences resourceReferences = new ResourceReferences();
	private final Map<BasicSubsystem, Set<Resource>> subsystemToConstituents = new HashMap<BasicSubsystem, Set<Resource>>();
	
	public void addChild(BasicSubsystem parent, BasicSubsystem child, boolean referenceCount) {
		graph.add(parent, child);
		child.addedParent(parent, referenceCount);
	}
	
	public void addConstituent(BasicSubsystem subsystem, Resource constituent, boolean referenced) {
		synchronized (subsystemToConstituents) {
			Set<Resource> constituents = subsystemToConstituents.get(subsystem);
			if (constituents == null) {
				constituents = new HashSet<Resource>();
				subsystemToConstituents.put(subsystem, constituents);
			}
			constituents.add(constituent);
		}
		subsystem.addedConstituent(constituent, referenced);
	}
	
	public void addReference(BasicSubsystem subsystem, Resource resource) {
		resourceReferences.addReference(subsystem, resource);
	}
	
	public void addSubsystem(BasicSubsystem subsystem) {
		synchronized (idToSubsystem) {
			synchronized (locationToSubsystem) {
				addIdToSubsystem(subsystem);
				addLocationToSubsystem(subsystem);
			}
		}
	}
	
	public Collection<Subsystem> getChildren(BasicSubsystem parent) {
		return graph.getChildren(parent);
	}
	
	public Collection<Resource> getConstituents(BasicSubsystem subsystem) {
		synchronized (subsystemToConstituents) {
			Collection<Resource> result = subsystemToConstituents.get(subsystem);
			if (result == null)
				return Collections.emptyList();
			return Collections.unmodifiableCollection(new ArrayList<Resource>(result));
		}
	}
	
	public Collection<Subsystem> getParents(BasicSubsystem child) {
		return graph.getParents(child);
	}
	
	public Collection<Resource> getResourcesReferencedBy(BasicSubsystem subsystem) {
		return resourceReferences.getResources(subsystem);
	}
	
	public synchronized BasicSubsystem getRootSubsystem() {
		if (root == null) {
			File file = Activator.getInstance().getBundleContext().getDataFile("");
			File[] fileArray = file.listFiles();
			List<File> fileList = new ArrayList<File>(Arrays.asList(fileArray));
			Collections.sort(fileList, new Comparator<File>() {
				@Override
				public int compare(File file1, File file2) {
					String name1 = file1.getName();
					String name2 = file2.getName();
					return Long.valueOf(name1).compareTo(Long.valueOf(name2));
				}
			});
			if (fileList.isEmpty()) {
				// There are no persisted subsystems, including root.
				SubsystemResource resource;
				try {
					resource = new SubsystemResource(file);
				}
				catch (SubsystemException e) {
					throw e;
				}
				catch (Exception e) {
					throw new SubsystemException(e);
				}
				Coordination coordination = Utils.createCoordination();
				try {
					root = new BasicSubsystem(resource);
					// TODO This initialization is a bit brittle. The root subsystem
					// must be gotten before anything else will be able to use the
					// graph. At the very least, throw IllegalStateException where
					// appropriate.
					graph = new SubsystemGraph(root);
					ResourceInstaller.newInstance(coordination, root, root).install();
					populateRootSubsystem(root, coordination);
				} catch (Exception e) {
					coordination.fail(e);
				} finally {
					coordination.end();
				}
			}
			else {
				// There are persisted subsystems.
				Coordination coordination = Utils.createCoordination();
				Collection<BasicSubsystem> subsystems = new ArrayList<BasicSubsystem>(fileList.size());
				try {
					for (File f : fileList) {
						BasicSubsystem s = new BasicSubsystem(f);
						if (State.UNINSTALLED.equals(s.getState())) {
							// left over cache, delete this
							IOUtils.deleteRecursive(f);
						} else {
							subsystems.add(s);
							addSubsystem(s);
						}
					}
					root = getSubsystemById(0);
					SubsystemIdentifier.setLastId(
							Long.parseLong(
									root.getDeploymentManifest().getHeaders().get(
											DeploymentManifest.ARIESSUBSYSTEM_LASTID).getValue()));
					graph = new SubsystemGraph(root);
					ResourceInstaller.newInstance(coordination, root, root).install();
					populateRootSubsystem(root, coordination);
				} catch (Exception e) {
					coordination.fail(e);
				} finally {
					coordination.end();
				}
			}
		}
		return root;
	}
	
	private void populateRootSubsystem(BasicSubsystem root, Coordination coordination) throws Exception {
		// TODO Begin proof of concept.
		// This is a proof of concept for initializing the relationships between the root subsystem and bundles
		// that already existed in its region. Not sure this will be the final resting place. Plus, there are issues
		// since this does not take into account the possibility of already existing bundles going away or new bundles
		// being installed out of band while this initialization is taking place. Need a bundle event hook for that.
		BundleContext context = Activator.getInstance().getBundleContext().getBundle(org.osgi.framework.Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext();
		for (Bundle bundle : context.getBundles()) {
			BundleRevision revision = bundle.adapt(BundleRevision.class);
			if (revision == null)
				// The bundle has been uninstalled. Do not process.
				continue;
			if (!resourceReferences.getSubsystems(revision).isEmpty())
				continue;
			ResourceInstaller.newInstance(coordination, revision, root).install();
		}
		// TODO End proof of concept.
	}
	
	public BasicSubsystem getSubsystemById(long id) {
		synchronized (idToSubsystem) {
			return idToSubsystem.get(id);
		}
	}
	
	public BasicSubsystem getSubsystemByLocation(String location) {
		synchronized (locationToSubsystem) {
			return locationToSubsystem.get(location);
		}
	}
	
	public Collection<BasicSubsystem> getSubsystems() {
		return new ArrayList<BasicSubsystem>(idToSubsystem.values());
	}
	
	// TODO Not very pretty. A quick fix.
	public Object[] getSubsystemsByBundle(Bundle bundle) {
		BundleRevision revision = null;
		ArrayList<BasicSubsystem> result = new ArrayList<BasicSubsystem>();
		synchronized (subsystemToConstituents) {
			for (BasicSubsystem subsystem : subsystemToConstituents.keySet()) {
				for (Resource constituent : getConstituents(subsystem)) {
					if (constituent instanceof BundleConstituent &&
							((BundleConstituent)constituent).getBundle() == bundle) {
						result.add(subsystem);
						revision = ((BundleConstituent)constituent).getRevision();
					}
				}	
			}
		}
		result.trimToSize();
		if (revision == null)
			return null;
		return new Object[]{revision, result};
	}
	
	public Collection<BasicSubsystem> getSubsystemsByConstituent(Resource constituent) {
		ArrayList<BasicSubsystem> result = new ArrayList<BasicSubsystem>();
		synchronized (subsystemToConstituents) {
			for (BasicSubsystem subsystem : subsystemToConstituents.keySet())
				if (getConstituents(subsystem).contains(constituent))
					result.add(subsystem);
		}
		result.trimToSize();
		return result;
	}
	
	public Collection<BasicSubsystem> getSubsystemsReferencing(Resource resource) {
		return resourceReferences.getSubsystems(resource);
	}
	
	public void removeChild(BasicSubsystem child) {
		graph.remove(child);
	}
	
	public void removeChild(BasicSubsystem parent, BasicSubsystem child) {
		graph.remove(parent, child);
	}
	
	public void removeConstituent(BasicSubsystem subsystem, Resource constituent) {
		synchronized (subsystemToConstituents) {
			Set<Resource> constituents = subsystemToConstituents.get(subsystem);
			if (constituents != null) {
				constituents.remove(constituent);
				if (constituents.isEmpty())
					subsystemToConstituents.remove(subsystem);
			}
		}
		subsystem.removedContent(constituent);
	}
	
	public void removeReference(BasicSubsystem subsystem, Resource resource) {
		resourceReferences.removeReference(subsystem, resource);
	}
	
	public void removeSubsystem(BasicSubsystem subsystem) {
		synchronized (idToSubsystem) {
			synchronized (locationToSubsystem) {
				removeLocationToSubsystem(subsystem);
				removeIdToSubsystem(subsystem);
			}
		}
	}
	
	private void addIdToSubsystem(BasicSubsystem subsystem) {
		long id = subsystem.getSubsystemId();
		idToSubsystem.put(id, subsystem);
	}
	
	private void addLocationToSubsystem(BasicSubsystem subsystem) {
		String location = subsystem.getLocation();
		locationToSubsystem.put(location, subsystem);
	}
	
	private void removeIdToSubsystem(BasicSubsystem subsystem) {
		long id = subsystem.getSubsystemId();
		idToSubsystem.remove(id);
	}
	
	private void removeLocationToSubsystem(BasicSubsystem subsystem) {
		String location = subsystem.getLocation();
		locationToSubsystem.remove(location);
	}
}
