/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.graph;

import java.awt.Rectangle;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import net.sf.jaxodraw.object.JaxoHandle;
import net.sf.jaxodraw.object.JaxoList;
import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectList;
import net.sf.jaxodraw.object.group.JaxoGroup;
import net.sf.jaxodraw.object.text.JaxoLatexText;
import net.sf.jaxodraw.object.text.JaxoPSText;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.JaxoInfo;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/**
 * A JaxoGraph is an extended version of a JaxoSaveGraph. In addition to the
 * list of JaxoObjects, the latex packages and the description of the graph,
 * it contains some information about how the graph is presented on the screen.
 * Note that the information in JaxoGraph is not restored when saving and opening graphs.
 * Only JaxoSaveGraph is serializable.
 *
 * @since 2.0
 */
public class JaxoGraph {
    private final JaxoSaveGraph saveGraph;
    private String saveFileName;
    private boolean graphIsSaved = true;

    /** Creates a new JaxoGraph with empty list of objects, empty description
     * and empty saveFileName.
     */
    public JaxoGraph() {
        this(new JaxoSaveGraph(), "");
    }

    /** Creates a new JaxoGraph with the given list of objects,
     * empty description and empty saveFileName.
     * @param list The new list of objects. Not null.
     */
    public JaxoGraph(final JaxoList<JaxoObject> list) {
        this(new JaxoSaveGraph(list), "");
    }

    /** Creates a new JaxoGraph with the given list of objects and description
     * and empty saveFileName.
     * @param list The new list of objects. Not null.
     * @param describe String which decsribes this JaxoGraph. May be null.
     */
    public JaxoGraph(final JaxoList<JaxoObject> list, final String describe) {
        this(new JaxoSaveGraph(list, describe), "");
    }

    /** Creates a new JaxoGraph with the given list of objects, description
     * and saveFileName.
     * @param list The new list of objects. Not null.
     * @param describe String which describes this JaxoGraph. May be null.
     * @param saveFile String holding the file name for saving. Not null.
     */
    public JaxoGraph(final JaxoList<JaxoObject> list, final String describe, final String saveFile) {
        this(new JaxoSaveGraph(list, describe), saveFile);
    }

    // private constructor
    private JaxoGraph(final JaxoSaveGraph graph, final String name) {
        if (name == null) {
            throw new IllegalArgumentException("filename cannot be null!");
        }

        this.saveGraph = graph;
        saveGraph.setJaxoDrawVersion(JaxoInfo.VERSION_NUMBER);
        this.saveFileName = name;
    }

    // methods to get/set the variables

    /** Returns the JaxoSaveGraph (i.e., the current list of objects and
     * the current description) of the current JaxoGraph.
     * @return The JaxoSaveGraph of the current JaxoGraph. Not null.
     */
    public final JaxoSaveGraph getSaveGraph() {
        return saveGraph;
    }

    /** Sets the JaxoSaveGraph (i.e., the current list of objects and
     * the current description) of the current JaxoGraph.
     * @param newSaveGraph The new JaxoSaveGraph. Not null.
     */
    public final void setSaveGraph(final JaxoSaveGraph newSaveGraph) {
        saveGraph.clearAll();
        saveGraph.setPackageList(newSaveGraph.getPackageList());
        saveGraph.setDescription(newSaveGraph.getDescription());
        saveGraph.setObjectList(newSaveGraph.getObjectList());
        // don't set Jaxodraw version!
    }

    /** Returns the list of JaxoObjects.
     * @return The list of JaxoObjects. Not null.
     */
    public final JaxoList<JaxoObject> getObjectList() {
        return saveGraph.getObjectList();
    }

    /** Sets the list of objects of this JaxoGraph to list and
     * flags the JaxoGraph as not saved.
     * @param list The list of JaxoObjects. Not null.
     */
    public final void setObjectList(final JaxoList<JaxoObject> list) {
        saveGraph.setObjectList(list);
        setSaved(false);
    }

    /** Returns the list of Latex packages.
     * @return The list of Latex packages.
     */
    public final List<String> getPackageList() {
        return saveGraph.getPackageList();
    }

    /** Sets the list of Latex packages of this JaxoGraph to list and
     * flags the JaxoGraph as not saved.
     * @param list The list of Latex packages. Not null.
     */
    public final void setPackageList(final List<String> list) {
        saveGraph.setPackageList(list);
        setSaved(false);
    }

