/*
 * ====================================================================
 * 
 * The ObjectStyle Group Software License, Version 1.0
 * 
 * Copyright (c) 2006 The ObjectStyle Group and individual authors of the
 * software. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The end-user documentation included with the redistribution, if any, must
 * include the following acknowlegement: "This product includes software
 * developed by the ObjectStyle Group (http://objectstyle.org/)." Alternately,
 * this acknowlegement may appear in the software itself, if and wherever such
 * third-party acknowlegements normally appear.
 * 
 * 4. The names "ObjectStyle Group" and "Cayenne" must not be used to endorse or
 * promote products derived from this software without prior written permission.
 * For written permission, please contact andrus@objectstyle.org.
 * 
 * 5. Products derived from this software may not be called "ObjectStyle" nor
 * may "ObjectStyle" appear in their names without prior written permission of
 * the ObjectStyle Group.
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * OBJECTSTYLE GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 * 
 * This software consists of voluntary contributions made by many individuals on
 * behalf of the ObjectStyle Group. For more information on the ObjectStyle
 * Group, please see <http://objectstyle.org/>.
 *  
 */
package org.objectstyle.wolips.eomodeler.core.model;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.objectstyle.woenvironment.plist.PropertyListParserException;
import org.objectstyle.woenvironment.plist.WOLPropertyListSerialization;
import org.objectstyle.wolips.baseforplugins.util.URLUtils;
import org.objectstyle.wolips.eomodeler.core.Activator;

public class EOModelGroup extends EOModelObject<Object> {
	public static final String MODELS = "models";

	private Set<EOModel> _models;

	private String _editingModelName;

	private boolean _dirty;

	public EOModelGroup() {
		_models = new HashSet<EOModel>();
	}

	public boolean hasProjectWonder() {
		return containsModelNamed("erprototypes");
	}

	public Set<EOModelReferenceFailure> getReferenceFailures() {
		return new HashSet<EOModelReferenceFailure>();
	}

	public void setDirty(boolean dirty) {
		_dirty = dirty;
	}

	public boolean isDirty() {
		boolean dirty = false;
		for (EOModel model : getModels()) {
			try {
				if (model.isDirty() && model.canSave()) {
					dirty = true;
					break;
				}
			} catch (MalformedURLException e) {
				Activator.getDefault().log("Unable to determine if the model " + model.getName() + " was dirty, so we skipped it.", e);
			}
		}
		return dirty;
	}

	@SuppressWarnings("unused")
	protected void _modelChanged(EOModel model, String propertyName, Object oldValue, Object newValue) {
		if (EOModel.DIRTY.equals(propertyName)) {
			boolean oldDirty = _dirty;
			boolean dirty = isDirty();
			firePropertyChange(propertyName, Boolean.valueOf(oldDirty), Boolean.valueOf(dirty));
			_dirty = dirty;
		}
	}

	@Override
	protected void _propertyChanged(String name, Object value, Object value2) {
		// DO NOTHING
	}

	public Set<EOModel> getModels() {
		return _models;
	}

	public Set<EOModel> getSortedModels() {
		return new PropertyListSet<EOModel>(_models);
	}

	/**
	 * Returns the list of names of entities that are not prototypes.
	 * 
	 * @return list of entity names
	 */
	public Set<String> getNonPrototypeEntityNames() {
		Set<String> entityNames = new TreeSet<String>();
		for (EOModel model : _models) {
			for (EOEntity entity : model.getEntities()) {
				if (!entity.isPrototype()) {
					entityNames.add(entity.getName());
				}
			}
		}
		return entityNames;
	}

	public Set<String> getEntityNames() {
		Set<String> entityNames = new TreeSet<String>();
		for (EOModel model : _models) {
			for (EOEntity entity : model.getEntities()) {
				entityNames.add(entity.getName());
			}
		}
		return entityNames;
	}

	/**
	 * Returns the list of entities that are not prototypes.
	 * 
	 * @return list of non prototype entities
	 */
	public Set<EOEntity> getNonPrototypeEntities() {
		Set<EOEntity> entities = new HashSet<EOEntity>();
		for (EOModel model : _models) {
			entities.addAll(model.getNonPrototypeEntities());
		}
		return entities;
	}

	public Set<EOEntity> getEntities() {
		Set<EOEntity> allEntities = new HashSet<EOEntity>();
		for (EOModel model : _models) {
			allEntities.addAll(model.getEntities());
		}
		return allEntities;
	}

	public Set<EOEntity> getSortedEntities() {
		return new PropertyListSet<EOEntity>(getEntities());
	}

	public String findUnusedEntityName(String _newName) {
		return _findUnusedName(_newName, "getEntityNamed");
	}

