/**
 *  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.gui;

import java.awt.Color;
import java.awt.Component;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import net.sf.jaxodraw.graph.JaxoGraph;
import net.sf.jaxodraw.graph.JaxoSaveGraph;
import net.sf.jaxodraw.gui.grid.JaxoDefaultGrid;
import net.sf.jaxodraw.gui.grid.JaxoPaintableGrid;
import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectList;
import net.sf.jaxodraw.util.JaxoConstants;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.JaxoPrefs;


/** One tab of the JaxoTabbedPane.
 * @since 2.0
 */
public class JaxoTab {
    // Events
    private EventListenerList listeners;
    private ChangeEvent event;

    // Components
    private final JScrollPane root;

    // Graph and state
    private JaxoGraph tabGraph;
    private String tabTitle = "";
    private final JaxoPaintableGrid tabGrid;
    private int tabMode = JaxoConstants.STANDBY;
    private boolean saved = true;
    private boolean isUsed;
    private final JaxoDrawingArea theCanvas;

    // Undo
    private List<JaxoSaveGraph> backupList;
    private int repeatCount;
    private int undoCounter = 0;
    private int backupPointer = 0;
    private boolean redoFlag = false;

    /**
     * Constructor.
     *
     * @param canvas the drawing area on which to paint this tab.
     */
    public JaxoTab(final JaxoDrawingArea canvas) {
        this(canvas, new JaxoGraph());
    }

    /**
     * Constructor.
     *
     * @param canvas the drawing area on which to paint this tab.
     * @param g The graph for the tab.
     */
    public JaxoTab(final JaxoDrawingArea canvas, final JaxoGraph g) {
        this(canvas, g, JaxoDefaultGrid.newDefaultGrid());
    }

    /**
     * Constructor.
     *
     * @param canvas the drawing area on which to paint this tab.
     * @param g A grid for the tab.
     */
    public JaxoTab(final JaxoDrawingArea canvas, final JaxoPaintableGrid g) {
        this(canvas, new JaxoGraph(), g);
    }

    /**
     * Constructor.
     *
     * @param canvas the drawing area on which to paint this tab.
     * @param g A graph.
     * @param grid A grid. May be null.
     */
    public JaxoTab(final JaxoDrawingArea canvas, final JaxoGraph g, final JaxoPaintableGrid grid) {
        if (canvas == null) {
            throw new IllegalArgumentException("Canvas cannot be null!");
        }

        if (g == null) {
            throw new IllegalArgumentException("Graph cannot be null!");
        }

        if (grid == null) {
            throw new IllegalArgumentException("Grid cannot be null!");
        }

        this.listeners = new EventListenerList();

        this.root = new JScrollPane();
        root.putClientProperty("JaxoDraw.JaxoTab", this);

        this.theCanvas = canvas;
        this.tabGraph = g;
        this.tabGrid = grid;

        this.backupList = new ArrayList<JaxoSaveGraph>(JaxoPrefs.getIntPref(JaxoPrefs.PREF_UNDODEPTH));
        this.backupList.add(0, tabGraph.getSaveGraph().copyOf());
        incrementBackupPointer();

        revalidate();
    }

    /**
     * Determine if a component is a JaxoTab.
     * @param c The component to test.
     * @return The component as a JaxoTab, or null if it was not a JaxoTab.
     */
    public static JaxoTab asJaxoTab(final Component c) {
        return (c instanceof JComponent)
        ? (JaxoTab) ((JComponent) c).getClientProperty("JaxoDraw.JaxoTab") : null;
    }

    // This really should not subclass JScrollPane (nor Component).
    // Internally, all it setup properly, but outside it is still
    // assumed that the selected component is the JaxoTab itself, so
    // leave it for the moment.

    /**
     * Returns the root component of this tab.
     * @return The root component.
     */
    public final JComponent getRoot() {
        return root;
    }

    /** Returns this tab's graph.
     * @return This tab's graph.
     */
    public final JaxoGraph getTabGraph() {
        return tabGraph;
    }

