///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below.       //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,       //
// 2007, 2008, 2009, 2010, 2014, 2015 by Peter Spirtes, Richard Scheines, Joseph   //
// Ramsey, and Clark Glymour.                                                //
//                                                                           //
// This program is free software; you can redistribute it and/or modify      //
// it under the terms of the GNU General Public License as published by      //
// the Free Software Foundation; either version 2 of the License, or         //
// (at your option) any later version.                                       //
//                                                                           //
// This program is distributed in the hope that it will be useful,           //
// but WITHOUT ANY WARRANTY; without even the implied warranty of            //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             //
// GNU General Public License for more details.                              //
//                                                                           //
// You should have received a copy of the GNU General Public License         //
// along with this program; if not, write to the Free Software               //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA //
///////////////////////////////////////////////////////////////////////////////

package edu.cmu.tetrad.graph;

import edu.cmu.tetrad.graph.EdgeTypeProbability.EdgeType;
import edu.cmu.tetrad.util.TetradSerializable;

import java.awt.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Represents an edge node1 *-# node2 where * and # are endpoints of type
 * Endpoint--that is, Endpoint.TAIL, Endpoint.ARROW, or Endpoint.CIRCLE.
 * <p>
 * Note that because speed is of the essence, and Edge cannot be compared to an
 * object of any other type; this will throw an exception.
 *
 * @author Joseph Ramsey
 */
public class Edge implements TetradSerializable, Comparable {
	static final long serialVersionUID = 23L;

	public enum Property {
		dd, nl, pd, pl
	}

	private Node node1;
	private Node node2;
	private Endpoint endpoint1;
	private Endpoint endpoint2;

	// Usual coloring--set to something else for a special line color.
	private transient Color lineColor = null;

	private boolean bold = false;

	private List<Property> properties = new ArrayList<>();

    private List<EdgeTypeProbability> edgeTypeProbabilities = new ArrayList<>();

	// =========================CONSTRUCTORS============================//

	/**
	 * Constructs a new edge by specifying the nodes it connects and the
	 * endpoint types.
	 *
	 * @param node1
	 *            the first node
	 * @param node2
	 *            the second node _
	 * @param endpoint1
	 *            the endpoint at the first node
	 * @param endpoint2
	 *            the endpoint at the second node
	 */
	public Edge(Node node1, Node node2, Endpoint endpoint1, Endpoint endpoint2) {
		if (node1 == null || node2 == null) {
			throw new NullPointerException("Nodes must not be null. node1 = " + node1 + " node2 = " + node2);
		}

		if (endpoint1 == null || endpoint2 == null) {
			throw new NullPointerException("Endpoints must not be null.");
		}

		// Flip edges pointing left the other way.
		if (pointingLeft(endpoint1, endpoint2)) {
			this.node1 = node2;
			this.node2 = node1;
			this.endpoint1 = endpoint2;
			this.endpoint2 = endpoint1;
		} else {
			this.node1 = node1;
			this.node2 = node2;
			this.endpoint1 = endpoint1;
			this.endpoint2 = endpoint2;
		}
	}

	public Edge(Edge edge) {
		this(edge.node1, edge.node2, edge.endpoint1, edge.endpoint2);
		this.lineColor = edge.getLineColor();
	}

	/**
	 * Generates a simple exemplar of this class to test serialization.
	 */
	public static Edge serializableInstance() {
		return new Edge(GraphNode.serializableInstance(), GraphNode.serializableInstance(), Endpoint.ARROW,
				Endpoint.ARROW);
	}

	// ==========================PUBLIC METHODS===========================//

	/**
	 * @return the A node.
	 */
	public final Node getNode1() {
		return this.node1;
	}

	/**
	 * @return the B node.
	 */
	public final Node getNode2() {
		return this.node2;
	}

	/**
	 * @return the endpoint of the edge at the A node.
	 */
	public final Endpoint getEndpoint1() {
		return this.endpoint1;
	}

	/**
	 * @return the endpoint of the edge at the B node.
	 */
	public final Endpoint getEndpoint2() {
		return this.endpoint2;
	}

	public final void setEndpoint1(Endpoint e) {
		this.endpoint1 = e;
	}

	public final void setEndpoint2(Endpoint e) {
		this.endpoint2 = e;
	}

	/**
	 * @return the endpoint nearest to the given node.
	 * @throws IllegalArgumentException
	 *             if the given node is not along the edge.
	 */
	public final Endpoint getProximalEndpoint(Node node) {
		if (this.node1 == node) {
			return getEndpoint1();
		} else if (this.node2 == node) {
			return getEndpoint2();
		}

		return null;
	}

