package org.jboss.tools.seam.pages.xml.model.helpers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.tools.common.model.XModelObject;
import org.jboss.tools.common.model.util.XModelObjectUtil;
import org.jboss.tools.jst.web.model.ReferenceObject;
import org.jboss.tools.jst.web.model.helpers.autolayout.AutoLayout;
import org.jboss.tools.seam.pages.xml.model.SeamPagesConstants;
import org.jboss.tools.seam.pages.xml.model.helpers.autolayout.SeamPagesItems;
import org.jboss.tools.seam.pages.xml.model.impl.ReferenceObjectImpl;
import org.jboss.tools.seam.pages.xml.model.impl.SeamPagesDiagramImpl;

public class SeamPagesDiagramHelper implements SeamPagesConstants {
	private XModelObject diagram;
	private static XModelObject TEMPLATE;
	private XModelObject config;
	private Map<String,XModelObject> pageItems = new HashMap<String,XModelObject>();
	private Set<String> pageViewIds = new HashSet<String>();
	private Map<String,XModelObject> exceptionItems = new HashMap<String,XModelObject>();
	private Map<String,XModelObject> targets = new HashMap<String,XModelObject>();
	private Set<String> changedTargets = new HashSet<String>();

	public SeamPagesDiagramHelper(XModelObject diagram) {
		this.diagram = diagram;
	}

	public static SeamPagesDiagramHelper getHelper(XModelObject diagram) {
		return ((SeamPagesDiagramImpl)diagram).getHelper();
	}

	private synchronized void reset() {
		pageItems.clear();
		pageViewIds.clear();
		exceptionItems.clear();
		targets.clear();
		changedTargets.clear();
		this.config = diagram.getParent();
	}

	public void restoreRefs() {
		((SeamPagesDiagramImpl)diagram).setReference(diagram.getParent());
	}
	
	Set<Object> updateLocks = new HashSet<Object>();
	
	public boolean isUpdateLocked() {
		return !updateLocks.isEmpty();
	}
	
	public void addUpdateLock(Object lock) {
		updateLocks.add(lock);
	}

	public void removeUpdateLock(Object lock) {
		updateLocks.remove(lock);
	}

	public void updateDiagram() {
		if(isUpdateLocked()) return;
		addUpdateLock(this);
		try {
			updateDiagram0();
		} finally {
			removeUpdateLock(this);
		}
	}

	private void updateDiagram0() {
		reset();
		XModelObject[] sourcePages = config.getChildByPath(FOLDER_PAGES).getChildren();

		XModelObject[] cs = diagram.getChildren();
		Map<XModelObject,XModelObject> old = new HashMap<XModelObject, XModelObject>();
		for (int i = 0; i < cs.length; i++) {
			if(cs[i] instanceof ReferenceObject) {
				XModelObject k = ((ReferenceObject)cs[i]).getReference();
				if(k != null) old.put(k, cs[i]);
			}
		}

		for (int i = 0; i < sourcePages.length; i++) {
			String view = sourcePages[i].getAttributeValue(ATTR_VIEW_ID);
			if(view == null) continue;
			String pp = toNavigationRulePathPart(view);
			XModelObject og = old.get(sourcePages[i]);
			if(og != null) {
				String opp = og.getPathPart();
				if(!pp.equals(opp)) {
					XModelObject og1 = diagram.getChildByPath(pp);
					if(og1 != null) {
						og1.removeFromParent();
						changedTargets.add(toNavigationRulePathPart(og1.getAttributeValue(ATTR_PATH)));
					}
					pageItems.remove(opp);
					og.setAttributeValue(ATTR_NAME, pp);
				}
			}
			XModelObject g = og != null ? og : findOrCreateItem(view, pp, TYPE_PAGE);
			((ReferenceObjectImpl)g).setReference(sourcePages[i]);
			pageItems.put(pp, g);
			pageViewIds.add(view);
			XModelObject[] ns = sourcePages[i].getChildren();
			for (int j = 0; j < ns.length; j++) {
				String entity = ns[j].getModelEntity().getName();
				if(!entity.startsWith(ENT_NAVIGATION)) continue;
				if(entity.startsWith(ENT_NAVIGATION_RULE)) {
					addTarget(ns[j], true);
					if(!entity.endsWith(SUFF_21) && !entity.endsWith(SUFF_22)) {
						continue;
					}
				}
				XModelObject[] rs = ns[j].getChildren();
				for (int k = 0; k < rs.length; k++) {
					addTarget(rs[k], true);
				}
			}
		}

		XModelObject[] sourceExceptions = config.getChildByPath(FOLDER_EXCEPTIONS).getChildren();
		for (int i = 0; i < sourceExceptions.length; i++) {
			String code = sourceExceptions[i].getAttributeValue("class");
			String pp = "exception:" + code;
			XModelObject og = old.get(sourceExceptions[i]);
			if(og != null) {
				String opp = og.getPathPart();
				if(!pp.equals(opp)) {
					exceptionItems.remove(opp);
					og.setAttributeValue(ATTR_NAME, pp);
					((ReferenceObjectImpl)og).setReference(null);
				}
			}
			XModelObject g = og != null ? og : findOrCreateItem(code, pp, TYPE_EXCEPTION);
			((ReferenceObjectImpl)g).setReference(sourceExceptions[i]);
			exceptionItems.put(pp, g);
			addTarget(sourceExceptions[i], false);
		}

		Iterator<String> it = pageItems.keySet().iterator();
		while(it.hasNext()) targets.remove(it.next());

		removeObsoleteExceptionItems();
		createPageItems();
		removeObsoletePageItems();
		updatePageItems();
		updateExceptionItems();
		
		updatePages();
	}

