/*******************************************************************************
*   Copyright 2012 Analog Devices, Inc.
*
*   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 com.analog.lyric.util.misc;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.UUID;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.model.core.FactorGraph;
import com.analog.lyric.dimple.model.core.FactorGraphIterables;
import com.analog.lyric.dimple.model.core.INameable;
import com.analog.lyric.dimple.model.core.INode;
import com.analog.lyric.dimple.model.core.Port;
import com.analog.lyric.dimple.model.factors.Factor;
import com.analog.lyric.dimple.model.variables.Discrete;


@Internal
public class FactorGraphDiffs
{
	private @Nullable INameable _a = null;
	private @Nullable INameable _b = null;
	private @Nullable ArrayList<INameable>	_childrenInANotB = null;
	private @Nullable ArrayList<INameable>	_childrenInBNotA = null;
	private @Nullable ArrayList<FactorGraphDiffs> _childDiffs = null;
	
	public FactorGraphDiffs()
	{
		this(null, null);
	}
	public FactorGraphDiffs(@Nullable INameable a, @Nullable INameable b)
	{
		_a = a;
		_b = b;
	}
	
	public @Nullable INameable getA()
	{
		return _a;
	}
	public @Nullable INameable getB()
	{
		return _a;
	}
	public @Nullable ArrayList<INameable> inANotB()
	{
		return _childrenInANotB;
	}
	public @Nullable ArrayList<INameable> inBNotA()
	{
		return _childrenInBNotA;
	}
	public @Nullable ArrayList<FactorGraphDiffs> childDiffs()
	{
		return _childDiffs;
	}

	static private String tabString(int numTabs)
	{
		String s = "";
		for(int i = 0; i < numTabs; ++i)
		{
			s += "\t";
		}
		return s;
	}
	
	static private String typeString(@Nullable INameable n)
	{
		String s = "";
		if(n instanceof FactorGraph)
		{
			s = "Graph   ";
		}
		else if(n instanceof Discrete)
		{
			s = "Variable";
		}
		else if(n instanceof Factor)
		{
			s = "Factor  ";
		}
		else if (n != null)
		{
			s = n.getClass().toString();
		}
		return s;
	}
	
	public String toString(int numTabs)
	{
		String s = "";

		int pathDBG = 0;
		try
		{
			//String tabString = tabString(numTabs);
			if(numTabs == 0)
			{
				pathDBG |= 0x00000001;
				s += "diffs ";
			}
			pathDBG |= 0x00000002;
			String aName = "none";
			String bName = "none";
			int inANotB = 0;
			int inBNotA = 0;
			int childDiffsCount = 0;
			String typeString = "none";
			final INameable a = _a;
			if(a != null)
			{
				aName = a.getQualifiedLabel();
				typeString = typeString(a);
			}
			final INameable b = _b;
			if(b != null)
			{
				bName = b.getQualifiedLabel();
				typeString = typeString(b);
			}
			final ArrayList<INameable> childrenInANotB = _childrenInANotB;
			if(childrenInANotB != null)
			{
				inANotB = childrenInANotB.size();
			}
			final ArrayList<INameable> childrenInBNotA = _childrenInBNotA;
			if(childrenInBNotA != null)
			{
				inANotB = childrenInBNotA.size();
			}
			final ArrayList<FactorGraphDiffs> childDiffs = _childDiffs;
			if(childDiffs != null)
			{
				childDiffsCount = childDiffs.size();
			}
			
			s += String.format("[%s]  a:[%s]  b:[%s]  inANotB:%d  inBNotA:%d  childDiffs:%d"
					,typeString
					,aName
					,bName
					,inANotB
					,inBNotA
					,childDiffsCount);
			pathDBG |= 0x00000004;
			String oneMoreTab = tabString(numTabs + 1);
			String twoMoreTabs = tabString(numTabs + 2);
			pathDBG |= 0x00000008;
			if(childrenInANotB != null)
			{
				pathDBG |= 0x00000010;
				s += String.format("\n%sIn A not B:\n", oneMoreTab);
				for(INameable n : childrenInANotB)
				{
					pathDBG |= 0x00000020;
					s += twoMoreTabs;
					s += "[";
					s += typeString(n);
					s += "] ";
					if(n != null)
					{
						s += n.getQualifiedLabel();
					}
					else
					{
						s += "none";
					}
					s += "\n";
				}
			}
			pathDBG |= 0x00000040;
			if(childrenInBNotA != null)
			{
				pathDBG |= 0x00000080;
				s += String.format("\n%sIn B not A:\n", oneMoreTab);
				for(INameable n : childrenInBNotA)
				{
					pathDBG |= 0x00000100;
					s += twoMoreTabs;
					s += "[";
					s += typeString(n);
					s += "] ";
					if(n != null)
					{
						s += n.getQualifiedLabel();
					}
					else
					{
						s += "none";
					}
					s += "\n";
				}
			}
			pathDBG |= 0x00000200;
			if(childDiffs != null)
			{
				pathDBG |= 0x00000400;
				s += String.format("\n%sChild diffs:\n", oneMoreTab);
				for(FactorGraphDiffs d : childDiffs)
				{
					pathDBG |= 0x00000800;
					s += twoMoreTabs;
					if(d != null)
					{
						//s += n.getQualifiedLabel();
						s += d.toString(numTabs + 1);
					}
					else
					{
						s += "none";
					}
					s += "\n";
				}
				pathDBG |= 0x00001000;
			}
			pathDBG |= 0x00002000;
		}
		catch(Exception e)
		{
			s += "\nException - " + e.toString() + " Path debug: " + pathDBG;
		}
		return s;
		
	}
	
	@Override
	public String toString()
	{
		return toString(0);
	}
	
	public void print()
	{
		print("");
	}
	public void print(String tag)
	{
		System.out.println(tag + " - " + toString());
	}
	public boolean noDiffs()
	{
		return  _childrenInANotB == null &&
				_childrenInBNotA == null &&
				_childDiffs == null;
	}
	private void addANotB(INameable node)
	{
		ArrayList<INameable> childrenInANotB = _childrenInANotB;
		if(childrenInANotB == null)
		{
			childrenInANotB = _childrenInANotB = new ArrayList<INameable>();
		}
		childrenInANotB.add(node);
		//System.out.println("--addANotB [" + node.getLabel() + "] [\n" + toString() + "\n]\n");
	}
	private void addBNotA(INameable node)
	{
		ArrayList<INameable> childrenInBNotA = _childrenInBNotA;
		if(childrenInBNotA == null)
		{
			childrenInBNotA = _childrenInBNotA = new ArrayList<INameable>();
		}
		childrenInBNotA.add(node);
		//System.out.println("--addBNotA [" + node.getLabel() + "] [\n" + toString() + "\n]\n");
	}
	private void addDifference(FactorGraphDiffs difference)
	{
		ArrayList<FactorGraphDiffs> childDiffs = _childDiffs;
		if(childDiffs == null)
		{
			childDiffs = _childDiffs = new ArrayList<FactorGraphDiffs>();
		}
		childDiffs.add(difference);
	}
	private void addDifference(INameable a, INameable b, @Nullable ArrayList<FactorGraphDiffs> childFactorGraphDiffs)
	{
		FactorGraphDiffs oneDiff = new FactorGraphDiffs();
		oneDiff._a = a;
		oneDiff._b = b;
		oneDiff._childDiffs = childFactorGraphDiffs;
		addDifference(oneDiff);
		//System.out.println("--addBNotA [" + a.getLabel() + "] [" + b.getLabel() + "]\n" + toString() + "\n]\n");
	}

	static private boolean CompareTwoNameables(INameable a, INameable b, boolean byName)
	{
		String aName = a.getName();
		String bName = b.getName();
		UUID aUUID = a.getUUID();
		UUID bUUID = b.getUUID();
		
		boolean bEquals =
			//identity same?
			(byName ?
					aName.equals(bName) :
					aUUID.equals(bUUID)
			) &&
			//if identity same, class same?
			a.getClass().equals(b.getClass());
		
		return bEquals;
	}
	static private boolean CompareTwoPorts(Port a, Port b, boolean byName)
	{
		INode aParent = a.getNode();
		INode aConnected = a.getSiblingNode();
		INode bParent = b.getNode();
		INode bConnected = b.getSiblingNode();
		
		boolean equal = CompareTwoNameables(aParent, bParent, byName) &&
		CompareTwoNameables(aConnected, bConnected, byName);
		
		return equal;
	}
	
	static private FactorGraphDiffs comparePorts(
			INameable a,
			INameable b,
			boolean quickExit,
			boolean byName)
	{
		INode ndA = (INode) a;
		INode ndB = (INode) b;
		ArrayList<Port> aPorts = new ArrayList<>(ndA.getPorts());
		ArrayList<Port> bPorts = new ArrayList<>(ndB.getPorts());

		FactorGraphDiffs diffs = new FactorGraphDiffs(a, b);
		
		//variables don't have an ordered set of ports like
		//factors, as they can 'accumulate' connections
		//whereas factors have all their connections added
		//exactly in addFactor
		//So for variables we must 'go looking'
		HashSet<Integer> bPortIndices = new HashSet<Integer>();
		for(int i = 0; i < bPorts.size(); ++i)
		{
			bPortIndices.add(i);
		}
		
		//a against b
		boolean exit = false;
		for(int i = 0; i < aPorts.size() && !exit; ++i)
		{
			boolean same = true;
			if(i < bPorts.size())
			{
				//For Factors, just go pairwise
				if(ndA instanceof Factor ||
				   ndB instanceof Factor)
				{
					same = CompareTwoPorts(aPorts.get(i), bPorts.get(i), byName);
					if(!same)
					{
						diffs.addANotB(aPorts.get(i).getSiblingNode());
						diffs.addBNotA(bPorts.get(i).getSiblingNode());
					}
				}
				else
				{
					//For variables, go 'looking'
					int match = -1;
					for(Integer bIdx : bPortIndices)
					{
						if(CompareTwoPorts(aPorts.get(i), bPorts.get(bIdx), byName))
						{
							match = bIdx;
							break;
						}
					}
					if(match == -1)
					{
						diffs.addANotB(aPorts.get(i).getSiblingNode());
						//Not strictly complete; if there's a mismatch, a
						//and b have same number of ports, there must also
						//be a port in B but not in A
						//But we don't bother to go look for that.
					}
					else
					{
						bPortIndices.remove(match);
					}
				}
			}
			else
			{
				same = false;
				diffs.addANotB(aPorts.get(i).getSiblingNode());
			}
			exit = quickExit && !same;
		}
		if(!exit && bPorts.size() > aPorts.size())
		{
			for(int i = aPorts.size(); i < bPorts.size() && !exit; ++i)
			{
				diffs.addBNotA(bPorts.get(i).getSiblingNode());
				exit = quickExit;
			}
		}

		return diffs;
	}

	static private void compareNameableMaps(FactorGraphDiffs diffs,
										 NameableMap aMap,
										 NameableMap bMap,
										 boolean quickExit,
										 boolean byName)
	{
		boolean someInANotB = false;
		
		//a against b
		for(INameable nA : aMap)
		{
			@Nullable INameable nB = null;
			if(byName)
			{
				nB = bMap.get(nA.getName());
			}
			else
			{
				nB = bMap.get(nA.getUUID());
			}

			
			if(nB == null || //in a, missing from b
			    //or in both places with same identity, but different class
			   !nB.getClass().equals(nA.getClass()))
			{
				someInANotB = true;
				diffs.addANotB(nA);
				if(quickExit)
				{
					break;
				}//else keep accumulating
			}
			//else in both; are they the same?
			else
			{
				FactorGraphDiffs oneDiff = null;
				if(nA instanceof FactorGraph)
				{
					oneDiff = getFactorGraphDiffsNoRootSearch(
							   (FactorGraph) nA,
							   (FactorGraph) nB,
							   quickExit,
							   byName);
				}
				else
				{
					oneDiff = comparePorts(
							nA,
							nB,
							quickExit,
							byName);
				}
				if(!oneDiff.noDiffs())
				{
					diffs.addDifference(oneDiff);
					if(quickExit)
					{
						break;
					}//else keep accumulating
				}//else same
			}//end check one nA vs one nB
		}//end loop comparing a list against b list
		
		//look for in b but not a
		if((someInANotB || aMap.size() != bMap.size()) &&
		   !quickExit)
		{
			for(INameable nB : bMap)
			{
				INameable nA = null;
				if(byName)
				{
					nA = aMap.get(nB.getName());
				}
				else
				{
					nA = aMap.get(nB.getUUID());
				}

				if(nA == null)
				{
					diffs.addBNotA(nB);
					if(quickExit)
					{
						break;
					}//else keep accumulating
				}//else both there - we must hace checked for node equality above
			}//end loop comparing b list against a list
		}//else everything happy or quick bomb out
	}

	static public FactorGraphDiffs	getFactorGraphDiffs(
			   FactorGraph a,
			   FactorGraph b,
			   boolean quickExit,
			   boolean byName)
	{
		return getFactorGraphDiffsNoRootSearch(
				   a.getRootGraph(),
				   b.getRootGraph(),
				   quickExit,
				   byName);
	}
	static public FactorGraphDiffs	getFactorGraphDiffsNoRootSearch(
								   FactorGraph a,
								   FactorGraph b,
								   boolean quickExit,
								   boolean byName)
	{
		FactorGraphDiffs diffs = new FactorGraphDiffs(a, b);
		
		String aName = a.getName();
		String bName = b.getName();
		UUID aUUID = a.getUUID();
		UUID bUUID = b.getUUID();
		
		//System.out.println("++getFactorGraphDiffs [" + a.getLabel() + "] [" + b.getLabel() + "]\n");
		
		boolean bEquals =
			byName ?
					aName.equals(bName) :
					aUUID.equals(bUUID);
		if(!bEquals)
		{
			diffs.addDifference(a, b, null);
		}
		
		if(!a.hasParentGraph())
		{
			@SuppressWarnings("all")
			NameableMap aMap = new NameableMap(FactorGraphIterables.boundary(a));

			@SuppressWarnings("all")
			NameableMap bMap = new NameableMap(FactorGraphIterables.boundary(b));
			
			compareNameableMaps(diffs,
							 aMap,
							 bMap,
							 quickExit,
							 byName);
		}
		
		if(diffs.noDiffs() || !quickExit)
		{
			@SuppressWarnings("all")
			NameableMap aMap
			= new NameableMap(a.getNodesTop()
				.values());

			@SuppressWarnings("all")
			NameableMap bMap
				= new NameableMap(b.getNodesTop()
					.values());

			compareNameableMaps(diffs,
					 aMap,
					 bMap,
					 quickExit,
					 byName);
		}
		if(diffs.noDiffs() || !quickExit)
		{
			@SuppressWarnings("all")
			NameableMap aMap
			= new NameableMap(a.getOwnedGraphs());

			@SuppressWarnings("all")
			NameableMap bMap
				= new NameableMap(b.getOwnedGraphs());

			compareNameableMaps(diffs,
					 aMap,
					 bMap,
					 quickExit,
					 byName);
		}
		//System.out.println("--getFactorGraphDiffs [\n" + diffs.toString() + "\n]\n");
		return diffs;
	}
}
