/* *******************************************************************************************************
Copyright (c) 2015 EXILANT Technologies Private Limited

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 ******************************************************************************************************** */
package com.exilant.exility.core;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/***
 * Exility Design component that models all data elements used in this
 * application
 * 
 */
public class DataDictionary {

	/***
	 * designers want to live with problems!!! if a field's data type is not
	 * known we assumed a default is assumed
	 */
	public static final String DEFAULT_ELEMENT_NAME = "text";

	private static final String DEFAULT_GROUP_NAME = "autoGenerated";

	/***
	 * Use an internal instance to keep it flexible for future
	 */
	private static DataDictionary instance = new DataDictionary();

	/***
	 * data elements belong to groups. Concept of group is not used anymore, but
	 * remains due to historic reason
	 */
	private Map<String, DataGroup> groups = new HashMap<String, DataGroup>();

	/**
	 * data elements (loaded from all groups in all files)
	 */
	private Map<String, DataElement> elements = new HashMap<String, DataElement>();

	private Set<String> refernces = null;

	private boolean inError = false;
	private List<FieldReference> pendingReferences = new ArrayList<FieldReference>();

	/**
	 * loads data elements from file system into memory. Called by start-up
	 * servlet if this application is deployed in a web-container.
	 * 
	 * @param removeExistingElements
	 *            existing entries are removed. Used in reloading for a
	 *            different project. Called with false in case data elements are
	 *            to be loaded after loading built-in definitions of Exility
	 */
	static void reload(boolean removeExistingElements) {
		if (DataDictionary.instance == null || removeExistingElements) {
			DataDictionary.instance = new DataDictionary();
		}
		DataDictionary.load();
	}

	/**
	 * use the utility ResourceManager to load data dictionary files
	 */
	static synchronized void load() {
		try {
			Map<String, Object> dictionaries = ResourceManager
					.loadFromFileOrFolder("dataDictionary", "dictionary",
							".xml");
			/*
			 * get ready to accumulate referred elements
			 */
			instance.refernces = new HashSet<String>();
			for (String fileName : dictionaries.keySet()) {
				Object obj = dictionaries.get(fileName);
				if (obj instanceof DataDictionary == false) {
					Spit.out("dataDictionary folder contains an xml that is not a dictionary. File ignored.");
					continue;
				}
				DataDictionary d = (DataDictionary) obj;
				DataDictionary.instance.copyFrom(d);
			}

			/*
			 * flash error for any reference errors
			 */
			for (String aName : instance.refernces) {
				if (instance.elements.containsKey(aName) == false) {
					Spit.out("data element " + aName
							+ " is referenced but not defined.");
				}
			}

			/*
			 * and, let us not keep garbage with us..
			 */
			instance.refernces = null;

			Spit.out("**Final dictionary has "
					+ DataDictionary.instance.groups.size() + " groups and "
					+ DataDictionary.instance.elements.size() + " elements. ");
		} catch (Exception e) {
			Spit.out("Unable to load data dictionary with root folder at "
					+ ResourceManager.getResourceFolder() + " : "
					+ e.getMessage());
			Spit.out(e);
		}
	}

	/***
	 * get data element with this (possibly fully qualified) name
	 * 
	 * @param name
	 * @return data element if found. Null otherwise.
	 */
	public static DataElement getElement(String name) {
		return DataDictionary.instance.elements.get(name);
	}

	/***
	 * get the label
	 * 
	 * @param name
	 * @return label. empty string if data element is does not exist
	 */
	public static String getElementLabel(String name) {
		DataElement de = DataDictionary.getElement(name);
		if (de == null) {
			return "";
		}

		return de.label;
	}

	/***
	 * get data type of this data element. return a default if it is not found
	 * 
	 * @param name
	 * @return data type of the element. if element is not found, a default text
	 *         data type is returned
	 */
	public static AbstractDataType getDataType(String name) {
		AbstractDataType dt = getDataTypeOrNull(name);
		if (dt == null) {
			return new TextDataType();
		}
		return dt;
	}

	/***
	 * get data type of this data element
	 * 
	 * @param name
	 * @return data type of the element. null if not found
	 */
	public static AbstractDataType getDataTypeOrNull(String name) {
		AbstractDataType dt = null;
		DataElement de = DataDictionary.getElement(name);
		if (de != null) {
			dt = DataTypes.getDataType(de.dataType, null);
			if (dt != null) {
				return dt;
			}
		}
		return null;
	}

	/***
	 * get the data value type of the data element
	 * 
	 * @param name
	 * @return underlying value type of the data element. text if data element
	 *         is non-existent
	 */
	public static DataValueType getValueType(String name) {
		AbstractDataType dt = getDataTypeOrNull(name);
		if (dt != null) {
			return dt.getValueType();
		}
		return DataValueType.TEXT;
	}