    /** Sets the description text of this JaxoGraph and
     * flags the JaxoGraph as not saved.
     * @param describe The description of this JaxoGraph. May be null.
     */
    public final void setDescription(final String describe) {
        final String old = saveGraph.getDescription();
        final boolean equal = (old == null) ? describe == null : old.equals(describe);

        if (!equal) {
            saveGraph.setDescription(describe);
            setSaved(false);
        }
    }

    /** Returns the description text of this JaxoGraph.
     * @return The decription of this JaxoGraph. May be null.
     */
    public final String getDescription() {
        return saveGraph.getDescription();
    }

    /** Indicates whether the current JaxoGraph contains unsaved changes.
     * @return True if the Current JaxoGraph is saved.
     */
    public final boolean isSaved() {
        return graphIsSaved;
    }

    /** Declares the current JaxoGraph as saved or unsaved,
     * according to the boolean variable isSaved.
     * @param isSaved Boolean variable to indicate whether the current
     * JaxoGraph is saved or not.
     */
    public final void setSaved(final boolean isSaved) {
        this.graphIsSaved = isSaved;
    }

    /** Sets the default file name for save operations.
     * @param fileName The file name to be used for saving. Not null.
     */
    public final void setSaveFileName(final String fileName) {
        if (fileName == null) {
            throw new IllegalArgumentException("filename cannot be null!");
        }

        this.saveFileName = fileName;
    }

    /** Returns the current default save file name.
     * This never returns null, at most an empty string.
     * @return The current default save file name.
     */
    public final String getSaveFileName() {
        return saveFileName;
    }

    /** Returns the JaxoObject at position i of the object list.
     * @param i The index of the JaxoObject to be returned.
     * @return The JaxoObject at position i of the object list.
     */
    public final JaxoObject listElementAt(final int i) {
        return saveGraph.listElementAt(i);
    }

    /** Returns the size of the current object list, i.e.,
     * the number of objects it contains.
     * @return The number of objects currently in the object list.
     */
    public final int listSize() {
        return saveGraph.listSize();
    }

    // methods that modify the objectList

    /** Puts the specified JaxoObject into the foreground, i.e.,
     * to the last position in the object list. If the JaxoGraph was
     * modified by this operation, flags it as not saved.
     * @param object The JaxoObject to be put into the foreground.
     */
    public final void foreground(final JaxoObject object) {
        final boolean isLastElement = saveGraph.foreground(object);

        if (!isLastElement) {
            setSaved(false);
        }
    }

    /** Move the object at 'index' to 'newIndex', leaving all
     * other elements in order.
     * @param index The index of the object to move.
     * @param newIndex The new index of the object.
     */
    public final void move(final int index, final int newIndex) {
        saveGraph.move(index, newIndex);

        if (index != newIndex) {
            setSaved(false);
        }
    }

    /**
     * Replace 'old' by 'o'.
     *
     * @param old The object to replace. If this is not contained in this
     *      graph, the graph is not modified.
     * @param o The new object. Must not be null.
     */
    public final void replace(final JaxoObject old, final JaxoObject o) {
        if (!o.isCopy(old)) {
            final int index = saveGraph.getObjectList().indexOf(old);

            if (index != -1) {
                saveGraph.getObjectList().set(index, o);
                setSaved(false);
            }
        }
    }

    /** Puts the specified JaxoObject into the background, i.e.,
     * to the first position in the object list. If the JaxoGraph was
     * modified by this operation, flags it as not saved.
     * @param object The JaxoObject to be put into the background.
     */
    public final void background(final JaxoObject object) {
        final boolean isFirstElement = saveGraph.background(object);

        if (!isFirstElement) {
            setSaved(false);
        }
    }

    /** Removes the specified JaxoObject from the list of objects
     * and flags the JaxoGraph as not saved (if it has been modified).
     * @param object The JaxoObject to be removed from the list.
     */
    public final void delete(final JaxoObject object) {
        final boolean modified = saveGraph.delete(object);
        if (modified) {
            setSaved(false);
        }
    }

    /** Removes all marked JaxoObjects from the list of objects
     * and flags the JaxoGraph as not saved (if it has been modified).
     */
    public final void deleteMarkedObjects() {
        final boolean modified = saveGraph.deleteMarkedObjects();
        if (modified) {
            setSaved(false);
        }
    }

    /** Removes the specified JaxoObjects from the list of objects
     * and flags the JaxoGraph as not saved (if it has been modified).
     * @param objects Collection of JaxoObjects to be removed from the list.
     */
    public final void deleteAll(final Collection<JaxoObject> objects) {
        final boolean modified = saveGraph.deleteAll(objects);
        if (modified) {
            setSaved(false);
        }
    }

