/**
 *    Copyright 2011 Peter Murray-Rust et. al.
 *
 *    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.xmlcml.cml.element;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import nu.xom.Element;
import nu.xom.Nodes;

import org.xmlcml.cml.base.CMLConstants;
import org.xmlcml.cml.base.CMLElement;
import org.xmlcml.cml.base.CMLElements;
import org.xmlcml.cml.base.CMLType;
import org.xmlcml.cml.interfacex.HasArraySize;
import org.xmlcml.cml.interfacex.HasDataType;
import org.xmlcml.cml.interfacex.HasDictRef;
import org.xmlcml.cml.interfacex.HasScalar;

/**
 * user-modifiable class supporting property. * autogenerated from schema use as
 * a shell which can be edited
 *
 */
public class CMLProperty extends AbstractProperty implements HasDictRef {
	
	/** basic properties (in dictionary) */
	public enum DictRef {
		/** */
        APPEARANCE("appearance"),
		/** */
        AUTO_IGNITION("autoIgnition"),
		/** */
        BPT("bpt"),
		/** */
        DENSITY("density"),
		/** */
        DIPOLE("dipole"),
		/** */
        FLASH_POINT("flashPoint"),
		/** */
        KH("kh"),
		/** */
        LOGP("logP"),
		/** */
        LOGPOW("logPow"),
		/** */
        MOLARMASS("molarMass"),
		/** */
        MPT("mpt"),
		/** */
        MWT("mwt"),
		/** */
        PKA("pka"),
		/** */
        PKB("pkb"),
		/** */
        REFRACTIVE_INDEX("refractiveIndex"),
		/** */
        RELATIVE_DENSITY("relativeDensity"),
		/** */
        RELATIVE_VAPOUR_DENSITY("relativeVapourDensity"),
		/** */
        SOLUBILITY("solubility"),
		/** */
        VAPOR_PRESSURE("vaporPressure"),
		/** */
        VISCOSITY("viscosity"),
		/** */
        WATER_SOLUBILITY("waterSolubility"),
        ;
        /** value */
        public String v;
        private Map<String, DictRef> map = new HashMap<String, DictRef>();
        private DictRef(String v) {
        	this.v = v;
        	map.put(v, this);
        }
        /** get DictRef for string
         * 
         * @param v
         * @return dictRef
         */
        public DictRef get(String v) {
        	return map.get(v);
        }
	}
	/** namespaced element name.*/
	public final static String NS = C_E+TAG;

	/** type of property */
	public enum Type {
		/** intensive properties do not depend on amoount*/
		INTENSIVE("intensive"),
		/** extensive properties depend on amount*/
		EXTENSIVE("extensive"),
		/** semintensive properties are intensive properties
		 * which also depend on polymer size*/
		SEMINTENSIVE("semintensive"),
		;
		/** value */
		public String value;
		private Type(String t) {
			this.value = t;
		}
	}
	
	/** common properties */
	public enum Prop {
		/** density*/
		DENSITY("cml:density"),
		/** molar mass*/
		MOLAR_MASS("cml:molarMass"),
		/** molar mass*/
		MOLAR_VOLUME("cml:molarVolume"),
		;
		/** value of dictRef */
		public final String value;
		private Prop(String s) {
			value = s;
		}
	}

	
	private HasDataType child;

    /**
     * constructor.
     */
    public CMLProperty() {
    }

    /**
     * constructor.
     *
     * @param old
     */
    public CMLProperty(CMLProperty old) {
        super((AbstractProperty) old);

    }

    /**
     * construct as property with child scalar
     * @param dictRef
     * @param value
     * @param units
     */
    public CMLProperty(String dictRef, double value, String units) { 
    	this();
    	CMLScalar scalar = new CMLScalar(value);
    	scalar.setUnits(units);
    	this.appendChild(scalar);
    	this.setDictRef(dictRef);
    }
    
    /**
     * copy node .
     *
     * @return Node
     */
    public Element copy() {
        return new CMLProperty(this);

    }

    /**
     * create new instance in context of parent, overridable by subclasses.
     *
     * @param parent
     *            parent of element to be constructed (ignored by default)
     * @return CMLProperty
     */
    public CMLElement makeElementInContext(Element parent) {
        return new CMLProperty();
    }
    
    /** gets descendant property elements.
     * may either thave the dictRef on the property element or on 
     * its child.
     * properties are normalized
     * @param parent
     * @param dictRef
     * @return propertyList containg references (normally 1 or 0 entries)
     */ 
    public static CMLPropertyList getPropertyList(CMLElement parent, String dictRef) {
    	CMLPropertyList propertyList = new CMLPropertyList();
    	Nodes nodes = parent.query("./cml:property", CMLConstants.CML_XPATH);
    	for (int i = 0; i < nodes.size(); i++ ) {
    		CMLProperty property = (CMLProperty) nodes.get(i);
    		property.canonicalize();
    		if (dictRef.equals(property.getAttributeValue("dictRef"))) {
	    		propertyList.addProperty(property);
    		}
    	}
    	return propertyList;
    }

    /** gets single property.
     * if zero or many properties with gievn dictRef returns null
     * @param parent
     * @param dictRef
     * @return property or null
     */
    public static CMLProperty getProperty(CMLElement parent, String dictRef) {
    	CMLPropertyList propertyList = CMLProperty.getPropertyList(parent, dictRef);
    	CMLElements<CMLProperty> properties = propertyList.getPropertyElements();
    	CMLProperty property = null;
    	if (properties.size() == 1) {
    		property = properties.get(0);
    	}
    	return property;
    }
    
    /** makes sure property has the structure:
     * {@code <property @title @dictRef><scalar @dataType @units>... }
     * if zero or many children (scalar, array, matrix) no-op
     *
     */
    public void canonicalize() {
    	CMLProperty.staticCanonicalize(this);
    }
    