	/**
	 * getValueType() would return text in case this element is not found. This
	 * method would return in that case.
	 * 
	 * @param name
	 *            element name
	 * @return value type of this element if found. null otherwise.
	 */
	public static DataValueType getValueTypeOrNull(String name) {
		AbstractDataType dt = null;
		DataElement de = DataDictionary.getElement(name);
		if (de != null) {
			dt = DataTypes.getDataType(de.dataType, null);
			if (dt != null) {
				return dt.getValueType();
			}
		}
		return null;
	}

	/***
	 * get a default data element. Used to create data elements for missing ones
	 * while operating in accommodative mode
	 * 
	 * @param name
	 * @return
	 */
	static DataElement getDefaultElement(String name) {
		DataElement de = new DataElement();
		de.dataType = "text";
		de.label = name;
		de.name = name;
		return de;
	}

	/***
	 * get a data group
	 * 
	 * @param groupName
	 * @return group object with this name
	 */
	public static DataGroup getGroup(String groupName) {
		return DataDictionary.getInstance().groups.get(groupName);
	}

	/***
	 * get the instance to work on
	 * 
	 * @return
	 */
	static DataDictionary getInstance() {
		return DataDictionary.instance;
	}

	/***
	 * not sure which project uses this.
	 * 
	 * @param writer
	 */
	static public void writeAll(OutputStream writer) {
		try {
			String tab = "\t";
			writer.write(new String(
					"groupName\tname\tqualifiedName\tbusinessDescription\ttechnicalDescription\tisInternalElement\tdataType\tlistServiceName\t\n")
					.getBytes());
			for (DataGroup group : DataDictionary.instance.groups.values()) {
				for (DataElement element : group.elements.values()) {
					String s = group.name + tab + element.name + tab
							+ group.name + AP.dataElementSeparator
							+ element.name + tab + element.businessDescription
							+ tab + element.technicalDescription + tab
							+ ((element.isInternalElement) ? "1" : "0") + tab
							+ element.dataType + tab + element.listServiceName
							+ '\n';
					writer.write(s.getBytes());
				}
			}
		} catch (Exception ex) {
			// hardly anything to be done here
		}
	}

	/***
	 * copy from one dictionary to the other
	 * 
	 * @param d
	 */
	private void copyFrom(DataDictionary d) {
		for (DataGroup group : d.groups.values()) {
			String groupName = group.name;
			this.groups.put(groupName, group);
			for (DataElement element : group.elements.values()) {
				String elementName = element.name;

				/*
				 * is this put here just for documentation for the group, and
				 * actually definition is in another group, like parent key
				 */
				if (element.referenceGroup != null) {
					this.refernces.add(element.name);
					continue;
				}

				DataElement el = this.elements.get(elementName);
				if (el != null && el.dataType != null
						&& el.dataType.equals(element.dataType) == false) {
					/*
					 * same name is used in more than one groups, and their data
					 * type name is different. That is OK, so long as the value
					 * type is same
					 */
					AbstractDataType d1 = DataTypes.getDataType(
							element.dataType, null);
					AbstractDataType d2 = DataTypes.getDataType(el.dataType,
							null);
					if (d1 != null && d2 != null
							&& d1.getValueType() != d2.getValueType()) {
						Spit.out("ERROR: "
								+ elementName
								+ " is defined in group "
								+ groupName
								+ " with dataType = "
								+ element.dataType
								+ " whose underlying value is "
								+ d1.getValueType()
								+ ". However, another group has defined it as "
								+ el.dataType
								+ " wich is "
								+ d2.getValueType()
								+ ". This is likley to result in some data error in your project.");
					}
				}
				/*
				 * add the element with no group name anyways for the sake of
				 * grid columns
				 */
				this.elements.put(elementName, element);

				/*
				 * put qualified name if required
				 */
				if (AP.uniqueNamesAcrossGroups == false) {
					elementName = groupName + AP.dataElementSeparator
							+ elementName;
					this.elements.put(elementName, element);
				}
			}
		}
	}

	/**
	 * gets all labels that are used by data elements. Used for preparing list
	 * of translation for multi-lingual facility.
	 * 
	 * @return labels used by all data elements. First row is header row. First
	 *         column is label, and second column is description
	 */
	public static String[][] getAllLabels() {
		Map<String, String> labels = new HashMap<String, String>();
		for (DataElement de : instance.elements.values()) {
			String thisDescription = "";
			if (de.businessDescription != null) {
				thisDescription += de.businessDescription;
			}
			if (de.technicalDescription != null) {
				thisDescription += de.technicalDescription;
			}
			String desc = labels.get(de.label);
			if (desc == null) {
				thisDescription += desc;
			}
			labels.put(de.label, thisDescription);
		}
		int nbrLabels = labels.size();
		String[] arr = new String[nbrLabels];
		labels.keySet().toArray(arr);
		/**
		 * we thought about sorting. But then, probably the order in which these
		 * labels come in may have bearing on their logical grouping. Let us
		 * retain the order as of now
		 */
		String[][] allLabels = new String[nbrLabels + 1][2];
		allLabels[0][0] = Dubhashi.DEFAULT_LANGUAGE;
		allLabels[0][1] = Dubhashi.TRANSLATION_DESCRIPTION;
		for (int i = 1; i < allLabels.length; i++) {
			String label = arr[i - 1];
			allLabels[i][0] = label;
			allLabels[i][1] = labels.get(label);
		}
		return allLabels;
	}