    /** Adds the specified JaxoObject to the list of objects by appending it
     * and flags the JaxoGraph as not saved (if it has been modified).
     * @param object The JaxoObject to be added to the list.
     */
    public final void addObject(final JaxoObject object) {
        final boolean modified = saveGraph.addObject(object);
        if (modified) {
            setSaved(false);
        }
    }

    /** Inserts a JaxoObject into the list of objects at a specified position
     *  and flags the JaxoGraph as not saved (if it has been modified).
     * @param index The position where the object has to be appended
     * @param object The JaxoObject to be added to the list.
     */
    public final void addObject(final int index, final JaxoObject object) {
        final boolean modified = saveGraph.addObject(index, object);
        if (modified) {
            setSaved(false);
        }
    }

    /** Removes all JaxoObjects from the list of objects.
     * If the JaxoGraph was modified by this operation, flags it as not saved.
     */
    public final void clear() {
        final boolean listIsEmpty = saveGraph.clear();
        if (!listIsEmpty) {
            setSaved(false);
        }
    }

    /** Removes all JaxoObjects from the list of objects and sets the
     * saveFileName to a string of size zero.
     * If the JaxoGraph was modified by this operation, flags it as not saved.
     */
    public final void clearAll() {
        final boolean graphNotModified = saveGraph.clearAll();
        final boolean fileNameEmpty = (saveFileName.length() == 0);

        if (!graphNotModified || !fileNameEmpty) {
            setSaveFileName("");
            setSaved(false);
        }
    }

    // other methods

    /** Returns the smallest bounding box that contains all the JaxoObjects
     * of this JaxoGraph.
     * @return A Rectangle holding the bounds of the current JaxoGraph,
     * or null if the graph is empty (ie contains no objects).
     */
    public final Rectangle getBounds() {
        return saveGraph.getBounds();
    }

    /** Returns the smallest bounding box that contains all the JaxoObjects
     * of this JaxoGraph except the ones given in 'except'.
     * @param except objects to exclude.
     * @return A Rectangle holding the bounds of the current JaxoGraph,
     * or null if the graph is (effectively empty (ie contains no objects).
     */
    public final Rectangle getBoundsExcept(final Collection<JaxoObject> except) {
        return saveGraph.getBoundsExcept(except);
    }

    /** Intersection of bounding box with given Rectangle.
     * This returns null if either any of the two rectangles are null,
     * or if they do not intersect.
     * @param inside The Rectangle to intersect with.
     * @return The intersection.
     * @see #getBounds()
     */
    public final Rectangle getBounds(final Rectangle inside) {
        return saveGraph.getBounds(inside);
    }

    /** Returns a copy of the specified JaxoList.
     * @param v A JaxoList to be duplicated.
     * @return A copy of v.
     */
    public static final JaxoList<JaxoObject> copyFrom(final JaxoList<JaxoObject> v) {
        final JaxoList<JaxoObject> temp = new JaxoObjectList<JaxoObject>(v.size());

        for (int i = 0; i < v.size(); i++) {
            final JaxoObject cur = v.get(i);
            temp.add(cur.copy());
        }

        return temp;
    }

    /** Checks if there are PSText objects present in this current graph.
     * @return True, if there are PSTexts, false otherwise.
     */
    public final boolean containsPSText() {
        return containsInstanceOf(JaxoPSText.class);
    }

    /** Checks if there are LatexText objects present in this current graph.
     * @return True, if there are LatexTexts, false otherwise.
     */
    public final boolean containsLatexText() {
        return containsInstanceOf(JaxoLatexText.class);
    }

    /**
     * Checks if there are instances if the given Class present in this graph.
     *
     * @param clazz a Class to look for.
     * @return True if the graph contains any Object ob for which
     *      clazz.isInstance(ob) returns true.
     */
    public final boolean containsInstanceOf(final Class<?> clazz) {
        boolean contains = false;
        final JaxoList<JaxoObject> templist = getObjectList();

        for (int j = 0; j < templist.size(); j++) {
            final JaxoObject ob = templist.get(j);

            if (clazz.isInstance(ob)) {
                contains = true;

                break;
            } else if (ob instanceof JaxoGroup) {
                contains = ((JaxoGroup) ob).containsInstanceOf(clazz);
                if (contains) {
                    break;
                }
            }
        }

        return contains;
    }

    /** Checks if there are marked Group objects present in the current selection.
     * @return True, if there are marked groups, false otherwise.
     */
    public final boolean containsMarkedGroups() {
        boolean contains = false;
        final JaxoList<JaxoObject> templist = getObjectList();

        for (int j = 0; j < templist.size(); j++) {
            final JaxoObject ob = templist.get(j);

            if (ob instanceof JaxoGroup && ob.isMarked()) {
                contains = true;

                break;
            }
        }

        return contains;
    }