	public EOEntity getEntityNamed(String _entityName) {
		EOEntity matchingEntity = null;
		Iterator modelsIter = _models.iterator();
		while (matchingEntity == null && modelsIter.hasNext()) {
			EOModel model = (EOModel) modelsIter.next();
			matchingEntity = model.getEntityNamed(_entityName);
		}
		return matchingEntity;
	}

	public boolean containsModelNamed(String _entityName) {
		return getModelNamed(_entityName) != null;
	}

	public void _checkForDuplicateModelName(EOModel _model, String _newName) throws DuplicateModelNameException {
		EOModel model = getModelNamed(_newName);
		if (model != null && model != _model) {
			throw new DuplicateModelNameException(_newName, this);
		}
	}

	public void addModel(EOModel _model) throws DuplicateEntityNameException, DuplicateModelNameException {
		addModel(_model, null);
	}

	public void addModel(EOModel _model, Set<EOModelVerificationFailure> _failures) throws DuplicateEntityNameException, DuplicateModelNameException {
		Set<EOModel> oldModels = new HashSet<EOModel>(_models);
		_model._setModelGroup(this);
		_checkForDuplicateModelName(_model, _model.getName());
		for (EOEntity entity : _model.getEntities()) {
			_model._checkForDuplicateEntityName(entity, entity.getName(), _failures);
		}
		_models.add(_model);
		clearCachedPrototypes(_failures);
		firePropertyChange(EOModelGroup.MODELS, oldModels, _models);
	}

	public void removeModel(EOModel _model, Set<EOModelVerificationFailure> _failures) {
		Set<EOModel> oldModels = new HashSet<EOModel>(_models);
		_models.remove(_model);
		clearCachedPrototypes(_failures);
		firePropertyChange(EOModelGroup.MODELS, oldModels, _models);
		_model._setModelGroup(null);
	}

	public Set<EOEntity> getPrototypeEntities() {
		Set<EOEntity> prototypeEntities = new HashSet<EOEntity>();
		for (EOModel model : _models) {
			for (EOEntity entity : model.getEntities()) {
				if (entity.isPrototype()) {
					prototypeEntities.add(entity);
				}
			}
		}
		return prototypeEntities;
	}

	protected void clearCachedPrototypes(Set<EOModelVerificationFailure> _failures) {
		for (EOModel model : _models) {
			model.clearCachedPrototypes(_failures, false);
		}
	}

	public Map<EOAttribute, Set<EORelationship>> _createReferencingRelationshipsCache() {
		Map<EOAttribute, Set<EORelationship>> cache = new HashMap<EOAttribute, Set<EORelationship>>();
		for (EOModel model : getModels()) {
			model._createReferencingRelationshipsCache(cache);
		}
		return cache;
	}

	public Map<EOEntity, Set<EOEntity>> _createInheritanceCache() {
		Map<EOEntity, Set<EOEntity>> cache = new HashMap<EOEntity, Set<EOEntity>>();
		for (EOModel model : getModels()) {
			model._createInheritanceCache(cache);
		}
		return cache;
	}

	public EOModel getModelNamed(String _name) {
		EOModel matchingModel = null;
		Iterator<EOModel> modelsIter = _models.iterator();
		while (matchingModel == null && modelsIter.hasNext()) {
			EOModel model = modelsIter.next();
			if (model.getName().equals(_name)) {
				matchingModel = model;
			}
		}
		return matchingModel;
	}

	/**
	 * Sets the URL of the model that is being editing (considered the primary
	 * model).
	 * 
	 * @param editingModelURL
	 *            the url of the model that is in edit mode
	 */
	public void setEditingModelURL(URL editingModelURL) {
		setEditingModelName(EOModelGroup.getModelNameForURL(editingModelURL));
	}

	/**
	 * Sets the name of the model that is being editing (considered the primary
	 * model).
	 * 
	 * @param editingModelName
	 *            the name of the model that is in edit mode
	 */
	public void setEditingModelName(String editingModelName) {
		_editingModelName = editingModelName;
	}

	/**
	 * Returns the name of the model that is being editing (considered the
	 * primary model).
	 * 
	 * @return the name of the model that is in edit mode
	 */
	public String getEditingModelName() {
		return _editingModelName;
	}

	/**
	 * Returns the model that is being editing (considered the primary model).
	 * 
	 * @return the model that is in edit mode
	 */
	public EOModel getEditingModel() {
		return getModelNamed(_editingModelName);
	}