	/**
	 * @return the endpoint furthest from the given node.
	 * @throws IllegalArgumentException
	 *             if the given node is not along the edge.
	 */
	public final Endpoint getDistalEndpoint(Node node) {
		if (this.node1 == node) {
			return getEndpoint2();
		} else if (this.node2 == node) {
			return getEndpoint1();
		}

		return null;
	}

	/**
	 * Traverses the edge in an undirected fashion--given one node along the
	 * edge, returns the node at the opposite end of the edge.
	 */
	public final Node getDistalNode(Node node) {
		if (this.node1 == node) {
			return this.node2;
		}

		if (this.node2 == node) {
			return this.node1;
		}

		return null;
	}

	/**
	 * @return true just in case this edge is directed.
	 */
	public boolean isDirected() {
		return Edges.isDirectedEdge(this);
	}

	/**
	 * @return true just in case the edge is pointing toward the given node--
	 *         that is, x --> node or x o--> node.
	 */
	public boolean pointsTowards(Node node) {
		Endpoint proximal = getProximalEndpoint(node);
		Endpoint distal = getDistalEndpoint(node);
		return (proximal == Endpoint.ARROW && (distal == Endpoint.TAIL || distal == Endpoint.CIRCLE));
	}

	/**
	 * @return the edge with endpoints reversed.
	 */
	public Edge reverse() {
		return new Edge(getNode2(), getNode1(), getEndpoint1(), getEndpoint2());
	}

	/**
	 * Produces a string representation of the edge.
	 */
	public final String toString() {
		StringBuilder buf = new StringBuilder();

		Endpoint endptTypeA = getEndpoint1();
		Endpoint endptTypeB = getEndpoint2();

		buf.append(getNode1());
		buf.append(" ");

		if (endptTypeA == Endpoint.TAIL) {
			buf.append("-");
		} else if (endptTypeA == Endpoint.ARROW) {
			buf.append("<");
		} else if (endptTypeA == Endpoint.CIRCLE) {
			buf.append("o");
		}

		buf.append("-");

		if (endptTypeB == Endpoint.TAIL) {
			buf.append("-");
		} else if (endptTypeB == Endpoint.ARROW) {
			buf.append(">");
		} else if (endptTypeB == Endpoint.CIRCLE) {
			buf.append("o");
		}

		buf.append(" ");
		buf.append(getNode2());

        // Bootstrapping edge type distribution
        List<EdgeTypeProbability> edgeTypeDist = getEdgeTypeProbabilities();
        if(edgeTypeDist.size() > 0){
            buf.append(" ");

            String n1 = getNode1().getName();
			String n2 = getNode2().getName();
			if(n1.compareTo(n2) > 0) {// Sort node's names
				n1 = getNode2().getName();
				n2 = getNode1().getName();
			}
			
            for (int i = 0; i < edgeTypeDist.size(); i++) {
            	EdgeTypeProbability etp = edgeTypeDist.get(i);
            	double prob = etp.getProbability();
            	if(prob > 0) {
        			String _type = "" + etp.getEdgeType();
        			switch (etp.getEdgeType()) {
        			case nil:
        				_type = "no edge";
        				break;
        			case ta:
        				_type = "-->";
        				break;
        			case at:
        				_type = "<--";
        				break;
        			case ca:
        				_type = "o->";
        				break;
        			case ac:
        				_type = "<-o";
        				break;
        			case cc:
        				_type = "o-o";
        				break;
        			case aa:
        				_type = "<->";
        				break;
        			case tt:
        				_type = "---";
        				break;
        			default:
        				break;
        			}
        			
        			if(etp.getEdgeType() != EdgeType.nil) {
        				_type = n1 + " " + _type + " " + n2;
        			}
        			List<Property> properties = etp.getProperties();
        			if(properties != null && properties.size() > 0) {
        	        	for(Property property : properties) {
        	        		_type = _type + " " + property.toString();
        	        	}
        	        }
        			buf.append("[" + _type + "]:" + String.format("%.4f", etp.getProbability()) + ";");
        			
            	}
            }
        }
        
        List<Property> properties = getProperties();
        if(properties != null && properties.size() > 0) {
        	for(Property property : properties) {
        		buf.append(" ");
        		buf.append(property.toString());
        	}
        }
        
		return buf.toString();
	}

	public final int hashCode() {
		return node1.hashCode() + node2.hashCode();
	}