    /** Checks if there are Group objects present in this graph.
     * @return True, if there are groups, false otherwise.
     */
    public final boolean containsGroups() {
        boolean contains = false;
        final JaxoList<JaxoObject> templist = getObjectList();

        for (int j = 0; j < templist.size(); j++) {
            final JaxoObject ob = templist.get(j);

            if (ob instanceof JaxoGroup) {
                contains = true;

                break;
            }
        }

        return contains;
    }

    /** Checks if there are marked objects present in this graph.
     * @return True, if there is at least one marked object in this graph, false otherwise.
     */
    public final boolean containsMarkedObjects() {
        final JaxoList<JaxoObject> templist = getObjectList();

        for (int j = 0; j < templist.size(); j++) {
            final JaxoObject ob = templist.get(j);

            if (ob.isMarked()) {
                return true;
            }
        }

        return false;
    }

    /** All marked objects of the graph, in the order of the graph.
     *  The result is mutable and free.
     * @return A JaxoList of all marked objects.
     */
    public JaxoList<JaxoObject> getMarkedObjects() {
        final JaxoList<JaxoObject> result = new JaxoObjectList<JaxoObject>(5);

        for (final Iterator<JaxoObject> i = getObjectList().iterator(); i.hasNext();) {
            final JaxoObject o = i.next();

            if (o.isMarked()) {
                result.add(o);
            }
        }

        return result;
    }

    /** Copies of the marked objects of the graph, in the order of the graph.
     *  The result is mutable and free.
     * @return A JaxoList of all marked objects.
     */
    public JaxoList<JaxoObject> getCopyOfMarkedObjects() {
        final JaxoList<JaxoObject> result = new JaxoObjectList<JaxoObject>(5);

        for (final Iterator<JaxoObject> i = getObjectList().iterator(); i.hasNext();) {
            final JaxoObject o = i.next();

            if (o.isMarked()) {
                result.add(o.copy());
            }
        }

        return result;
    }

    /** Determines if at least a certain number of objects are marked
     *  (<code>containsMarkedObjects() == hasMoreMarkedObjectsThan(0)</code>).
     * @param less The lower bound to test.
     * @return True if at least 'less + 1' (less >= 0) objects of the graph are 'marked'.
     */
    public boolean hasMoreMarkedObjectsThan(final int less) {
        int count = 0;

        for (final Iterator<JaxoObject> i = getObjectList().iterator(); i.hasNext();) {
            final JaxoObject o = i.next();

            if (o.isMarked()) {
                count++;

                if (count > less) {
                    return true;
                }
            }
        }

        return false;
    }

    /** Paint all objects to 'g'.
     * @param g The graphics context to paint to.
     */
    public void paint(final JaxoGraphics2D g) {
        getObjectList().paint(g);
    }

    /** Paint all objects to 'g' if they intersect the clip bounds.
     * @param g The graphics contect to paint to.
     */
    public void paintClipped(final JaxoGraphics2D g) {
        getObjectList().paintClipped(g);
    }

    /** Paints handles on all objects in this graph.
     * @param g2 The graphics context to paint the handles to.
     * @param h The handle to paint.
     * @param editMode The edit mode that the handles are being painted in.
     */
    public final void paintHandles(final JaxoGraphics2D g2, final JaxoHandle h,
        final int editMode) {
        for (int j = 0; j < listSize(); j++) {
            final JaxoObject jaxoOb = listElementAt(j);
            jaxoOb.paintHandles(g2, h, editMode);
        }
    }

    /** Marks all objects in this graph.
     * @param marked A boolean that indicates whether to mark or not.
     */
    public final void setAsMarked(final boolean marked) {
        for (int i = 0; i < listSize(); i++) {
            final JaxoObject ob = listElementAt(i);
            ob.setAsMarked(marked);
        }
    }

    /** Returns an exact copy of this JaxoGraph.
     * @return A copy of this JaxoGraph.
     */
    public final JaxoGraph copyOf() {
        final JaxoGraph newGraph = new JaxoGraph();
        final JaxoSaveGraph newSaveGraph = saveGraph.copyOf();
        newGraph.setSaveGraph(newSaveGraph);

        newGraph.setSaved(isSaved());
        newGraph.setSaveFileName(getSaveFileName());

        return newGraph;
    }