	/**
	 * Returns the model name for the given URL.
	 * 
	 * @param url
	 *            the URL to lookup (could be index.eomodeld file, or .eomodeld
	 *            folder)
	 * @return the model name for the given URL
	 */
	public static String getModelNameForURL(URL url) {
		String modelName;
		if (url == null) {
			modelName = null;
		}
		else {
			String protocol = url.getProtocol();
			if ("jar".equals(protocol)) {
				try {
					JarURLConnection conn = (JarURLConnection) url.openConnection();
					JarEntry jarEntry = conn.getJarEntry();
					if (jarEntry != null) {
						modelName = EOModelGroup.getModelNameForJarEntry(jarEntry);
					} else {
						modelName = null;
					}
				} catch (IOException ioe) {
					throw new IllegalStateException("The jar url '" + url.toString() + "' was unable to be used to find out the EO model's name.", ioe);
				}
			} else {
				File file = URLUtils.cheatAndTurnIntoFile(url);
				modelName = EOModelGroup.getModelNameForFile(file);
			}
		}
		return modelName;
	}

	/**
	 * Returns the model name for the given jar entry. This would be used in the
	 * situation where a Jar file is being introspected for the models within
	 * it.
	 * 
	 * @param jarEntry
	 *            the jar entry to lookup (could be index.eomodeld file, or
	 *            .eomodeld folder)
	 * @return the model name for the given jar entry
	 */
	public static String getModelNameForJarEntry(JarEntry jarEntry) {
		String modelName = null;

		if (jarEntry != null) {
			String jarEntryName = jarEntry.getName();

			if (jarEntryName.endsWith("index.eomodeld")) {
				int lastSlashIndex = jarEntryName.lastIndexOf('/');

				if (lastSlashIndex == -1) {
					jarEntryName = null;
				} else {
					jarEntryName = jarEntryName.substring(0, lastSlashIndex + 1);
				}
			}

			if (jarEntryName != null) {
				if (jarEntryName.endsWith(".eomodeld/")) {
					int lastDotIndex = jarEntryName.lastIndexOf('.', jarEntryName.length() - 2);
					int lastSlashIndex = jarEntryName.lastIndexOf('/', jarEntryName.length() - 2);

					modelName = jarEntryName.substring(lastSlashIndex + 1, lastDotIndex);

					if (modelName.length() == 0) {
						modelName = null;
					}
				}
			}
		}

		return modelName;
	}

	/**
	 * Returns the model name for the given file.
	 * 
	 * @param url
	 *            the file to lookup (could be index.eomodeld file, or .eomodeld
	 *            folder)
	 * @return the model name for the given file
	 */
	public static String getModelNameForFile(File file) {
		String modelName = null;
		if (file != null) {
			String fileName = file.getName();
			if (file.getName().equals("index.eomodeld")) {
				fileName = file.getParentFile().getName();
			}
			if (fileName.endsWith(".eomodeld")) {
				modelName = fileName.substring(0, fileName.lastIndexOf('.'));
			}
		}
		return modelName;
	}

	public EOModel loadModelFromURL(URL url) throws IOException, EOModelException {
		Set<EOModelVerificationFailure> failures = new HashSet<EOModelVerificationFailure>();
		EOModel model = loadModelFromURL(url, failures, true, new NullProgressMonitor());
		if (failures.size() > 0) {
			throw new EOModelException("Failed to load model from URL '" + url + "': " + failures);
		}
		return model;
	}

	public EOModel loadModelFromURL(URL url, Set<EOModelVerificationFailure> failures) throws IOException, EOModelException {
		EOModel model = loadModelFromURL(url, failures, true, new NullProgressMonitor());
		return model;
	}

	public EOModel loadModelFromURL(URL url, Set<EOModelVerificationFailure> failures, IProgressMonitor progressMonitor) throws IOException, EOModelException {
		EOModel model = loadModelFromURL(url, failures, true, progressMonitor);
		return model;
	}

	public void loadModelsFromURL(URL url, Set<EOModelVerificationFailure> failures, IProgressMonitor progressMonitor) throws IOException, EOModelException {
		loadModelsFromURL(url, -1, failures, true, progressMonitor);
	}

	protected void loadModelsFromURL(URL url, Set<EOModelVerificationFailure> failures, boolean skipOnDuplicates, IProgressMonitor progressMonitor) throws IOException, EOModelException {
		loadModelsFromURL(url, -1, failures, skipOnDuplicates, progressMonitor);
	}