    /** Sets this tab's graph.
     * @param theGraph The new graph.
     */
    private void setTabGraph(final JaxoGraph theGraph) {
        this.tabGraph = theGraph;
        theCanvas.setCanvasGraph(tabGraph);
    }

    /** Returns this tab's title.
     * @return This tab's title.
     */
    public final String getTabTitle() {
        return tabTitle;
    }

    /** Sets this tab's title.
     * @param theTitle The new title.
     */
    public final void setTabTitle(final String theTitle) {
        this.tabTitle = theTitle;
    }

    /**
     * ChangeEvents will be fired when a graph change is performed,
     * basically this means when the properties "canUndo", "canRedo",
     * "isSaved" and "saveFileName" may have changed.
     * @param l The listener to add.
     */
    public void addChangeListener(final ChangeListener l) {
        listeners.add(ChangeListener.class, l);
    }

    /**
     * Removes a change listener.
     * @param l The listener to remove.
     */
    public void removeChangeListener(final ChangeListener l) {
        listeners.remove(ChangeListener.class, l);
    }

    /** Notifies all listeners of state changes. */
    protected void fireStateChanged() {
        final Object[] pairs = listeners.getListenerList();

        for (int i = pairs.length - 2; i >= 0; i -= 2) {
            if (pairs[i] == ChangeListener.class) {
                if (event == null) {
                    event = new ChangeEvent(this);
                }
                ((ChangeListener) pairs[i + 1]).stateChanged(event);
            }
        }
    }

    /**
     * Initialize the canvas with this tab's graph and grid.
     */
    public final void revalidate() {
        theCanvas.setCanvasGraph(tabGraph);
        theCanvas.setGrid(tabGrid);

        root.setViewportView(theCanvas.asComponent());
        root.revalidate();
    }

    /**
     * Returns the current grid.
     *
     * @return The grid. May be null.
     */
    public JaxoPaintableGrid getGrid() {
        return this.tabGrid;
    }

    /**
     * Switches snapping to the grid on/off.
     * If the current grid is null, this method returns silently.
     *
     * @param value Boolean that sets the grid on/off.
     */
    public final void setSnappingToGrid(final boolean value) {
        if (value != tabGrid.isSnapping()) {
            tabGrid.setSnapping(value);
            theCanvas.setGrid(tabGrid);
        }
    }

    /**
     * Determines whether snapping to the grid is currently on.
     *
     * @return True if snapping is enabled, false otherwise or if the grid is null.
     */
    public final boolean isSnappingToGrid() {
        return tabGrid.isSnapping();
    }

    /**
     * Switch on the grid.
     *
     * @param on true if the grid should be painted, false otherwise.
     * @since 2.1
     */
    public void setGridPainted(final boolean on) {
        tabGrid.setPainted(on);

        if (on) {
            theCanvas.setGrid(tabGrid);
        } else {
            theCanvas.setGrid(null);
        }
    }

    /**
     * Check if the grid is switched on.
     *
     * @return true if the grid is painted, false otherwise.
     * @since 2.1
     */
    public boolean isGridPainted() {
        return tabGrid.isPainted();
    }

    /**
     * Sets the size of the grid to the given value.
     * If the current grid is null, this method returns silently.
     *
     * @param gs The grid size to be set.
     */
    public final void setGridSize(final int gs) {
        tabGrid.setGridSize(gs);
    }

    /**
     * Returns the current size of the grid.
     *
     * @return The current grid size, or 0 if the current grid is null.
     */
    public final int getGridSize() {
        return tabGrid.getGridSize();
    }

    /**
     * Sets the type of the grid.
     * If the current grid is null, this method returns silently.
     *
     * @param type The type of the grid to be set.
     * One of the types defined in {@link JaxoConstants}.
     */
    public final void setGridType(final int type) {
        switch (type) {
            case JaxoConstants.RECTANGULAR_GRID:
                tabGrid.setGridType(JaxoPaintableGrid.TYPE_RECTANGULAR);
                break;
            case JaxoConstants.HEXAGONAL_GRID:
                tabGrid.setGridType(JaxoPaintableGrid.TYPE_HEXAGONAL);
                break;
            default:
                break;
        }
    }

