package com.tandbergtv.metadatamanager.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.log4j.Logger;

import com.tandbergtv.metadatamanager.factoryImpl.IdentifierFactory;
import com.tandbergtv.metadatamanager.specimpl.ttv.TTVId;
import com.tandbergtv.metadatamanager.util.AssetUtil;

/**
 * Class representing an asset in the common metadata manager module.
 * 
 * @author spuranik
 * 
 */
public abstract class Asset {
	private static final Logger logger = Logger.getLogger(Asset.class);

	private static final String _CUSTOMFIELD = "CustomField";
	private static final String _XPATH_SPLIT = "/";
	private static final String _ATTRIBUTE = "@";
	private static final String _TNS = "tns:";
	private static final String _TNS_FIELDS = "tns:Fields";

	private long id;

	private List<Field> fields;

	private List<FieldRevision> fieldRevisions;

	private List<Field> allDescendantAssetFields;

	private Asset root;

	private FieldTree fieldTree;

	private int latestRevisionNumber = NextRevision.STARTING_REVISION_NUMBER;
	private boolean isRevisionNumberUpdated = false;

	// <TTVXpath+Indices, FieldRevision>
	private Map<String, FieldRevision> fieldRevisionsMap;
	// <TTVXpath+Indices, Field>
	private Map<String, Field> fieldsMap;
	// <TTVXpath+value, Field>
	private Map<String, Field> fieldsMapByPathValue;

	/**
	 * These are the set of all kinds of relationships.
	 */
	private List<Relation> relations;

	private AssetState state;

	private String externalRevision;
	public static String EXTERNAL_INTERNAL_REVISION_DELIMITER = ".";