	public void loadModelsFromURL(URL url, int maxDepth, Set<EOModelVerificationFailure> failures, boolean skipOnDuplicates, IProgressMonitor progressMonitor) throws IOException, EOModelException {
		String path = url.getPath();
		if (path.endsWith(".eomodeld") || path.endsWith(".eomodeld/")) {
			loadModelFromURL(url, failures, skipOnDuplicates, progressMonitor);
		} else if (maxDepth != 0) {
			for (URL childURL : URLUtils.getChildrenFolders(url)) {
				if (URLUtils.isFolder(childURL)) {
					boolean processFolder = true;
					
					// Skip over Resources/*.wo and Resources/Java ... just a performance optimization
					String childPath = childURL.getPath();
					if (childPath.endsWith("/Java/") || childPath.endsWith(".wo/")) {
						processFolder = false;
					}
					
					if (processFolder) {
						loadModelsFromURL(childURL, maxDepth - 1, failures, skipOnDuplicates, progressMonitor);
					}
				}
			}
		}
	}

	public EOModel loadModelFromURL(URL modelURL, Set<EOModelVerificationFailure> failures, boolean skipOnDuplicates, IProgressMonitor progressMonitor) throws IOException, EOModelException {
		String modelName = EOModelGroup.getModelNameForURL(modelURL);
		progressMonitor.setTaskName("Loading " + modelName + " ...");
		EOModel model = getModelNamed(modelName);
		if (model != null) {
			if (skipOnDuplicates) {
				// _failures.add(new EOModelVerificationFailure(model, "The
				// model named '" + modelName + "' exists in " +
				// model.getIndexURL() + " and " + _folder + ". Skipping " +
				// _folder + ".", true));
			} else {
				failures.add(new EOModelVerificationFailure(model, model, "The model named '" + modelName + "' exists in " + model.getIndexURL() + " and " + modelURL + ".", true));
			}
		}
		if (!skipOnDuplicates || model == null) {
			boolean reloadModel = true;
			while (reloadModel) {
				model = new EOModel(modelName);
				model.setModelURL(modelURL);
				URL indexURL = model.getIndexURL();
				if (!URLUtils.exists(indexURL)) {
					reloadModel = false;
					failures.add(new EOModelVerificationFailure(model, model, "Skipping model because " + indexURL + " does not exist.", true));
				} else {
					model._setModelGroup(this);
					try {
						model.loadFromURL(modelURL, failures);
						addModel(model, failures);
						reloadModel = false;
					} catch (DuplicateEntityNameException e) {
						e.printStackTrace();
						if (!skipOnDuplicates) {
							throw e;
						}
						EOEntity existingEntity = e.getExistingEntity();
						EOModel existingEntityModel = existingEntity.getModel();
						failures.add(new EOModelVerificationFailure(model, model, existingEntityModel.getName() + " and " + model.getName() + " both declare an entity named " + existingEntity.getName() + ", so " + existingEntityModel.getName() + " is being removed. You can create an EOModelGroup file to resolve this.", true, e));
						removeModel(existingEntityModel, failures);
					} catch (Exception e) {
						e.printStackTrace();
						failures.add(new EOModelVerificationFailure(model, model, model.getName() + " failed to load.", true, e));
						reloadModel = false;
					}
				}
			}
		}
		return model;
	}

	public void verify(Set<EOModelVerificationFailure> _failures) {
		VerificationContext verificationContext = new VerificationContext(this);
		for (EOModel model : _models) {
			model.verify(_failures, verificationContext);
		}
	}

	public void resolve(Set<EOModelVerificationFailure> _failures) {
		for (EOModel model : _models) {
			model.resolve(_failures);
		}
		for (EOModel model : _models) {
			model.resolveFlattened(_failures);
		}
	}

	public String getFullyQualifiedName() {
		return "EOModelGroup";
	}

	public String toString() {
		return "[EOModelGroup: models = " + _models + "]";
	}

	@Override
	public String getName() {
		return "EOModelGroup";
	}

	@Override
	public EOModelGroup _cloneModelObject() {
		throw new IllegalStateException("Not implemented");
	}

	@Override
	public void _addToModelParent(Object modelParent, boolean findUniqueName, Set<EOModelVerificationFailure> failures) {
		// DO NOTHING
	}

	@Override
	public Class<Object> _getModelParentType() {
		return null;
	}

	@Override
	public Object _getModelParent() {
		return null;
	}

	@Override
	public void _removeFromModelParent(Set<EOModelVerificationFailure> failures) {
		// DO NOTHING
	}

	public static void main(String[] args) throws IOException, PropertyListParserException {
		long a = System.currentTimeMillis();
		for (int i = 0; i < 1000; i++) {
			WOLPropertyListSerialization.propertyListFromFile(new File("/Users/mschrag/Documents/workspace/MDTask/Resources/MDTask.eomodeld/index.eomodeld"));
		}
		System.out.println("EOModelGroup.main: " + (System.currentTimeMillis() - a));
	}
}