	/**
	 * Two edges are equal just in case they connect the same nodes and have the
	 * same endpoints proximal to each node.
	 */
	public final boolean equals(Object o) {
		if (o == this)
			return true;
		if (!(o instanceof Edge))
			return false;

		Edge edge = (Edge) o;

		if (NodeEqualityMode.getEqualityType() == NodeEqualityMode.Type.OBJECT) {
			Node node1 = getNode1();
			Node node2 = getNode2();
			Node node1b = edge.getNode1();
			Node node2b = edge.getNode2();

			Endpoint end1 = getEndpoint1();
			Endpoint end2 = getEndpoint2();
			Endpoint end1b = edge.getEndpoint1();
			Endpoint end2b = edge.getEndpoint2();

			boolean equal;

			// if ((node1 == node1b && node2 == node2b) || (node1 == node2b &&
			// node2 == node1b)) {
			// System.out.println();
			// }

			if (node1 == node1b && node2 == node2b) {
				equal = end1 == end1b && end2 == end2b;
			} else
				equal = node1 == node2b && node2 == node1b && end1 == end2b && end2 == end1b;

			return equal;
		} else if (NodeEqualityMode.getEqualityType() == NodeEqualityMode.Type.NAME) {
			String name1 = getNode1().getName();
			String name2 = getNode2().getName();
			String name1b = edge.getNode1().getName();
			String name2b = edge.getNode2().getName();

			Endpoint end1 = getEndpoint1();
			Endpoint end2 = getEndpoint2();
			Endpoint end1b = edge.getEndpoint1();
			Endpoint end2b = edge.getEndpoint2();

			if (name1.equals(name1b) && name2.equals(name2b)) {
				return end1 == end1b && end2 == end2b;
			} else
				return name1.equals(name2b) && name2.equals(name1b) && end1 == end2b && end2 == end1b;
		} else {
			throw new IllegalStateException();
		}
	}

	public int compareTo(Object o) {
		Edge _edge = (Edge) o;

		int comp1 = getNode1().compareTo(_edge.getNode1());

		if (comp1 != 0) {
			return comp1;
		}

		return getNode2().compareTo(_edge.getNode2());
	}

	// ===========================PRIVATE METHODS===========================//

	private boolean pointingLeft(Endpoint endpoint1, Endpoint endpoint2) {
		return (endpoint1 == Endpoint.ARROW && (endpoint2 == Endpoint.TAIL || endpoint2 == Endpoint.CIRCLE));
	}

	/**
	 * Adds semantic checks to the default deserialization method. This method
	 * must have the standard signature for a readObject method, and the body of
	 * the method must begin with "s.defaultReadObject();". Other than that, any
	 * semantic checks can be specified and do not need to stay the same from
	 * version to version. A readObject method of this form may be added to any
	 * class, even if Tetrad sessions were previously saved out using a version
	 * of the class that didn't include it. (That's what the
	 * "s.defaultReadObject();" is for. See J. Bloch, Effective Java, for help.
	 *
	 * @throws java.io.IOException
	 * @throws ClassNotFoundException
	 */
	private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
		s.defaultReadObject();

		if (node1 == null) {
			throw new NullPointerException();
		}

		if (node2 == null) {
			throw new NullPointerException();
		}

		if (endpoint1 == null) {
			throw new NullPointerException();
		}

		if (endpoint2 == null) {
			throw new NullPointerException();
		}
	}

	public boolean isNull() {
		return endpoint1 == Endpoint.NULL && endpoint2 == Endpoint.NULL;
	}

	public Color getLineColor() {
		return this.lineColor;
	}

	public void setLineColor(Color lineColor) {
		if (lineColor != null) {
			this.lineColor = lineColor;
		}
	}

	public boolean isBold() {
		return bold;
	}

	public void setBold(boolean bold) {
		this.bold = bold;
	}

	public void addProperty(Property property) {
		if (!properties.contains(property)) {
			this.properties.add(property);
		}
	}

	public void removeProperty(Property property) {
		this.properties.remove(property);
	}

	public ArrayList<Property> getProperties() {
		return new ArrayList<>(this.properties);
	}

	public void addEdgeTypeProbability(EdgeTypeProbability prob) {
		if (!edgeTypeProbabilities.contains(prob)) {
			this.edgeTypeProbabilities.add(prob);
		}
	}

	public void removeEdgeTypeProbability(EdgeTypeProbability prob) {
		this.edgeTypeProbabilities.remove(prob);
	}

	public List<EdgeTypeProbability> getEdgeTypeProbabilities() {
		return edgeTypeProbabilities;
	}
}