    static void staticCanonicalize(CMLElement element) {
    	HasDataType child =  CMLProperty.getStaticChild(element);
    	if (child != null) {
    		String thisDictRef = ((HasDictRef)element).getDictRef();
    		String childDictRef = ((HasDictRef) child).getDictRef();
    		if (thisDictRef == null) {
    			if (childDictRef == null) {
    				throw new RuntimeException("No dictRef attribute given: ");
    			}
    			// copy to property
    			((HasDictRef)element).setDictRef(childDictRef);
    		} else {
    			if (childDictRef == null) {
    			} else if (thisDictRef.equals(childDictRef)) {
    				// OK
    			} else {
    				throw new RuntimeException("inconsistent dictRefs: "+thisDictRef+" // "+childDictRef);
    			}
    		}
    		String units = CMLProperty.getStaticUnits(element);
    		String dataType = CMLType.getNormalizedValue(child.getDataType());
    		if (units != null) {
    			if (!dataType.equals(XSD_DOUBLE)) {
    				((HasDataType) child).setDataType(XSD_DOUBLE);
    				throw new RuntimeException("units require data type of double");
    			}
    		} else {
    			if (dataType.equals(XSD_DOUBLE)) {
    				throw new RuntimeException("dataType not double");
    			}
    		}
    	}
    }

	/**
	 * @return units on child
	 */
	public String getUnits() {
		return CMLProperty.getStaticUnits(this);
	}
	
	/**
	 * gets real value of scalar child
	 * 
	 * @return the value (NaN if not set)
	 */
	public double getDouble() {
		return CMLProperty.getStaticDouble(this);
	}

	/**
	 * gets String value. dataType must be XSD_STRING.
	 * 
	 * @return the value (null if not set)
	 */
	public String getString() {
		return CMLProperty.getStaticString(this);
	}
	

	/**
	 * gets int value. dataType must be XSD_INTEGER.
	 * 
	 * @return the value
	 * @throws RuntimeException
	 *             if different type
	 */
	public int getInt() {
		return CMLProperty.getStaticInt(this);
	}

    /** get array elements.
     * recalculates each time so best cached for frequent use
     * @return elements as String
     */
    public List<String> getStringValues() {
		return CMLProperty.getStaticStringValues(this);
	}
    
    /**
     * gets values of element;
     * 
     * @return integer values
     */
    public int[] getInts() {
		return CMLProperty.getStaticInts(this);
	}

    /**
     * gets values of element;
     * 
     * @return double values
     */
    public double[] getDoubles() {
		return CMLProperty.getStaticDoubles(this);
	}
	
	/**
	 * requires exactly one child of type scalar array matrix
	 * @return the child
	 */
	public HasDataType getChild() {
		if (child == null) {
			child = getStaticChild(this);
		}
		return child;
	}
	
	/**
	 * @param child the child to set
	 */
	public void setChild(HasDataType child) {
		this.child = child;
	}


	/** gets dataType
	 * @return dataType as string
	 */
	public String getDataType() {
		return getStaticDataType(this);
	}
	
	// -------------------------- helpers ---------------------------
	static String getStaticUnits(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		String units = ((CMLElement) child).getAttributeValue("units");
		return units;
	}
    
	static double getStaticDouble(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		double result = Double.NaN;
		String dataType = CMLType.getNormalizedValue(getStaticDataType(child));
		if (XSD_DOUBLE.equals(dataType) && child instanceof HasScalar) {
			result = ((HasScalar) child).getDouble();
		}
		return result;
	}
	
	static String getStaticDataType(HasDataType hasDataType) {
		return hasDataType.getDataType();
	}

	
	static String getStaticString(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		String result = null;
		if (XSD_STRING.equals(child.getDataType()) &&
				(child instanceof HasScalar)
				) {
			result = ((HasScalar) child).getString();
		}
		return result;
	}
	
	static int getStaticInt(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		int result = Integer.MIN_VALUE;
		String dataType = CMLType.getNormalizedValue(child.getDataType());
		if (XSD_INTEGER.equals(dataType) && 
				(child instanceof HasScalar)) {
			result = ((HasScalar) child).getInt();
		}
		return result;
	}
	
	static List<String> getStaticStringValues(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		List<String> result = null;
		String dataType = CMLType.getNormalizedValue(child.getDataType());
		if (XSD_STRING.equals(dataType) && 
			child instanceof HasArraySize) {
			result = ((HasArraySize) child).getStringValues();
		}
		return result;
    }
	
	static int[] getStaticInts(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		int[] result = null;
		String dataType = CMLType.getNormalizedValue(child.getDataType());
		if (XSD_INTEGER.equals(dataType) && 
			child instanceof HasArraySize) {
			result = ((HasArraySize) child).getInts();
		}
		return result;
    }
	
	static double[] getStaticDoubles(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		double[] result = null;
		String dataType = CMLType.getNormalizedValue(child.getDataType());
		if (XSD_DOUBLE.equals(dataType) && 
			child instanceof HasArraySize) {
			result = ((HasArraySize) child).getDoubles();
		}
		return result;
    }

	
	static HasDataType getStaticChild(CMLElement element) {
		
		HasDataType dataType = null;
    	Nodes nodes = element.query("cml:scalar | cml:array | cml:matrix", CMLConstants.CML_XPATH);
    	if (nodes.size() == 1) {
    		dataType = (HasDataType) nodes.get(0);
    	}
		return dataType;
	}

	
	static String getStaticDataType(CMLElement element) {
		HasDataType child =  getStaticChild(element);
		String dataType = (child == null) ? null : ((HasDataType) child).getDataType();
		return CMLType.getNormalizedValue(dataType);
	}
	
}