    /**
     * Returns the current type of the grid.
     *
     * @return The type of the grid or 0 if thecurrent grid is null.
     */
    public final int getGridType() {
        return tabGrid.getGridType();
    }

    /**
     * Sets the style of the grid.
     * If the current grid is null, this method returns silently.
     *
     * @param style The style of the grid to be set.
     * One of the styles defined in {@link JaxoConstants}.
     */
    public final void setGridStyle(final int style) {
        switch (style) {
            case JaxoConstants.GRID_STYLE_DOT:
                tabGrid.setGridStyle(JaxoPaintableGrid.STYLE_DOT);
                break;
            case JaxoConstants.GRID_STYLE_CROSS:
                tabGrid.setGridStyle(JaxoPaintableGrid.STYLE_CROSS);
                break;
            case JaxoConstants.GRID_STYLE_LINE:
                tabGrid.setGridStyle(JaxoPaintableGrid.STYLE_LINE);
                break;
            case JaxoConstants.GRID_STYLE_LINE_HONEYCOMB:
                tabGrid.setGridStyle(JaxoPaintableGrid.STYLE_LINE_HONEYCOMB);
                break;
            default:
                break;
        }
    }

    /**
     * Returns the current style of the grid.
     *
     * @return The style of the current grid or 0 if the grid is null.
     */
    public final int getGridStyle() {
        return tabGrid.getGridStyle();
    }

    /**
     * Sets the color of the grid.
     * If the current grid is null, this method returns silently.
     *
     * @param color The color of the grid to be set.
     */
    public final void setGridColor(final Color color) {
        tabGrid.setGridColor(color);
    }

    /**
     * Returns the current color of the grid.
     *
     * @return The color of the grid or null if the grid is null.
     */
    public final Color getGridColor() {
        return tabGrid.getGridColor();
    }

    /** Commit changes to 'tabGraph' to the undo history. */
    public void commitGraphChanges() {
        commitGraph(0);
    }

    /** Commit changes to 'tabGraph' to the undo history,
     * where the change is the addition of the last object
     * that should be repeatable.
     */
    public void commitRepeatableGraphChanges() {
        commitRepeatableGraphChanges(1);
    }

    /**
     * Commit changes to 'tabGraph' to the undo history,
     * where the change is the addition of 'count' last objects
     * that should be repeatable.
     * @param count The number of changes to commit.
     */
    public void commitRepeatableGraphChanges(final int count) {
        commitGraph(count);
    }

    /** Adds a new element to the backup list at the position
    * specified by the current value of the eventCounter.
    * @param count number of objects added at the end for repeatable change.
    */
    private void commitGraph(final int count) {
        setRedoFlag(false);

        repeatCount = count;

        final int pos = getBackupPointer();

        if (pos < backupList.size()) {
            backupList.subList(pos, backupList.size()).clear();
        }

        final int maxSize = JaxoPrefs.getIntPref(JaxoPrefs.PREF_UNDODEPTH);

        if (backupList.size() >= maxSize) {
            backupList.subList(0, backupList.size() - maxSize + 1).clear();
        }

        backupList.add(tabGraph.getSaveGraph().copyOf());

        resetUndoCounter();
        incrementBackupPointer();

        setUnsaved();

        fireStateChanged();
    }

    /**
     * Sets the save file name.
     * @param value The name to set.
     */
    public void setSaveFileName(final String value) {
        tabGraph.setSaveFileName(value);

        fireStateChanged();
    }

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

    /** Clear undo history. Start with a new history, as if
     * newly constructed.
     */

    // TODO: This is still a mess
    public void clearBackupList() {
        final boolean wasSaved = isSaved();

        backupList.clear();
        backupList.add(tabGraph.getSaveGraph().copyOf());
        resetUndoCounter();
        setBackupPointer(1);
        setRedoFlag(false);
        repeatCount = 0;

        setUnsaved();

        setSaved(wasSaved);

        fireStateChanged();
    }