	public Asset() {
		fields = new ArrayList<Field>();
		this.relations = new ArrayList<Relation>();
		state = AssetState.ACTIVE;
		root = null;
	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public List<Field> getFields() {
		return fields;
	}

	/**
	 * clears the current field list and calls addfield for each field passed
	 * in.
	 * 
	 * @param fields
	 */
	public void setFields(List<Field> fields) {
		this.fields.clear();
		for (Field f : fields) {
			addField(f);
		}
	}

	/**
	 * Adds the specified field. Sets the root asset for the field
	 * 
	 * @param field
	 */
	public void addField(Field field) {
		Asset root = getRoot();
		if (root == null)
			root = this;

		field.setParentAsset(this);
		field.setRoot(root);
		this.fields.add(field);
	}

	public void addFieldRevision(FieldRevision fieldRevision,
			NextRevision nextRevision) {
		Asset root = getRoot();
		if (root == null)
			root = this;

		fieldRevision.setParentAsset(this);
		fieldRevision.setRoot(root);
		if (this.fieldRevisions == null) {
			this.fieldRevisions = new ArrayList<FieldRevision>();
		}
		this.fieldRevisions.add(fieldRevision);
		this.setLatestRevisionNumber(nextRevision.getRevisionNumber());
	}

	/**
	 * Adds a child to this asset.
	 * 
	 * @param child
	 */
	public void addChild(Asset child) {
		logger.debug("added a new child to asset(" + this.getId() + ")");
		this.relations.add(new ParentChildRelation(this, child));
		if (root == null) {
			child.root = this;
		} else {
			child.root = root;
		}

		child.reRootFields();
	}

	/**
	 * copy fields to fieldRevisions if child Asset not existing in DB
	 * 
	 * @param child
	 * @param nextRevision
	 * @param isChildNew
	 *            true if child Asset not existing in DB
	 */
	public void addChild(Asset child, NextRevision nextRevision,
			boolean isChildNew) {
		this.relations.add(new ParentChildRelation(this, child, nextRevision));
		if (root == null) {
			child.root = this;
		} else {
			child.root = root;
		}
		child.reRootFields();
		this.setLatestRevisionNumber(nextRevision.getRevisionNumber());

		if (isChildNew) {
			AssetUtil.copyFieldsToFieldRevisions(child, nextRevision);
		}
	}

	/**
	 * Update all fields in the descendant tree to point to the root asset
	 */
	private void reRootFields() {
		for (Field field : this.fields)
			field.setRoot(root);

		for (Relation relation : this.relations) {
			if (relation instanceof ParentChildRelation) {
				if (relation.getOwnerAsset().equals(this)) {
					Asset asset = relation.getTargetAsset();
					asset.reRootFields();
				}
			}
		}
	}

	public AssetState getState() {
		return state;
	}

	public boolean isActive() {
		return state == AssetState.ACTIVE ? true : false;
	}

	public void setState(AssetState state) {
		this.state = state;
	}

	public void setState(boolean state) {
		if (state) {
			setState(AssetState.ACTIVE);
		} else {
			setState(AssetState.INACTIVE);
		}
	}

	public List<Field> getAllDescendantAssetFields() {
		return allDescendantAssetFields;
	}

	public List<Relation> getRelations() {
		return relations;
	}

	public void setRelations(List<Relation> relations) {
		this.relations = relations;
	}

	public Asset getRoot() {
		return root;
	}

	public TTVId getTTVId() {
		TTVId ttvId = (TTVId) IdentifierFactory.getTTVIdentifier();
		ttvId.setId(id);
		return ttvId;
	}

	public void setTTVId(TTVId id) {
		this.id = id.getId();
	}

	/**
	 * Determines the parent asset for a given asset. Called via the root asset.
	 * 
	 * @param asset
	 * @return
	 */
	public Asset getAssetsParent(Asset a) {
		for (Relation r : this.relations) {
			Asset targetAsset = r.getTargetAsset();
			if (targetAsset.getId() == a.getId()) {
				return this;
			} else {
				Asset asset = targetAsset.getAssetsParent(a);

				if (asset != null)
					return asset;
			}
		}
		return null;
	}

	/**
	 * returns the first occurrence of a field with the specified xpath
	 * 
	 * @param xpath
	 * @return
	 */
	public Field getFirstField(String xpath) {
		for (Field f : fields) {
			if (f.getTtvXPath().equals(xpath)) {
				return f;
			}
		}
		return null;
	}

	/**
	 * For a given xpath, returns all fields that have the given xpath (for
	 * different indices)
	 * 
	 * @param xpath
	 * @return list containing all fields that have the given xpath
	 */
	public List<Field> getAllFieldsForXpath(String assetType, String xpath) {
		ArrayList<Field> list = new ArrayList<Field>();

		List<Asset> assets = getAllAssetsOfType(assetType);

		for (Asset child : assets) {
			list = child.getFieldsForXpath(xpath);
		}
		return list;
	}

	/**
	 * @param assetType
	 * @return
	 */
	public List<Asset> getAllAssetsOfType(String assetType) {
		List<Asset> assets = getTargetAssets(assetType);

		Asset a = new AssetUtil().unWrap(this);
		if (a instanceof Group) {
			Group group = (Group) a;
			String type = group.getType().toString();
			if (type.toLowerCase().equals(assetType.toLowerCase())) {
				assets.add(a);
			}
		} else if (a instanceof Item) {
			Item item = (Item) a;
			String type = item.getType().toString();
			if (type.toLowerCase().equals(assetType.toLowerCase())) {
				assets.add(a);
			}
		}
		return assets;
	}

	private ArrayList<Field> getFieldsForXpath(String xpath) {
		ArrayList<Field> list = new ArrayList<Field>();
		for (Field f : this.getFields()) {
			if (f.getTtvXPath().startsWith(xpath)) {

				list.add(f);
			}
		}
		return list;
	}

	/**
	 * For a given xpath, returns all fields that match or are a part of the
	 * xpath
	 * 
	 * returns a list of fieldtrees. each fieldtree is rooted at the xpath
	 * provided. there could be multiple fieldtrees if there were multiple
	 * assets found for the given assettype
	 * 
	 * @param assetType
	 * @param xpath
	 * @return a list of fieldtrees
	 */
	public List<FieldTree> getFieldTreeForXpath(String assetType, String xpath) {
		List<FieldTree> fieldTreeList = new ArrayList<FieldTree>();

		List<Asset> assets = getAllAssetsOfType(assetType);

		ArrayList<Field> fieldList = new ArrayList<Field>();

		for (Asset a : assets) {
			fieldList = a.getFieldsForXpath(xpath);
			if (fieldList != null && fieldList.size() > 0) {

				int depth = xpath.split(_XPATH_SPLIT).length - 1;
				int index = fieldList.get(0).getIndices().get(depth - 1);

				FieldTree tree = new FieldTree(xpath, index);
				populateFieldTree(tree, fieldList);

				fieldTreeList.add(tree);
			}
		}

		return fieldTreeList;
	}

	/**
	 * Returns an asset which is an immediate neighbor (descendant) matching the
	 * specified id
	 * 
	 * @param id
	 * @return
	 */
	public Asset getAsset(long id) {
		for (Relation relation : this.getRelations()) {
			Asset a = relation.getTargetAsset();

			if (a.getId() == id)
				return a;
		}

		return null;
	}

	/**
	 * Return an child asset which satisfy the searchCriteria
	 * 
	 * @param searchCriteria
	 * @return
	 */
	public Asset getAsset(SearchCriteria searchCriteria) {
		Asset returnAsset = null;
		for (Relation relation : this.getRelations()) {
			Asset targetAsset = relation.getTargetAsset();
			if (targetAsset.meetCriteria(searchCriteria)) {
				returnAsset = targetAsset;
				break;
			}
		}

		return returnAsset;
	}

	/**
	 * Check to see if asset(this) meet the searchCriteria
	 * 
	 * @param searchCriteria
	 * @return
	 */
	protected boolean meetCriteria(SearchCriteria searchCriteria) {
		boolean result = true;

		for (Entry<String, String> criteriaEntry : searchCriteria.entrySet()) {

			if (!this.getFieldsMapByPathValue().containsKey(
					criteriaEntry.getKey() + criteriaEntry.getValue())) {
				result = false;
				break;
			}
		}
		return result;
	}

	/**
	 * Returns all first level child items that matches the targetAssetType
	 * 
	 * @param targetAssetType
	 * @return
	 */
	public List<Asset> getTargetAssets(String targetAssetType) {
		ArrayList<Asset> targetAssetList = new ArrayList<Asset>();

		Asset ta = null;
		for (Relation relation : this.getRelations()) {
			ta = new AssetUtil().unWrap(relation.getTargetAsset());
			if (ta instanceof Item) {
				Item item = (Item) ta;
				String type = item.getType().toString();
				if (type.toLowerCase().equals(targetAssetType.toLowerCase())) {
					targetAssetList.add(ta);
				}
			} else if (ta instanceof Group) {
				Group group = (Group) ta;
				String type = group.getType().toString();
				if (type.toLowerCase().equals(targetAssetType.toLowerCase())) {
					targetAssetList.add(ta);
				}
			}
		}
		return targetAssetList;
	}

	/**
	 * gets children of a specific type (only next level)
	 * 
	 * @param clazz
	 * @return
	 */

	public <T extends Asset> List<T> getChildrenOfType(Class<T> clazz) {
		Asset ta = null;
		List<T> list = new ArrayList<T>();
		for (Relation relation : this.getRelations()) {
			ta = new AssetUtil().unWrap(relation.getTargetAsset());
			if (clazz.isAssignableFrom(ta.getClass())) {
				list.add((clazz.cast(ta)));
			}
		}
		return list;
	}

	/**
	 * File Type Items are included if includeFiles is TRUE
	 * 
	 * @param includeFiles
	 * @return a flat list of the asset tree's items (ITEM types)
	 */
	public List<Asset> getAllDescendantItems(boolean includeFiles) {
		List<Asset> list = new ArrayList<Asset>();
		walkAssetTree(this, list, includeFiles);

		return list;
	}

	/**
	 * 
	 * @param asset
	 * @param list
	 */
	private void walkAssetTree(Asset asset, List<Asset> list,
			boolean includeFiles) {
		asset = new AssetUtil().unWrap(asset);
		if (asset instanceof Item) {
			if (asset instanceof File) {
				if (includeFiles) {
					list.add(asset);
				}
			} else {
				list.add(asset);
			}
		}

		for (Relation r : asset.getRelations()) {
			walkAssetTree(r.getTargetAsset(), list, includeFiles);
		}
	}

	/**
	 * 
	 * @return a flat list of all files at descended directly from this asset or
	 *         through other file asset only
	 */
	public List<File> getDirectDescendantFiles() {
		List<File> list = new ArrayList<File>();

		getAllFilesRecursively(this, list, true);

		return list;
	}

	/**
	 * 
	 * @return a flat list of all files at all levels
	 */
	public List<File> getAllDescendantFiles() {
		List<File> list = new ArrayList<File>();

		getAllFilesRecursively(this, list, false);

		return list;
	}

	private void getAllFilesRecursively(Asset asset, List<File> list,
			boolean onlyFilesInThisAsset) {
		asset = new AssetUtil().unWrap(asset);
		if (asset instanceof File) {
			list.add((File) asset);
		}

		if (asset.getRelations() != null) {
			for (Relation r : asset.getRelations()) {
				Asset targetAsset = r.getTargetAsset();
				if (!onlyFilesInThisAsset || targetAsset instanceof File) {
					getAllFilesRecursively(targetAsset, list,
							onlyFilesInThisAsset);
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		String EOL = System.getProperty("line.separator");
		StringBuilder builder = new StringBuilder();

		builder.append("[" + id + "]");

		if (relations != null)
			builder.append(", ").append(relations.size())
					.append(" relation(s)");

		builder.append(EOL);

		for (Field field : this.fields)
			builder.append("\t").append(field).append(EOL);

		return builder.toString();
	}

	/**
	 * Gets the un-marshalled asset using the visitor passed in.
	 * 
	 * @param visitor
	 * @return
	 */
	public void accept(IAssetVisitor visitor) {
		visitor.unWrap(this);
	}

	/**
	 * Checks if the current asset object is an assignable form of the given
	 * class.
	 * 
	 * @param clazz
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public boolean isInstance(Class clazz) {
		return this.getClass().isAssignableFrom(clazz);
	}

	/**
	 * generates a tree representation of the fields
	 * 
	 * @return
	 */
	public FieldTree getFieldTree() {
		/* generate the fieldTree */
		fieldTree = new FieldTree();
		populateFieldTree(fieldTree, fields);

		return fieldTree;
	}

	/**
	 * given a tree structure representing the fields, it generates the field
	 * list (xpath, value, indices, etc)
	 * 
	 * @param fieldTree
	 */
	public void setFieldTree(FieldTree fieldTree) {
		this.fieldTree = fieldTree;
		List<Field> fields = buildFieldListFromTree(fieldTree);
		this.setFields(fields);
	}

	/**
	 * takes the fields in the list of fields and adds them in the correct
	 * position in the field tree
	 * 
	 * @param fieldTree
	 * @param fields
	 */
	private void populateFieldTree(FieldTree fieldTree, List<Field> fields) {
		for (Field f : fields) {
			String xpath = f.getTtvXPath();
			String[] xpathParts = xpath.split(_XPATH_SPLIT);

			FieldTreeNode node = null;
			FieldTreeNode parentNode = null;

			FieldTreeNode rootNode = fieldTree.getRootElement();
			parentNode = rootNode;

			for (int i = 0; i < xpathParts.length; i++) {
				String xpathPart = xpathParts[i];
				if (xpathPart.equals("") || xpathPart.equals(_TNS_FIELDS)) {
					continue;
				}
				xpathPart = xpathPart.replace(_TNS, "");

				boolean isAttribute = false;
				if (!xpathPart.contains(_CUSTOMFIELD)) {
					if (xpathPart.contains(_ATTRIBUTE)) {
						isAttribute = true;
						/* if its an attribute, there will be an @. remove that. */
						xpathPart = xpathPart.replace(_ATTRIBUTE, "");
					}
				}

				Integer curIndex = 0;
				if (isAttribute) {
					curIndex = 1;
				} else {
					curIndex = f.getIndices().get(i - 1);
				}
				node = parentNode.getNode(xpathPart, curIndex);
				if (node == null) {
					// node was not found. Need to create one.
					node = new FieldTreeNode();
					node.setName(xpathPart);
					node.setCurrentIndex(curIndex);
					parentNode.addChild(node);
				}
				if (i == xpathParts.length - 1) {
					node.setField(f);
					node.setAttribute(isAttribute);
				}
				parentNode = node;
			}
		}
	}

	/**
	 * given a tree structure representing the fields, generates the list of
	 * field objects that will be persisted
	 * 
	 * @param fieldTree
	 * @return
	 */
	private List<Field> buildFieldListFromTree(FieldTree fieldTree) {
		// do a breadth first traversal and set the correct index for each node.
		ConcurrentLinkedQueue<FieldTreeNode> queue = new ConcurrentLinkedQueue<FieldTreeNode>();
		fieldTree.breadthFirstTraversal(queue);

		List<Field> fields = fieldTree.depthFirstTraversal();
		return fields;
	}

	public int getLatestRevisionNumber() {
		return latestRevisionNumber;
	}

	public void setLatestRevisionNumber(int latestRevisionNumber) {
		if (!isRevisionNumberUpdated) {
			this.setRevisionNumberUpdated(true);
		}
		this.latestRevisionNumber = latestRevisionNumber;
	}

	public boolean isRevisionNumberUpdated() {
		return isRevisionNumberUpdated;
	}

	public void setRevisionNumberUpdated(boolean isRevisionNumberUpdated) {
		this.isRevisionNumberUpdated = isRevisionNumberUpdated;
	}

	public List<FieldRevision> getFieldRevisions() {
		return fieldRevisions;
	}

	public void setFieldRevisions(List<FieldRevision> fieldRevisions) {
		this.fieldRevisions = fieldRevisions;
	}

	/**
	 * 
	 * @return the asset type
	 */
	public String getAssetType() {
		String assetType = "";
		if (this instanceof Group) {
			Group g = (Group) this;
			assetType = g.getType();
		} else {
			Item i = (Item) this;
			assetType = i.getType();
		}
		return assetType;
	}

	public String getExternalRevision() {
		return externalRevision;
	}

	public void setExternalRevision(String externalRevision) {
		this.externalRevision = externalRevision;
	}

	public String getVersion() {
		String extRevision = "";
		if (getExternalRevision() != null) {
			extRevision = getExternalRevision().trim();
		}
		return extRevision + EXTERNAL_INTERNAL_REVISION_DELIMITER
				+ getLatestRevisionNumber();
	}

	/**
	 * gets a flat list of all immediate children
	 * 
	 * @return
	 */
	public List<Asset> getImmediateChildren() {
		List<Asset> immediateChildren = new ArrayList<Asset>();
		for (Relation r : this.relations) {
			immediateChildren.add(r.getTargetAsset());
		}
		return immediateChildren;
	}

	public Map<String, FieldRevision> getFieldRevisionsMap() {
		if (this.fieldRevisionsMap == null) {
			this.fieldRevisionsMap = new HashMap<String, FieldRevision>();
			constructIFieldMap(this.getFieldRevisions(), this.fieldRevisionsMap);
		}
		return fieldRevisionsMap;
	}

	public void setFieldRevisionsMap(
			Map<String, FieldRevision> fieldRevisionsMap) {
		this.fieldRevisionsMap = fieldRevisionsMap;
	}

	public Map<String, Field> getFieldsMap() {
		initFieldsMap();
		return this.fieldsMap;
	}

	public Map<String, Field> getFieldsMapByPathValue() {
		initFieldsMap();
		return this.fieldsMapByPathValue;
	}

	private void initFieldsMap() {
		if (this.fieldsMap == null && this.fieldsMapByPathValue == null) {
			this.fieldsMap = new HashMap<String, Field>();
			this.fieldsMapByPathValue = new HashMap<String, Field>();
			constructFieldMap(this.getFields(), this.fieldsMap,
					this.fieldsMapByPathValue);
		}
	}

	public void setFieldsMap(Map<String, Field> fieldsMap) {
		this.fieldsMap = fieldsMap;
	}

	protected <N extends IField> void constructIFieldMap(List<N> iFields,
			Map<String, N> iFieldsMap) {
		for (N iField : iFields) {
			iFieldsMap.put(iField.getTtvXPath() + iField.getIndices(), iField);
		}
	}

	protected <N extends IField> void constructFieldMap(List<N> iFields,
			Map<String, N> iFieldsMap, Map<String, N> iFieldsMapByTTVXpath) {
		for (N iField : iFields) {
			iFieldsMap.put(iField.getTtvXPath() + iField.getIndices(), iField);
			iFieldsMapByTTVXpath.put(iField.getTtvXPath() + iField.getValue(),
					iField);
		}
	}

	/**
	 * walks the complete tree (as well as the fields). this is used to avoid
	 * the lazyinit exception
	 */
	public void loadCompleteTree() {
		if (this.getFields() != null)
			this.getFields().size();

		if (this.getAllDescendantAssetFields() != null)
			this.getAllDescendantAssetFields().size();

		if (this.getFieldRevisions() != null)
			this.getFieldRevisions().size();

		for (Relation r : this.getRelations()) {
			r.getTargetAsset().loadCompleteTree();
		}
	}
}
