///////////////////////////////////////////////////////////////////////////////
// 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.session;

import edu.cmu.tetrad.util.TetradSerializable;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * <p>Stores a directed graph over models of specific types, where the possible
 * parent relationships between the models are given in the constructors of the
 * model classes themselves. For instance, if a class Model1 has only this
 * constructor:</p> </p> <code>public Model1(Model2 x, Model3 y)... </code> </p>
 * <p>then if a SessionNode is constructed with Model.class as argument, it will
 * configure itself as a SessionNode requiring two parents, one capable of
 * implementing models of type Model2 and a second capable of implementing
 * models of type Model3. If SessionNodes capable of implementing models of
 * types Model2 and Model3 are available as parents of Model1, a new object of
 * type Model1 can be constructed using them. If Model1 has more than one
 * constructor, then there is more than one possible set of parents that can be
 * used to construct it. SessionNodes can also support more than one possible
 * type of model. A SessionNode, for instance, can support the construction of
 * graphs in general, even if different graphs are implemented using different
 * classes. The SessionNode can keep track of what its parents are and therefore
 * which of its possible models it's capable of constructing. </p> <p>The
 * Session itself keeps track of which nodes are in the session and manages
 * adding and removing nodes. Nodes that are added to the session must be
 * freshly constructed. This constraint eliminates a number of problems that
 * might otherwise exist if interconnected SessionNodes were permitted to
 * participate in more than one Session. If the addNode method is called with a
 * node that is not in the freshly constructed state (either because it was
 * actually just constructed or because the <code>reset</code> method was just
 * called on the node), an IllegalArgumentException is thrown./p> </p> <p>When a
 * node is removed from a session, all of its connections to other objects are
 * eliminated and its models destroyed. This has consequences for other objects,
 * since destroying the model of a session node may result in the destruction of
 * models downstream and the elimination of parent/child relationships between
 * nodes is mutual.</p> </p> <p>The Session organizes events coming from the
 * nodes in the session so that a listener to the Session receives all events
 * from the Session. This is convenience service so that listeners do not need
 * to pay attention to all of the different nodes in the session individually.
 * See <code>SessionEvent</code> for the types of events that are sent.</p> </p>
 * <p>It is intended for the Session to be serializable. For the Session and
 * SessionNode classes, this can be checked directly in unit tests. For the
 * various models that the Session can construct, this has to be tested
 * separately.</p>
 *
 * @author Joseph Ramsey
 * @see SessionNode
 * @see SessionListener
 * @see SessionAdapter
 * @see SessionEvent
 */
public final class Session implements TetradSerializable {
    static final long serialVersionUID = 23L;

    /**
     * The name of the session.
     *
     * @serial Can't be null.
     */
    private String name;

    /**
     * The session nodes, stored as a Set of nodes.
     *
     * @serial Can't be null.
     */
    private List<SessionNode> nodes = new LinkedList<>();

    /**
     * Notes when the model has changed. Should be false at time of
     * deserialization.
     */
    private transient boolean sessionChanged = true;

    /**
     * True iff the session is new. Should be false at time of deserialization.
     */
    private transient boolean newSession = true;

    /**
     * Convenience class for firing SessionEvents.
     */
    private transient SessionSupport sessionSupport;

    /**
     * Handles incoming session events, basically by redirecting to any
     * listeners of this session.
     */
    private transient SessionHandler sessionHandler;

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

    /**
     * Constructs a new session with the given name. (The name cannot be null.)
     */
    public Session(String name) {
        setName(name);
    }

    /**
     * Generates a simple exemplar of this class to test serialization.
     */
    public static Session serializableInstance() {
        return new Session("X");
    }

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

    /**
     * Sets the name.
     */
    public final void setName(String name) {
        if (name == null) {
            throw new NullPointerException("Name must not be null.");
        }

        this.name = name;
    }

    /**
     * Gets the name.
     */
    public String getName() {
        return this.name;
    }

    /**
     * <p>Adds the given node to the session, provided the node is in a freshly
     * created state.</p>
     *
     * @throws NullPointerException     if the node is null.
     * @throws IllegalArgumentException if the node is not in a freshly created
     *                                  state. There are two ways to put a
     *                                  SessionNode into a freshly created
     *                                  state. One is to freshly create it,
     *                                  using one of the constructors. The other
     *                                  was is to call the <code>reset</code>
     *                                  method on the SessionNode.
     * @see edu.cmu.tetrad.session.SessionNode#isFreshlyCreated
     * @see edu.cmu.tetrad.session.SessionNode#resetToFreshlyCreated
     */
    public void addNode(SessionNode node) {
        if (node == null) {
            throw new NullPointerException("Node cannot be null.");
        }

        if (!node.isFreshlyCreated()) {
            throw new IllegalArgumentException("Node must be freshly " +
                    "created in order to be added to a session: " + node);
        }

        // Causing templates not to work sometimes. Unnecessary. jdramsey 6/5/2015
//        if (existsNodeByName(node.getDisplayName())) {
//            throw new IllegalArgumentException(
//                    "Attempt to add node to the session with duplicate name: " +
//                            node.getDisplayName());
//        }

        this.nodes.add(node);
        node.addSessionListener(getSessionHandler());
        getSessionSupport().fireNodeAdded(node);
    }