	/**
	 * Get all defined data elements
	 * 
	 * @return sorted array of data element names
	 * 
	 */
	public static String[] getAllElements() {

		DataDictionary dictionary = getInstance();
		String[] names = dictionary.elements.keySet().toArray(new String[0]);
		Arrays.sort(names);
		return names;
	}

	/**
	 * Create an instance of data dictionary with a default group. This is
	 * created for auto-generation of data elements as of now.
	 * 
	 * @param groupName
	 * @return an instance of dictionary
	 */
	public static DataDictionary getADictionary(String groupName) {
		String groupNameToUse = groupName == null ? DEFAULT_GROUP_NAME
				: groupName;
		DataGroup group = new DataGroup();
		group.name = groupNameToUse;
		DataDictionary dictionary = new DataDictionary();
		dictionary.groups.put(groupNameToUse, group);
		return dictionary;
	}

	/**
	 * This is used by project initializer to load data dictionary from records.
	 * Records concepts replaces dataGroup based approach.
	 */
	public static synchronized void loadFromRecords() {

		DataDictionary dictionary = new DataDictionary();
		/*
		 * get all records that are defined
		 */
		String[] recordNames = ResourceManager.getResourceList("record",
				".xlsx");
		Spit.out("We will work with " + recordNames.length + " records");

		for (String recordName : recordNames) {
			Record record = Records.getRecord(recordName);
			if (record == null) {
				Spit.out(recordName
						+ " has some errors. Not able to process it.");
			} else {

				record.addToDictionary(dictionary);
			}
		}

		dictionary.resolveReferences();
		dictionary.pendingReferences = null;
		if (dictionary.inError) {
			Spit.out("Error encountered during extraction of data element from records. Dictionary NOT created");
		} else {
			DataDictionary.instance = dictionary;
			Spit.out("Dictionary created with "
					+ DataDictionary.instance.elements.size()
					+ " data elements.");
		}
	}

	/**
	 * add a data element to dictionary. This must be called as part of loading
	 * process
	 * 
	 * @param elementName
	 * 
	 * @param dataElement
	 * 
	 */
	public void addDataElement(String elementName, DataElement dataElement) {
		DataElement existingElement = this.elements.get(elementName);

		if (existingElement == null) {
			this.elements.put(elementName, dataElement);
			return;
		}

		if (existingElement.isCompatibleWith(dataElement)) {
			return;
		}
		Spit.out("Design Error: "
				+ elementName
				+ " is a field/data-element that has inconsistent data types across tables/records.");
		this.inError = true;
	}

	/**
	 * add a data element that refers to another element
	 * 
	 * @param elementName
	 * @param referredName
	 * @param recordName
	 */
	public void addReference(String elementName, String referredName,
			String recordName) {
		DataElement referredElement = this.elements.get(referredName);

		if (referredElement == null) {
			this.pendingReferences.add(new FieldReference(elementName,
					referredName, recordName));
			return;
		}
		this.addDataElement(elementName, referredElement);
	}

	/**
	 * re-try all references after all records are scanned
	 * 
	 * @param dictionary
	 * @param references
	 * @param incompatibleElements
	 */
	private void resolveReferences() {
		if (this.pendingReferences == null
				|| this.pendingReferences.size() == 0) {
			return;
		}
		for (FieldReference reference : this.pendingReferences) {
			DataElement referredElement = this.elements
					.get(reference.referredName);
			if (referredElement != null) {
				this.addDataElement(reference.fieldName, referredElement);
				continue;
			}
			StringBuilder sbf = new StringBuilder();
			sbf.append("Design Error: Data element ").append(
					reference.fieldName);
			String recordName = reference.recordName;
			if (recordName != null) {
				sbf.append(" which is part of record/group ")
						.append(recordName);
			}
			sbf.append(" refers to a non-existing name ").append(
					reference.referredName);
			Spit.out(sbf.toString());
			this.inError = true;
		}
	}

	/**
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		ResourceManager
				.loadAllResources(
						"D:/exilityTurtle/exilityTurtleClient/WebContent/WEB-INF/resource/",
						"d:/exilityTurtle/exilityTurtleClient/WebContent/WEB-INF/exilityResource/");
		DataElement ele = DataDictionary.getElement("number");
		Spit.out("Element is " + ele == null ? "null"
				: (ele.name + " " + DataTypes.getDataType("number", null)));
	}
}

/**
 * keep track of unresolved references
 * 
 * @author raghu.bhandi
 * 
 */
class FieldReference {
	final String fieldName;
	final String referredName;
	final String recordName;

	FieldReference(String fieldName, String referredName, String recordName) {
		this.fieldName = fieldName;
		this.referredName = referredName;
		this.recordName = recordName;
	}
}
