/*******************************************************************************
 * Copyright (c) 2003, 2019 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.common.frameworks.internal.datamodel;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.wst.common.frameworks.datamodel.AbstractDataModelOperation;
import org.eclipse.wst.common.frameworks.datamodel.DataModelEvent;
import org.eclipse.wst.common.frameworks.datamodel.DataModelPropertyDescriptor;
import org.eclipse.wst.common.frameworks.datamodel.IDataModel;
import org.eclipse.wst.common.frameworks.datamodel.IDataModelListener;
import org.eclipse.wst.common.frameworks.datamodel.IDataModelOperation;
import org.eclipse.wst.common.frameworks.datamodel.IDataModelProperties;
import org.eclipse.wst.common.frameworks.datamodel.IDataModelProvider;
import org.eclipse.wst.common.frameworks.internal.WTPResourceHandler;

public final class DataModelImpl implements IDataModel, IDataModelListener {

	private static final String PROPERTY_NOT_LOCATED_ = WTPResourceHandler.getString("20"); //$NON-NLS-1$
	private static final String NESTED_MODEL_NOT_LOCATED = WTPResourceHandler.getString("21"); //$NON-NLS-1$

	private static final DataModelPropertyDescriptor[] NO_DESCRIPTORS = new DataModelPropertyDescriptor[0];

	private Collection basePropertyNames;
	private Collection allPropertyNames;
	private Collection nestedPropertyNames; // lazily initialzed when nested models added
	private Map propertyValues = new Hashtable();
	private Map nestedModels;
	private Set nestingModels;
	private List listeners;

	private IDataModelProvider provider;

	public DataModelImpl(IDataModelProvider dataModelProvider) {
		init(dataModelProvider);
	}

	private void init(IDataModelProvider dataModelProvider) {
		this.provider = dataModelProvider;
		dataModelProvider.setDataModel(this);
		Collection propertyNames = dataModelProvider.getPropertyNames();
		HashSet properties = new HashSet();
		properties.addAll(propertyNames);

		properties.add(IDataModelProperties.ALLOW_EXTENSIONS);
		properties.add(IDataModelProperties.RESTRICT_EXTENSIONS);
		basePropertyNames = Collections.unmodifiableCollection(properties);
		allPropertyNames = new HashSet();
		allPropertyNames.addAll(basePropertyNames);
		dataModelProvider.init();
	}

	@Override
	public boolean isBaseProperty(String propertyName) {
		return basePropertyNames.contains(propertyName);
	}

	@Override
	public Collection getBaseProperties() {
		return Collections.unmodifiableCollection(basePropertyNames);
	}

	@Override
	public boolean isProperty(String propertyName) {
		return allPropertyNames.contains(propertyName);
	}

	@Override
	public Collection getAllProperties() {
		return Collections.unmodifiableCollection(allPropertyNames);
	}

	@Override
	public boolean isNestedProperty(String propertyName) {
		return null != nestedPropertyNames && nestedPropertyNames.contains(propertyName);
	}

	@Override
	public Collection getNestedProperties() {
		return Collections.unmodifiableCollection(nestedPropertyNames);
	}

	private void checkValidPropertyName(String propertyName) {
		if (!isProperty(propertyName)) {
			throw new RuntimeException(PROPERTY_NOT_LOCATED_ + propertyName);
		}
	}

	private DataModelImpl getOwningDataModel(String propertyName) {
		checkValidPropertyName(propertyName);
		return searchNestedModels(propertyName);
	}

	private DataModelImpl searchNestedModels(String propertyName) {
		if (isBaseProperty(propertyName)) {
			return this;
		} else if (nestedModels != null) {
			DataModelImpl dataModel = null;
			Object[] keys = nestedModels.keySet().toArray();
			for (int i = 0; i < keys.length; i++) {
				dataModel = (DataModelImpl) nestedModels.get(keys[i]);
				if (dataModel.isProperty(propertyName)) {
					return dataModel.searchNestedModels(propertyName);
				}
			}
		}
		throw new RuntimeException(PROPERTY_NOT_LOCATED_ + propertyName);
	}

	@Override
	public Object getProperty(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		if (dataModel.propertyValues.containsKey(propertyName)) {
			return dataModel.propertyValues.get(propertyName);
		}
		return dataModel.provider.getDefaultProperty(propertyName);
	}

	@Override
	public Object getDefaultProperty(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		return dataModel.provider.getDefaultProperty(propertyName);
	}

	@Override
	public int getIntProperty(String propertyName) {
		Object prop = getProperty(propertyName);
		if (prop == null)
			return -1;
		return ((Integer) prop).intValue();
	}

	@Override
	public boolean getBooleanProperty(String propertyName) {
		Object prop = getProperty(propertyName);
		if (prop == null)
			return false;
		return ((Boolean) prop).booleanValue();
	}

	@Override
	public String getStringProperty(String propertyName) {
		Object prop = getProperty(propertyName);
		if (prop == null)
			return ""; //$NON-NLS-1$
		return (String) prop;
	}

	@Override
	public boolean isPropertySet(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		return dataModel.propertyValues.containsKey(propertyName);
	}

	@Override
	public boolean isPropertyEnabled(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		return dataModel.provider.isPropertyEnabled(propertyName);
	}


	@Override
	public void setProperty(String propertyName, Object propertyValue) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		dataModel.internalSetProperty(propertyName, propertyValue);
	}

	private void internalSetProperty(String propertyName, Object propertyValue) {
		Object oldValue = propertyValues.get(propertyName);
		if (valueChanged(propertyValue, oldValue)) {
			if (null != propertyValue)
				propertyValues.put(propertyName, propertyValue);
			else if (propertyValues.containsKey(propertyName))
				propertyValues.remove(propertyName);
			if (provider.propertySet(propertyName, propertyValue)) {
				notifyPropertyChange(propertyName, DataModelEvent.VALUE_CHG);
			}
		}
	}

	private boolean valueChanged(Object o1, Object o2) {
		return o1 != o2 && ((o1 != null && !o1.equals(o2)) || !o2.equals(o1));
	}

	@Override
	public void setIntProperty(String propertyName, int value) {
		setProperty(propertyName, new Integer(value));
	}

	@Override
	public void setBooleanProperty(String propertyName, boolean value) {
		setProperty(propertyName, (value) ? Boolean.TRUE : Boolean.FALSE);
	}

	@Override
	public void setStringProperty(String propertyName, String value) {
		setProperty(propertyName, value);
	}

	@Override
	public boolean addNestedModel(String modelName, IDataModel dataModel) {
		if (this == dataModel) {
			return false;
		}
		if (null == nestedModels) {
			nestedModels = new Hashtable();
			nestedPropertyNames = new HashSet();
		}
		DataModelImpl nestedDataModel = (DataModelImpl) dataModel;
		if (null == nestedDataModel.nestingModels) {
			nestedDataModel.nestingModels = new HashSet();
		}
		if (nestedDataModel.nestingModels.contains(this)) {
			return false;
		}
		nestedDataModel.nestingModels.add(this);

		nestedModels.put(modelName, nestedDataModel);

		addNestedProperties(nestedDataModel.allPropertyNames);
		nestedDataModel.addListener(this);
		return true;
	}

	private void addNestedProperties(Collection nestedProperties) {
		boolean propertiesAdded = allPropertyNames.addAll(nestedProperties);
		propertiesAdded = nestedPropertyNames.addAll(nestedProperties) || propertiesAdded;
		// Pass the new properties up the nesting chain
		if (propertiesAdded && nestingModels != null) {
			Iterator iterator = nestingModels.iterator();
			while (iterator.hasNext()) {
				((DataModelImpl) iterator.next()).addNestedProperties(nestedProperties);
			}
		}
	}

	@Override
	public Collection getNestedModels() {
		return nestedModels != null ? Collections.unmodifiableCollection(nestedModels.values()) : Collections.EMPTY_SET;
	}

	@Override
	public Collection getNestedModelNames() {
		return nestedModels != null ? Collections.unmodifiableCollection(nestedModels.keySet()) : Collections.EMPTY_SET;
	}

	@Override
	public Collection getNestingModels() {
		return nestingModels != null ? Collections.unmodifiableCollection(nestingModels) : Collections.EMPTY_SET;
	}

	@Override
	public IDataModel removeNestedModel(String modelName) {
		if (!isNestedModel(modelName)) {
			return null;
		}
		DataModelImpl model = (DataModelImpl) nestedModels.remove(modelName);
		model.nestingModels.remove(this);
		removeNestedProperties(model.allPropertyNames);
		model.removeListener(this);
		if (nestedModels.isEmpty()) {
			nestedModels = null;
		}
		return model;
	}

	private void removeNestedProperties(Collection nestedProperties) {
		Iterator iterator = nestedProperties.iterator();
		String property = null;
		boolean keepProperty = false;
		Set nestedPropertiesToRemove = null;
		while (iterator.hasNext()) {
			keepProperty = false;
			property = (String) iterator.next();
			if (basePropertyNames.contains(property)) {
				keepProperty = true;
			}
			if (!keepProperty && nestedModels != null) {
				Iterator nestedModelsIterator = nestedModels.values().iterator();
				while (!keepProperty && nestedModelsIterator.hasNext()) {
					DataModelImpl nestedModel = (DataModelImpl) nestedModelsIterator.next();
					if (nestedModel.isProperty(property)) {
						keepProperty = true;
					}
				}
			}
			if (!keepProperty) {
				if (null == nestedPropertiesToRemove) {
					nestedPropertiesToRemove = new HashSet();
				}
				nestedPropertiesToRemove.add(property);
			}
		}

		if (null != nestedPropertiesToRemove) {
			allPropertyNames.removeAll(nestedPropertiesToRemove);
			nestedPropertyNames.removeAll(nestedPropertiesToRemove);
			if (nestingModels != null) {
				Iterator nestingModelsIterator = nestingModels.iterator();
				while (nestingModelsIterator.hasNext()) {
					((DataModelImpl) nestingModelsIterator.next()).removeNestedProperties(nestedPropertiesToRemove);
				}
			}
		}
	}

	@Override
	public boolean isNestedModel(String modelName) {
		return modelName != null && null != nestedModels && nestedModels.containsKey(modelName);
	}

	@Override
	public IDataModel getNestedModel(String modelName) {
		IDataModel dataModel = (null != nestedModels && null != modelName) ? (IDataModel) nestedModels.get(modelName) : null;
		if (null == dataModel) {
			throw new RuntimeException(NESTED_MODEL_NOT_LOCATED + modelName);
		}
		return dataModel;
	}

	@Override
	public DataModelPropertyDescriptor[] getValidPropertyDescriptors(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		DataModelPropertyDescriptor[] descriptors = dataModel.provider.getValidPropertyDescriptors(propertyName);
		return descriptors == null ? NO_DESCRIPTORS : descriptors;
	}

	@Override
	public DataModelPropertyDescriptor getPropertyDescriptor(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		DataModelPropertyDescriptor descriptor = dataModel.provider.getPropertyDescriptor(propertyName);
		return descriptor == null ? new DataModelPropertyDescriptor(getProperty(propertyName)) : descriptor;
	}

	@Override
	public void notifyPropertyChange(final String propertyName, final int flag) {
		int innerFlag = flag;
		if (innerFlag == DEFAULT_CHG) {
			if (isPropertySet(propertyName)) {
				return;
			}
			innerFlag = VALUE_CHG;
		}
		notifyListeners(new DataModelEvent(this, propertyName, innerFlag));
	}

	private void notifyListeners(DataModelEvent event) {
		if (listeners != null && !listeners.isEmpty()) {
			IDataModelListener listener;
			for (int i = 0; i < listeners.size(); i++) {
				listener = (IDataModelListener) listeners.get(i);
				if (listener != event.getDataModel()) {
					listener.propertyChanged(event);
				}
			}
		}
	}

	@Override
	public void propertyChanged(DataModelEvent event) {
		notifyListeners(event);
	}

	@Override
	public IStatus validate() {
		return validate(true);
	}

	@Override
	public IStatus validate(boolean stopOnFirstFailure) {
		IStatus status = null;
		IStatus propStatus;
		String propName;
		Iterator it;
		for (int i = 0; i < 2; i++) {
			switch (i) {
				case 0 :
					it = basePropertyNames.iterator();
					break;
				case 1 :
				default :
					it = getNestedModelNames().iterator();
			}
			while (it.hasNext()) {
				propName = (String) it.next();
				propStatus = provider.validate(propName);
				if (propStatus != null) {
					if (status == null || status.isOK()) {
						status = propStatus;
					} else {
						if (status.isMultiStatus()) {
							((MultiStatus) status).merge(propStatus);
						} else {
							MultiStatus multi = new MultiStatus("org.eclipse.wst.common.frameworks.internal", 0, "", null); //$NON-NLS-1$ //$NON-NLS-2$
							multi.merge(status);
							multi.merge(propStatus);
							status = multi;
						}
					}
					if (stopOnFirstFailure && !status.isOK() && status.getSeverity() == IStatus.ERROR)
						return status;
				}
			}
		}

		if (status == null)
			return IDataModelProvider.OK_STATUS;
		return status;
	}

	@Override
	public void addListener(IDataModelListener listener) {
		if (listener != null) {
			if (listeners == null) {
				listeners = new ArrayList();
				listeners.add(listener);
			} else if (!listeners.contains(listener))
				listeners.add(listener);
		}
	}

	@Override
	public void removeListener(IDataModelListener listener) {
		if (listeners != null && listener != null)
			listeners.remove(listener);
	}

	/**
	 * Return true if the model doesn't have any errors.
	 * 
	 * @return boolean
	 */
	@Override
	public boolean isValid() {
		return validate(true).getSeverity() != IStatus.ERROR;
	}

	@Override
	public boolean isPropertyValid(String propertyName) {
		return validateProperty(propertyName).getSeverity() != IStatus.ERROR;
	}

	@Override
	public IStatus validateProperty(String propertyName) {
		DataModelImpl dataModel = getOwningDataModel(propertyName);
		IStatus status = dataModel.provider.validate(propertyName);
		return status == null ? IDataModelProvider.OK_STATUS : status;
	}

	@Override
	public List getExtendedContext() {
		List extendedContext = provider.getExtendedContext();
		return extendedContext == null ? Collections.EMPTY_LIST : extendedContext;
	}

	@Override
	public void dispose() {
		provider.dispose();
	}

	public IDataModelOperation getRawOperation() {
		IDataModelOperation providerOp = provider.getDefaultOperation();
		if (null == providerOp) {
			providerOp = new AbstractDataModelOperation(this) {
				@Override
				public IStatus execute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
					return OK_STATUS;
				}
			};
		}
		return providerOp;
	}

	@Override
	public IDataModelOperation getDefaultOperation() {
		return new DataModelPausibleOperationImpl(getRawOperation());
	}

	@Override
	public String toString() {
		return "IDataModel, provider=" + provider.toString(); //$NON-NLS-1$
	}

	@Override
	public String getID() {
		String id = provider.getID();
		return null != id ? id : ""; //$NON-NLS-1$
	}
}