    /**
     * Adds a list of nodes to the session. Each item in the list must be a
     * SessionNode, and none of them may have a name that already exists in the
     * session. Upon being added to the session, if any node has a parent that
     * is not in the list, the parent is removed, the node's model is destroyed,
     * and any models downstream are destroyed as well. Any children not in the
     * list are removed. Also, any listeners that are not SessionNodes are
     * removed from each node.
     */
    public void addNodeList(List<SessionNode> nodes) {

        if (nodes == null) {
            throw new NullPointerException(
                    "The list of nodes must not be " + "null.");
        }

        // Verify that all of the nodes are SessionNodes.
        for (int i = 0; i < nodes.size(); i++) {
            if (nodes.get(i) == null) {
                throw new IllegalArgumentException(
                        "The object at index " + i + " is null.");
            }
        }

        // Check to make sure none of the nodes has a name already in the
        // session.
        for (SessionNode node : nodes) {
            if (existsNodeByName(node.getDisplayName())) {
                throw new IllegalArgumentException(
                        "Attempt to add node to the " +
                                "session with duplicate " + "name: " +
                                node.getDisplayName());
            }
        }

        // Add the nodes, removing any parents or children not in the list.
        for (int i = 0; i < nodes.size(); i++) {
            SessionNode node = nodes.get(i);
            node.restrictConnectionsToList(nodes);
            node.restrictListenersToSessionNodes();
            this.nodes.add(node);
            node.addSessionListener(getSessionHandler());
            getSessionSupport().fireNodeAdded(node);
        }
    }

    /**
     * <p>Removes the given node from the session, removing any connectivity the
     * node might have to other objects.</p>
     *
     * @param node the SessionNode to be removed.
     * @throws IllegalArgumentException if the specified node is not in the
     *                                  session.
     * @see edu.cmu.tetrad.session.SessionNode#resetToFreshlyCreated
     */
    public void removeNode(SessionNode node) {
        if (nodes.contains(node)) {
            node.resetToFreshlyCreated();
            nodes.remove(node);
            node.removeSessionListener(getSessionHandler());
            getSessionSupport().fireNodeRemoved(node);
        } else {
            throw new IllegalArgumentException(
                    "Node doesn't exist in" + "graph: " + node);
        }
    }

    /**
     * @return the getModel set of session nodes.
     */
    public Set<SessionNode> getNodes() {
        return new HashSet<>(nodes);
    }

    /**
     * Removes all nodes.
     */
    public void clearNodes() {

        // Use the removeNode method to make sure events are fired.
        Set<SessionNode> _nodes = new HashSet<>(this.nodes);

        for (SessionNode node : _nodes) {
            removeNode(node);
        }
    }

    public boolean contains(SessionNode node) {
        return this.nodes.contains(node);
    }

    /**
     * Adds a session listener.
     */
    public void addSessionListener(SessionListener l) {
        getSessionSupport().addSessionListener(l);
    }

    /**
     * Removes a session listener.
     */
    public void removeSessionListener(SessionListener l) {
        getSessionSupport().removeSessionListener(l);
    }

    //=====================PACKAGE PROTECTED METHODS=====================//

    /**
     * Indirect reference to session support to avoid saving out any listeners
     * during serialization.
     */
    SessionSupport getSessionSupport() {

        if (this.sessionSupport == null) {
            this.sessionSupport = new SessionSupport(this);

            for (SessionNode node : this.nodes) {
                node.addSessionListener(getSessionHandler());
            }
        }

        return this.sessionSupport;
    }

    /**
     * Indirect reference to session handler to avoid saving out listeners
     * during serialization.
     */
    SessionHandler getSessionHandler() {
        if (this.sessionHandler == null) {
            this.sessionHandler = new SessionHandler();
        }

        return this.sessionHandler;
    }

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

    /**
     * @return true iff a node exists in the session with the given name.
     */
    private boolean existsNodeByName(String name) {
        if (name == null) {
            return false;
        }

        for (SessionNode node : getNodes()) {
            if (name.equals(node.getDisplayName())) {
                return true;
            }
        }
        return false;
    }

    public boolean isSessionChanged() {
        return sessionChanged;
    }

    public void setSessionChanged(boolean sessionChanged) {
        this.sessionChanged = sessionChanged;
    }

    public boolean isNewSession() {
        return newSession;
    }

    public void setNewSession(boolean newSession) {
        this.newSession = newSession;
    }

    //=========================== MEMBER CLASSES =========================//

    /**
     * Handles <code>SessionEvent</code>s. Hides the handling of these from the
     * API.
     */
    private class SessionHandler extends SessionAdapter {

        /**
         * This method is called when a node is added.
         */
        public void nodeAdded(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * This method is called when a node is removed.
         */
        public void nodeRemoved(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * This method is called when a parent is added.
         */
        public void parentAdded(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * This method is called when a parent is removed.
         */
        public void parentRemoved(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * This method is called when a model is created for a node.
         */
        public void modelCreated(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * This method is called when a model is destroyed for a node.
         */
        public void modelDestroyed(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event);
            setSessionChanged(true);
        }

        /**
         * Relays addingEdge events up the chain, without changing their
         * source.
         */
        public void addingEdge(SessionEvent event) {
            getSessionSupport().fireSessionEvent(event, false);
        }
    }

    public boolean isEmpty() {
        return nodes.isEmpty();
    }

    /**
     * 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 (name == null) {
            throw new NullPointerException();
        }

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

        sessionChanged = false;
        newSession = false;
    }
}