    /** Breaks all the groups in this graph. */
    public final void breakAllGroups() {
        // breaking a group modifies the list but should preserve the save state
        final boolean wasSaved = isSaved();

        for (int j = 0; j < listSize(); j++) {
            final JaxoObject ob = listElementAt(j);
            if (ob instanceof JaxoGroup) {
                final JaxoGroup tempg = (JaxoGroup) ob;

                for (int i = 0; i < tempg.size(); i++) {
                    final JaxoObject tempob = tempg.getObjectList().get(i);
                    // objects in groups are still marked from grouping
                    tempob.setAsMarked(false);
                    this.addObject(j + i, tempob);
                }

                this.delete(tempg);
            }
        }

        if (this.containsGroups()) {
            this.breakAllGroups();
        }

        setSaved(wasSaved);
    }

    /** Puts all the objects in the current selection in the foreground. */
    public final void objectsToForeground() {
        int marked = listSize() - 1;
        for (int j = listSize() - 1; j > -1; j--) {
            final JaxoObject ob = listElementAt(j);
            if (ob.isMarked()) {
                move(j, marked);
                marked--;
            }
        }
    }

    /** Puts all the objects in the current selection in the background. */
    public final void objectsToBackground() {
        int marked = 0;
        for (int j = 0; j < listSize(); j++) {
            final JaxoObject ob = listElementAt(j);
            if (ob.isMarked()) {
                move(j, marked);
                marked++;
            }
        }
    }

    /** Returns the JaxoObject in this graph that is closest
     * to the given coordinates (ie which has the nearest handle).
     * @param x The x coordinate.
     * @param y The y coordinate.
     * @return The nearest JaxoObject or null if the graph is empty.
     */
    public final JaxoObject getNearestObject(final int x, final int y) {
        int element = -1;

        if (listSize() != 0) {
            float distance = 0.f;
            float smallest = 0.f;

            for (int j = 0; j < listSize(); j++) {
                final JaxoObject ob = listElementAt(j);
                distance = ob.smallestDistanceTo(x, y);
                if (j == 0) {
                    smallest = distance;
                    element = j;
                } else if (distance < smallest) {
                    smallest = distance;
                    element = j;
                }
            }
        }

        return listElementAt(element);
    }

    /**
     * Ungroup (one level) all marked objects.
     *
     * @return true if there were marked groups in this graph,
     *      ie the graph has been modified.
     */
    public boolean ungroupMarkedObjects() {
        boolean modified = false;

        for (final ListIterator<JaxoObject> i = getObjectList().listIterator(); i.hasNext();) {
            final JaxoObject o = i.next();

            if (o.isMarked() && o instanceof JaxoGroup) {
                modified = true;
                i.remove();

                for (final Iterator<JaxoObject> j = ((JaxoGroup) o).getObjectList().iterator();
                        j.hasNext();) {
                    i.add(j.next());
                }
            }
        }

        return modified;
    }

    /**
     * Groups the currently marked objects.
     *
     * @return true if there were marked objects in this graph,
     *      ie the graph has been modified.
     */
    public boolean groupMarkedObjects() {
        boolean modified = false;

        final JaxoList<JaxoObject> groupObjects = getMarkedObjects();

        if (groupObjects.size() > 1) {
            final Rectangle groupBounds = JaxoGeometry.getBounds(groupObjects);

            final JaxoGroup newGroup = new JaxoGroup();

            newGroup.setObjectList(groupObjects);
            newGroup.setX(groupBounds.x);
            newGroup.setY(groupBounds.y);
            newGroup.setX2(groupBounds.x + groupBounds.width);
            newGroup.setY2(groupBounds.y + groupBounds.height);

            deleteAll(groupObjects);

            addObject(newGroup);
            //setSelectedObject(newGroup); // why? instead mark the new one?

            modified = true;
        }

        return modified;
    }

    /**
     * If the given object is a group and contained in this graph, ungroup it.
     * In this case, the constituents of the group are appended to the graph,
     * and the group is removed.
     *
     * @param ob the object to ungroup.
     * @return true if the object was a group and contained in this graph,
     *      and therefore the graph has been modified.
     */
    public boolean ungroup(final JaxoObject ob) {
        boolean modified = false;

        if (ob instanceof JaxoGroup && getObjectList().contains(ob)) {
            final JaxoGroup g = (JaxoGroup) ob;

            for (final Iterator<JaxoObject> i = g.getObjectList().iterator(); i.hasNext();) {
                final JaxoObject o = i.next();
                // objects may still be marked from before grouping
                o.setAsMarked(false);
                addObject(o);
            }

            delete(g);

            modified = true;
        }

        return modified;
    }
}