    /**
     * Starts a new graph for this tab. This should be equivalent with
     * re-starting, eg it is not un-doable and the new status is saved.
     */
    public void newGraph() {
        setTabGraph(new JaxoGraph());
        commitGraphChanges();
        setSaved(true);
        clearBackupList();
    }

    /**
     * Undo last operation.
     */
    public void undoMove() {
        repeatCount = 0;
        incrementUndoCounter();
        setRedoFlag(true);
        final int lastGraphPosition = backupList.size() - undoCounter;
        setBackupPointer(lastGraphPosition);
        setSaveGraph(backupList.get(lastGraphPosition - 1)
            .copyOf());

        setUnsaved();

        fireStateChanged();
    }

    private void setSaveGraph(final JaxoSaveGraph g) {
        tabGraph.setSaveGraph(g);
        theCanvas.markImageInvalid(null);
    }

    /**
     * Redo last operation.
     */
    public void redoMove() {
        if (isRedoFlag()) {
            decrementUndoCounter();
            final int lastGraphPosition = backupList.size() - undoCounter;
            setBackupPointer(lastGraphPosition);

            setSaveGraph(backupList.get(lastGraphPosition - 1)
                .copyOf());

            if (lastGraphPosition > (backupList.size() - 1)) {
                setRedoFlag(false);
            }

            setUnsaved();

            fireStateChanged();
        } else {
            repeatChange();
        }
    }

    private void repeatChange() {
        final int size = tabGraph.getObjectList().size();

        final List<JaxoObject> l = JaxoGraph.copyFrom(
            new JaxoObjectList<JaxoObject>(tabGraph.getObjectList().subList(size - repeatCount,
                    size)));

        for (final Iterator<JaxoObject> i = l.iterator(); i.hasNext();) {
            tabGraph.addObject(i.next());
        }

        theCanvas.markImageInvalid(JaxoGeometry.getBounds(l));

        commitRepeatableGraphChanges(repeatCount);
        setUnsaved();
        fireStateChanged();
    }

    /** Sets the tab mode.
     * @param mode The tab mode.
     */
    public void setTabMode(final int mode) {
        this.tabMode = mode;
    }

    /** Returns the tab mode.
    * @return The tab mode.
    */
    public int getTabMode() {
        return tabMode;
    }

    /** Indicates whether this tab has been used.
     * @return True, if the canvas has been modified, false otherwise.
     */
    public boolean hasBeenUsed() {
        return this.isUsed;
    }

    /**
     * Sets the tab saved / not saved.
     * @param value True for saved.
     */
    public void setSaved(final boolean value) {
        if (saved != value) {
            saved = value;
            fireStateChanged();
        }
    }

    /**
     * Determines if the graph is saved.
     * This is internally cleared at each change (also undo/redo).
     * @return True if the tab is saved.
     */
    public boolean isSaved() {
        return saved;
    }

    private void setUnsaved() {
        isUsed = true;
        saved = false;
    }

    private void incrementUndoCounter() {
        this.undoCounter++;
    }

    private void decrementUndoCounter() {
        this.undoCounter--;
    }

    private void resetUndoCounter() {
        this.undoCounter = 0;
    }

    private int getBackupPointer() {
        return backupPointer;
    }

    private void setBackupPointer(final int fup) {
        this.backupPointer = fup;
    }

    private void incrementBackupPointer() {
        this.backupPointer++;
    }

    private void setRedoFlag(final boolean bool) {
        this.redoFlag = bool;
    }

    private boolean isRedoFlag() {
        return this.redoFlag;
    }

    /**
     * Determines if undo is possible.
     * @return True if undo is possible.
     */
    public boolean canUndo() {
        return (backupList.size() - undoCounter) > 1;
    }

    /**
     * Determines if redo is possible.
     * @return True if redo is possible.
     */
    public boolean canRedo() {
        return (backupPointer < backupList.size())
        || (!redoFlag && (repeatCount != 0));
    }
}