	private void addTarget(XModelObject rule, boolean addEmpty) {
		XModelObject target = rule.getChildByPath("target");
		if(target == null) return;
		String tvi = target.getAttributeValue(ATTR_VIEW_ID);
		if(tvi == null) return;
		if(!addEmpty && tvi.length() == 0) return;
		targets.put(toNavigationRulePathPart(tvi), getTemplate());							
	}

	private XModelObject getTemplate() {
		if(TEMPLATE == null && diagram != null) {
			TEMPLATE = diagram.getModel().createModelObject(ENT_DIAGRAM_ITEM, null);
		}
		return TEMPLATE;
	}
	
	public XModelObject findOrCreateItem(String path, String pp, String type) {
		if(pp == null) pp = toNavigationRulePathPart(path);
		XModelObject g = diagram.getChildByPath(pp);
		if(g == null) {
			g = diagram.getModel().createModelObject(ENT_DIAGRAM_ITEM, null);
			g.setAttributeValue(ATTR_NAME, pp);
			g.setAttributeValue(ATTR_PATH, path);
			g.setAttributeValue(ATTR_TYPE, type);
			diagram.addChild(g);
			changedTargets.add(pp);
		}
		return g;
	}
	
	private void removeObsoletePageItems() {
		XModelObject[] ps = diagram.getChildren(ENT_DIAGRAM_ITEM);
		for (int i = 0; i < ps.length; i++) {
			String path = ps[i].getPathPart();
			String type = ps[i].getAttributeValue(ATTR_TYPE);
			if(!TYPE_PAGE.equals(type)) continue;
			if(!pageItems.containsKey(path) && targets.get(path) != ps[i]) {
				ps[i].removeFromParent();
			}
		}
	}

	private void removeObsoleteExceptionItems() {
		XModelObject[] ps = diagram.getChildren(ENT_DIAGRAM_ITEM);
		for (int i = 0; i < ps.length; i++) {
			String path = ps[i].getPathPart();
			String type = ps[i].getAttributeValue(ATTR_TYPE);
			if(!TYPE_EXCEPTION.equals(type)) continue;
			if(!exceptionItems.containsKey(path)) {
				ps[i].removeFromParent();
			}
		}
	}

	private void createPageItems() {
		String[] paths = (String[])targets.keySet().toArray(new String[0]);
		for (int i = 0; i < paths.length; i++) {
			String fvi = toFromViewId(paths[i]);
//			fvi = findBestMatch(fvi);
			XModelObject g = findOrCreateItem(fvi, toNavigationRulePathPart(fvi), TYPE_PAGE);
			targets.put(paths[i], g);			
		}
	}

	private void updatePageItems() {
		ReferenceObjectImpl[] gs = (ReferenceObjectImpl[])pageItems.values().toArray(new ReferenceObjectImpl[0]);
		for (int i = 0; i < gs.length; i++) {
			updatePageItem(gs[i]);
		}
		gs = (ReferenceObjectImpl[])targets.values().toArray(new ReferenceObjectImpl[0]);
		for (int i = 0; i < gs.length; i++) {
			long ts = gs[i].getTimeStamp();
			boolean hadReference = gs[i].getReference() != null;
			gs[i].setReference(null);
			gs[i].setAttributeValue("params", "");
			
			XModelObject[] os = gs[i].getChildren(ENT_DIAGRAM_ITEM_OUTPUT);
			for (int j = 0; j < os.length; j++) {
				if(SUBTYPE_CUSTOM.equals(os[j].getAttributeValue(ATTR_SUBTYPE))) {
					continue;
				}
				gs[i].removeChild(os[j]);
			}
			updatePageItem(gs[i]);
			if(hadReference && ts == gs[i].getTimeStamp()) {
				gs[i].fireReferenceChanged();
			}
		}
	}
	
	private void updateExceptionItems() {
		ReferenceObjectImpl[] gs = (ReferenceObjectImpl[])exceptionItems.values().toArray(new ReferenceObjectImpl[0]);
		for (int i = 0; i < gs.length; i++) {
			updateExceptionItem(gs[i]);
		}
	}

	private void updatePageItem(ReferenceObjectImpl item) {
		if(item.getReference() == null) {
			updateUndeclaredPageItem(item);
			return;
		}
//		if(item.isUpToDate()) return;
		boolean iud = item.isUpToDate();
		long ts = item.getTimeStamp();
		item.notifyUpdate();
		XModelObject sourcePage = item.getReference();		
		item.setAttributeValue(ATTR_ID, sourcePage.getPathPart());
		item.setAttributeValue(ATTR_PATH, sourcePage.getAttributeValue(ATTR_VIEW_ID));
		String[][] params = SeamPagesDiagramStructureHelper.getInstance().getParams(item);
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < params.length; i++) {
			sb.append(params[i][0]).append('=').append(params[i][1]).append(';');
		}
		item.setAttributeValue("params", sb.toString());
		XModelObject[] cs = getPageTargets(sourcePage);		
		updateOutputs(item, cs);
		if(!iud && ts == item.getTimeStamp()) {
			item.fireReferenceChanged();
		}
	}

	private void updateUndeclaredPageItem(ReferenceObjectImpl item) {
		//update virtual link to findBestMatch(path) element
		XModelObject[] os = item.getChildren();
		String path = item.getAttributeValue(ATTR_PATH);
		String fvi = findBestMatch(path);
		XModelObject g = getPage(fvi);
		if(g != null && item != g) {
			XModelObject output = item.getModel().createModelObject(ENT_DIAGRAM_ITEM_OUTPUT, null);
			output.setAttributeValue(ATTR_ID, fvi);
			output.setAttributeValue(ATTR_PATH, fvi);
			String name = XModelObjectUtil.createNewChildName("output", item);
			output.setAttributeValue(ATTR_NAME, name);

			ReferenceObjectImpl r = (ReferenceObjectImpl)output;
			r.setReference(null);

			String target = (g == null) ? "" : g.getPathPart();
			output.setAttributeValue(ATTR_TARGET, target);

			item.addChild(output);
		}
		
	}

	private XModelObject[] getPageTargets(XModelObject o) {
		XModelObject[] ns = o.getChildren();
		List<XModelObject> result = null;
		for (int i = 0; i < ns.length; i++) {
			String entity = ns[i].getModelEntity().getName();
			if(!entity.startsWith(ENT_NAVIGATION)) continue;
			if(entity.startsWith(ENT_NAVIGATION_RULE)) {
				XModelObject t = getTargetChild(ns[i]);
				if(t != null) {
					if(result == null) result = new ArrayList<XModelObject>();
					result.add(t);
				}
				if(!entity.endsWith(SUFF_21) && !entity.endsWith(SUFF_22)) {
					continue;
				}
			}
			XModelObject[] rs = ns[i].getChildren();
			for (int k = 0; k < rs.length; k++) {
				XModelObject t = getTargetChild(rs[k]);
				if (t != null) {
					if (result == null) result = new ArrayList<XModelObject>();
					result.add(t);
				}
			}
			
		}
		return result == null ? new XModelObject[0]
		             : result.toArray(new XModelObject[0]);
	}
	
	private void updateExceptionItem(ReferenceObjectImpl item) {
		if(item.isUpToDate()) return;
		item.notifyUpdate();
		XModelObject exc = item.getReference();		
		item.setAttributeValue(ATTR_ID, exc.getPathPart());
		item.setAttributeValue(ATTR_PATH, exc.getAttributeValue("class"));
		XModelObject t = getTargetChild(exc);
		XModelObject[] cs = t == null ? new XModelObject[0] : new XModelObject[]{t};
		if(cs.length == 1) {
			String path = t.getAttributeValue(ATTR_VIEW_ID);
			if(path == null || path.length() == 0) {
				cs = new XModelObject[0];
			}
		}
		updateOutputs(item, cs);
	}

	private XModelObject getTargetChild(XModelObject o) {
		XModelObject t = o.getChildByPath("target");
		if(t == null || t.getModelEntity().getAttribute(ATTR_VIEW_ID) == null) return null;
		return t;		
	}
	
	private void updateOutputs(ReferenceObjectImpl item, XModelObject[] cases) {
		XModelObject[] os = item.getChildren();
		if(isOutputOrderUpToDate(cases, os)) {
			updateOutputs_1(item, cases, os);
		} else {
			updateOutputs_2(item, cases, os);
		}
	}

	private void updateOutputs_1(ReferenceObjectImpl item, XModelObject[] cases, XModelObject[] outputs) {
		int c = 0;
		for (int i = 0; i < cases.length; i++) {
			XModelObject output = null;
			if(c < outputs.length) {
				output = outputs[c]; 
			} else {
				output = createOutput(item, cases[i]);
			}
			ReferenceObjectImpl r = (ReferenceObjectImpl)output;
			r.setReference(cases[i]);
			updateOutput(r);
			++c;
		}
		for (int i = c; i < outputs.length; i++) outputs[i].removeFromParent();
	}

	private void updateOutputs_2(ReferenceObjectImpl item, XModelObject[] cases, XModelObject[] outputs) {
		Map<String,XModelObject> map = new HashMap<String,XModelObject>();
		for (int i = 0; i < outputs.length; i++) {
			XModelObject output = outputs[i];
			output.removeFromParent();
			map.put(output.getAttributeValue(ATTR_ID), output);			
		}
		for (int i = 0; i < cases.length; i++) {
			XModelObject output = map.get(cases[i].getPathPart());
			if(output == null) {
				output = createOutput(item, cases[i]);
			} else {
				item.addChild(output);
			}
			ReferenceObjectImpl r = (ReferenceObjectImpl)output;
			r.setReference(cases[i]);
			updateOutput(r);
		}
	}
	
	private boolean isOutputOrderUpToDate(XModelObject[] cases, XModelObject[] outputs) {
		for (int i = 0; i < cases.length && i < outputs.length; i++) {
			ReferenceObject r = (ReferenceObject)outputs[i];
			if(r.getReference() == cases[i]) continue;
			String pp = cases[i].getAttributeValue(ATTR_VIEW_ID);
			String id = outputs[i].getAttributeValue(ATTR_ID);
			if(!pp.equals(id)) return false;			
		}
		return true;
	}
	
	private XModelObject createOutput(XModelObject item, XModelObject rulecase) {
		XModelObject output = item.getModel().createModelObject(ENT_DIAGRAM_ITEM_OUTPUT, null);
		output.setAttributeValue(ATTR_ID, rulecase.getAttributeValue(ATTR_VIEW_ID));
		output.setAttributeValue(ATTR_PATH, rulecase.getAttributeValue(ATTR_VIEW_ID));
		String name = XModelObjectUtil.createNewChildName("output", item);
		output.setAttributeValue(ATTR_NAME, name);

		ReferenceObjectImpl r = (ReferenceObjectImpl)output;
		r.setReference(rulecase);
		updateOutput(r);

		item.addChild(output);
		return output;
	}
	
	private void updateOutput(ReferenceObjectImpl output) {
		if(output.getReference() == null) return;
//		if(output.isUpToDate()) return;
		output.notifyUpdate();
		XModelObject rulecase = output.getReference();		
		output.setAttributeValue(ATTR_ID, rulecase.getAttributeValue(ATTR_VIEW_ID));
		String path = rulecase.getAttributeValue(ATTR_VIEW_ID);
		output.setAttributeValue(ATTR_PATH, path);
//		String title = SeamPagesDiagramStructureHelper.createItemOutputPresentation(rulecase);
//		output.setAttributeValue("title", title);

//		String fvi = findBestMatch(path);
		XModelObject g = getPage(path);
		String target = (g == null) ? "" : g.getPathPart();
		if(target != null && target.equals(output.getAttributeValue(ATTR_TARGET)) && changedTargets.contains(target)) {
			output.setAttributeValue(ATTR_TARGET, "");
		}
		output.setAttributeValue(ATTR_TARGET, target);
	}
	
	public void autolayout() {
		AutoLayout auto = new AutoLayout();
		auto.setItems(new SeamPagesItems());
		auto.setProcess(diagram);
	}
	
	public XModelObject getPage(String path) {
		path = toNavigationRulePathPart(path);
		XModelObject g = (XModelObject)pageItems.get(path);
		if(g == null) g = (XModelObject)targets.get(path);
		return g;
	}

	public void updatePages() {
		SeamPagesPageRefUpdateManager pu = SeamPagesPageRefUpdateManager.getInstance(diagram.getModel());
		pu.lock();
		XModelObject[] items = diagram.getChildren();
		for (int i = 0; i < items.length; i++) pu.updatePage(this, items[i]);
		pu.unlock();
	}
	
	public String findBestMatch(String viewId) {
		if(viewId == null || pageViewIds.contains(viewId)) {
			return viewId;
		}
		String best = viewId;
		int match = 0;
		for (String v: pageViewIds) {
			int i = v.indexOf('*');
			if(i < 0) continue;
			String head = v.substring(0, i);
			String tail = v.substring(i + 1);
			int m = head.length() + tail.length();
			if(m > viewId.length() || m <= match) continue;
			if(head.length() > 0 && !viewId.startsWith(head)) continue;
			if(tail.length() > 0 && !viewId.endsWith(tail)) continue;
			best = v;
			match = m;			
		}
		return best;
	}

	public static String toNavigationRulePathPart(String path) {
		return "" + encode(path);
	}
	
	public static String toFromViewId(String pathpart) {
		if(!pathpart.startsWith("rules:")) return decode(pathpart);
		pathpart = decode(pathpart.substring(6));
		int i = pathpart.lastIndexOf(':');
		return (i < 0) ? pathpart : pathpart.substring(0, i);
	}

	public static boolean isPattern(String path) {
		return path != null && (path.length() == 0 || path.indexOf('*') >= 0);
	}

	static String encode(String s) {
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if(c == '/') {
				result.append("#x");
			} else if(c == '#') {
				result.append("##");
			} else {
				result.append(c);
			}
		}
		return result.toString();
	}

	static String decode(String s) {
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if(c == '#') {
				char c1 = i + 1 < s.length() ? s.charAt(i + 1) : '\0';
				if(c1 == 'x') {
					result.append('/');
					i++;
				} else if(c1 == '#') {
					result.append("#");
					i++;
				} else {
					result.append("#");
				}
			} else {
				result.append(c);
			}
		}
		return result.toString();
	}

}
