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

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;

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

import net.sf.jaxodraw.util.JaxoColor;
import net.sf.jaxodraw.util.JaxoPrefs;


/** Paints a grid on the canvas.
 * @since 2.0
 */
public class JaxoDefaultGrid implements JaxoPaintableGrid, Cloneable {
    // used to calculate the distance between rows = (int)Math.round(FACTOR * gridSize)
    private static final double FACTOR = Math.sqrt(3) / 2;

    // color used for transparent pixels
    private static Color transparentBackground =
        new Color(0xff, 0xff, 0xff, 0);

    // Events
    private EventListenerList listeners;
    private ChangeEvent event;
    private int style;

    /** The size of the grid, i.e., the distance between two grid points.*/
    private int gridSize;

    /** The type of grid: rectangular or hexagonal.*/
    private int type;

    // Needed for hexagonal X snapping
    private boolean isOddRow = true;
    private Dimension canvasSize;
    private boolean transparent = true;
    private Color gridColor;
    private Color background;

    // distance between rows in hexagonal grid
    private int gridDistance;
    private BufferedImage image;

    // Is the image invalid (must be repainted) even though
    // existing? (has no meaning when image == null)
    private boolean imageInvalid;
    private boolean paint = true;
    private boolean snap;

    /**
     * Constructor.
     * @param gsize The grid size to be set.
     * @param gtype The grid type to be set.
     */
    public JaxoDefaultGrid(final int gsize, final int gtype) {
        this(gsize, gtype, new Dimension());
    }

    /**
     * Constructor: Sets the dimension to the current screen size
     * and take the other argument values.
     * @param gsize The grid size to be set.
     * @param gtype The grid type to be set.
     * @param gridStyle The grid style to be set
     * @param color The grid color to be set
     */
    public JaxoDefaultGrid(final int gsize, final int gtype, final int gridStyle, final Color color) {
        this(gsize, gtype, gridStyle, color, new Dimension());
    }

    /** Constructor: Sets the dimension, type and value from
     * the arguments.
     * @param gsize The grid size to be set.
     * @param gtype The grid type to be set.
     * @param canvasDim The canvasSize.
     */
    public JaxoDefaultGrid(final int gsize, final int gtype, final Dimension canvasDim) {
        this(gsize, gtype, STYLE_DOT, JaxoColor.GRAYSCALE120, canvasDim);
    }

    /**
     * Constructor: Sets the dimension, type and value from
     * the arguments.
     * @param gsize The grid size to be set.
     * @param gtype The grid type to be set.
     * @param gridStyle The grid style to be set.
     * @param color The grid color to be set.
     * @param canvasDim The current canvas size.
     */
    public JaxoDefaultGrid(final int gsize, final int gtype, final int gridStyle, final Color color,
        final Dimension canvasDim) {
        this.gridSize = gsize;

        if ((gtype == TYPE_HEXAGONAL) && ((gsize % 2) != 0)) {
            this.gridSize++;
        }

        this.style = gridStyle;

        this.listeners = new EventListenerList();

        this.gridDistance = (int) Math.round(gridSize * FACTOR);
        this.type = gtype;
        this.gridColor = color;
        this.background = Color.white;
        this.canvasSize = (Dimension) canvasDim.clone();
    }

    /**
     * Standard clone with all properties at the same values.
     * @return A clone of this grid.
     * @throws CloneNotSupportedException If the object's class does
     * not support the Cloneable interface.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        final JaxoDefaultGrid clone = (JaxoDefaultGrid) super.clone();

        clone.listeners = new EventListenerList();

        clone.image = null;
        clone.imageInvalid = false;

        return clone;
    }

    /**
     * Copies all properties from the argument.
     *
     * @param g The grid to take the properties from.
     */
    public void copyFrom(final JaxoDefaultGrid g) {
        this.transparent = g.transparent;
        setBackground(g.getBackground());
        setGridColor(g.getGridColor());
        setGridType(g.getGridType());
        setGridSize(g.getGridSize());
        setGridStyle(g.getGridStyle());
        setCanvasSize(g.getCanvasSize());
    }

    /** {@inheritDoc} */
    public void addChangeListener(final ChangeListener l) {
        listeners.add(ChangeListener.class, l);
    }

    /** {@inheritDoc} */
    public void removeChangeListener(final ChangeListener l) {
        listeners.remove(ChangeListener.class, l);
    }

    /**
     * Notifies all listeners of a state change.
     */
    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);
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isSnapped(final Point p) {
        final boolean old = isOddRow;

        try {
            return (snapY(p.y) == p.y) && (snapX(p.x) == p.x);
        } finally {
            isOddRow = old;
        }
    }

    /** {@inheritDoc} */
    public final Point snappedPoint(final Point p) {
        final Point q = (Point) p.clone();

        snapPoint(q);

        return q;
    }

    /** {@inheritDoc} */
    public final void snapPoint(final Point p) {
        final boolean old = isOddRow;

        try {
            p.y = snapY(p.y);
            p.x = snapX(p.x);
        } finally {
            isOddRow = old;
        }
    }

    /** Returns the x coordinate of the grid point that is closest to the
     * given coordinate.
     * @param x The current x coordinate.
     * @return The x coordinate of the grid point corresponding to x.
     */
    private int snapX(final int x) {
        int snappedX = gridSize * (Math.round(x / ((float) gridSize)));

        //snap to points in the middle
        if ((type == TYPE_HEXAGONAL) && isOddRow) {
            final int snappedX1 = snappedX - (gridSize / 2);
            final int snappedX2 = snappedX + (gridSize / 2);

            if (Math.abs(x - snappedX1) < Math.abs(x - snappedX2)) {
                snappedX = snappedX1;
            } else {
                snappedX = snappedX2;
            }
        }

        return snappedX;
    }

    /** Returns the y coordinate of the grid point that is closest to the
     * given coordinate.
     * @param y The current y coordinate.
     * @return The y coordinate of the grid point corresponding to y.
     */
    private int snapY(final int y) {
        int snappedY;

        if (type == TYPE_HEXAGONAL) {
            //Hexagonal grid Y snapping: IT IS IMPORTANT THAT THE SNAPPING
            //OF A HEXAGONAL GRID IS DONE FIRST IN THE Y COORD (WHICH
            //DETERMINES IF THE ROW IS EVEN OR ODD) AND THEN IN
            //THE X COORD.
            final int row = Math.round(y / ((float) gridDistance));

            snappedY = gridDistance * row;

            isOddRow = (row % 2) != 0;
        } else {
            //Normal grid Y snapping
            snappedY = gridSize * (Math.round(y / ((float) gridSize)));
        }

        return snappedY;
    }

    private void ensureValidImage() {
        createImage();
        paintImage();
    }

    private void createImage() {
        if (image != null) {
            return;
        }

        int width;
        int height;

        // Create the image with a rough size of 200 x 200:
        // For hexagonal, 'width'  must be a multiple of 3 x gridSize,
        //                'height' must be a multiple of 2 x gridDistance.
        // For rectangular, 'width'/'height' must be multiples of gridSize.
        if (type == TYPE_HEXAGONAL) {
            width = Math.max(1, 70 / gridSize) * 3 * gridSize;
            height = Math.max(1, 100 / gridDistance) * 2 * gridDistance;
        } else {
            width = Math.max(1, 200 / gridSize) * gridSize;
            height = width;
        }

        final Color b = hasBackground() ? background : transparentBackground;

        image =
            new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED,
                new IndexColorModel(8, 2,
                    new byte[]{(byte) b.getRed(), (byte) gridColor.getRed()},
                    new byte[]{(byte) b.getGreen(), (byte) gridColor.getGreen()},
                    new byte[]{(byte) b.getBlue(), (byte) gridColor.getBlue()},
                    new byte[]{(byte) b.getAlpha(), (byte) gridColor.getAlpha()}));

        imageInvalid = true;
    }

    private static BasicStroke newStroke(final float[] dash) {
        return new BasicStroke(1.f, BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_MITER, 10.f, dash, 0.f);
    }

    private void paintImage() {
        if (!imageInvalid) {
            return;
        }

        final int width = image.getWidth();
        final int height = image.getHeight();

        final Graphics2D g = image.createGraphics();

        g.setComposite(AlphaComposite.Src);
        g.setColor(hasBackground() ? background : transparentBackground);
        g.fillRect(0, 0, width, height);

        g.setColor(gridColor);

        if (type == TYPE_HEXAGONAL) {
            if (style == STYLE_LINE) {
                g.setStroke(new BasicStroke(1));

                for (int y = 0; y < height; y += (gridDistance * 2)) {
                    g.drawLine(0, y, width, y);
                    g.drawLine(0, y + gridDistance, width, y + gridDistance);
                }

                final int rows = Math.round(height / (float) gridDistance) + 1;

                for (int x = -(rows + 1) / 2 * gridSize; x < width;
                        x += gridSize) {
                    g.drawLine(x, 0, x + ((rows * gridSize) / 2),
                        rows * gridDistance);
                }

                for (int x = gridSize; (x - ((rows * gridSize) / 2)) < width;
                        x += gridSize) {
                    g.drawLine(x, 0, x - ((rows * gridSize) / 2),
                        rows * gridDistance);
                }
            } else if (style == STYLE_LINE_HONEYCOMB) {
                g.setStroke(new BasicStroke(1));

                for (int y = 0; y < height; y += (gridDistance * 2)) {
                    for (int x = 0; x < width; x += (3 * gridSize)) {
                        g.drawLine(x, y, x + gridSize, y);
                        g.drawLine(x + ((3 * gridSize) / 2), y + gridDistance,
                            x + ((5 * gridSize) / 2), y + gridDistance);
                    }
                }

                for (int y = 0; y < height; y += (gridDistance * 2)) {
                    for (int x = -3 * gridSize; x < width;
                            x += (3 * gridSize)) {
                        g.drawLine(x + gridSize, y, x + ((3 * gridSize) / 2),
                            y + gridDistance);
                        g.drawLine(x + (3 * gridSize), y,
                            x + ((5 * gridSize) / 2), y + gridDistance);
                        g.drawLine(x + ((3 * gridSize) / 2), y + gridDistance,
                            x + gridSize, y + (2 * gridDistance));
                        g.drawLine(x + ((5 * gridSize) / 2), y + gridDistance,
                            x + (3 * gridSize), y + (2 * gridDistance));
                    }
                }
            } else {
                g.setStroke(newStroke(new float[]{1.f, (float) (gridSize - 1)}));

                final boolean cross = style == STYLE_CROSS;

                // for STYLE_CROSS: draw first normal grid, shifted one pixel up,
                // then normal, shiften one pixel down
                // then three-pixel pattern, starting one pixel to the left
                for (int y = cross ? (-1) : 0; y < height;
                        y += (gridDistance * 2)) {
                    g.drawLine(0, y, width, y);
                    g.drawLine(-gridSize / 2, y + gridDistance, width,
                        y + gridDistance);
                }

                if (cross) {
                    for (int y = (gridDistance == 1) ? (-1) : 1; y < height;
                            y += (gridDistance * 2)) {
                        g.drawLine(0, y, width, y);
                        g.drawLine(-gridSize / 2, y + gridDistance, width,
                            y + gridDistance);
                    }

                    if (gridSize >= 3) {
                        g.setStroke(newStroke(
                                new float[]{3.f, (float) (gridSize - 3)}));

                        for (int y = 0; y < height; y += (gridDistance * 2)) {
                            g.drawLine(-1, y, width, y);
                            g.drawLine((-gridSize / 2) - 1, y + gridDistance,
                                width, y + gridDistance);
                        }
                    }
                }
            }
        } else { // Rectangular Grid
            if ((style == STYLE_LINE) || (style == STYLE_LINE_HONEYCOMB)) {
                g.setStroke(new BasicStroke(1));

                for (int y = 0; y < height; y += gridSize) {
                    g.drawLine(0, y, width, y);
                }

                for (int x = 0; x < width; x += gridSize) {
                    g.drawLine(x, 0, x, height);
                }
            } else {
                g.setStroke(newStroke(new float[]{1.f, (float) (gridSize - 1)}));
                final boolean cross = style == STYLE_CROSS;

                for (int y = cross ? (gridSize - 1) : 0; y < height;
                        y += gridSize) {
                    g.drawLine(0, y, width, y);
                }

                if (cross) {
                    for (int y = (gridSize == 1) ? 0 : 1; y < height;
                            y += gridSize) {
                        g.drawLine(0, y, width, y);
                    }

                    if (gridSize >= 3) {
                        g.setStroke(newStroke(
                                new float[]{3.f, (float) (gridSize - 3)}));

                        for (int y = 0; y < height; y += gridSize) {
                            g.drawLine(-1, y, width, y);
                        }
                    }
                }
            }
        }

        g.dispose();
        imageInvalid = false;
    }

    private void disposeImage() {
        if (image != null) {
            image.flush();
            image = null;
            imageInvalid = false;
        }
    }

    /**
     * Base on the 'transparent' property and the 'background' and
     * 'gridColor' properties, assuming SRC_OVER rule.
     * @return The transparency.
     */
    public final int getTransparency() {
        // If BITMASK, the background has alpha=0, i.e. practically does not exist.
        // If background is opaque, painting with gridColor will leave it that way.
        if (hasBackground() && (background.getTransparency() != BITMASK)) {
            return background.getTransparency();
        }

        return (gridColor.getTransparency() == TRANSLUCENT) ? TRANSLUCENT
                                                            : BITMASK;
    }

    /** {@inheritDoc} */
    public final void paint(final Graphics2D g) {
        ensureValidImage();

        for (int x = 0; x < canvasSize.width; x += image.getWidth()) {
            for (int y = 0; y < canvasSize.height; y += image.getHeight()) {
                g.drawImage(image, null, x, y);
            }
        }
    }

    /** {@inheritDoc} */
    public Dimension getCanvasSize() {
        return (Dimension) canvasSize.clone();
    }

    /** {@inheritDoc} */
    public void setCanvasSize(final Dimension value) {
        canvasSize.setSize(value);
    }

    /** {@inheritDoc} */
    public final void setGridSize(final int value) {
        int newValue = value;
        if ((type == TYPE_HEXAGONAL) && ((value % 2) != 0)) {
            newValue++;
        }

        if (newValue != gridSize) {
            this.gridSize = newValue;
            this.gridDistance = (int) Math.round(gridSize * FACTOR);

            disposeImage();

            fireStateChanged();
        }
    }

    /** {@inheritDoc} */
    public final int getGridSize() {
        return gridSize;
    }

    /** {@inheritDoc} */
    public final void setGridType(final int value) {
        if (type != value) {
            this.type = value;

            if ((type == TYPE_HEXAGONAL) && ((gridSize % 2) != 0)) {
                gridSize++;
                gridDistance = (int) Math.round(gridSize * FACTOR);
            }

            disposeImage();

            fireStateChanged();
        }
    }

    /** {@inheritDoc} */
    public final int getGridType() {
        return type;
    }

    /** {@inheritDoc} */
    public final Color getGridColor() {
        return gridColor;
    }

    /** {@inheritDoc} */
    public void setGridColor(final Color value) {
        if (!gridColor.equals(value)) {
            gridColor = value;

            disposeImage();

            fireStateChanged();
        }
    }

    private boolean hasBackground() {
        return !transparent && (background != null);
    }

    /**
     * Background of the grid (filling the whole canvas).
     * May be null, indicating no background painting, and
     * is ignored in 'transparent' mode.
     * The default is White (the default mode is transparent,
     * so it does not matter).
     * @return The background color.
     */
    public final Color getBackground() {
        return background;
    }

    /**
     * Sets the grid background color.
     * @param value The color to set.
     */
    public void setBackground(final Color value) {
        if ((background == null) ? (value != null) : (
                    !background.equals(value)
                )) {
            background = value;

            if (!transparent) {
                disposeImage(); // could improve

                fireStateChanged();
            }
        }
    }

    /** {@inheritDoc} */
    public final int getGridStyle() {
        return style;
    }

    /** {@inheritDoc} */
    public void setGridStyle(final int value) {
        if (style != value) {
            style = value;

            imageInvalid = true;

            fireStateChanged();
        }
    }

    /**
     * A new grid with the values from the preferences.
     * @return A new grid.
     */
    public static final JaxoDefaultGrid newDefaultGrid() {
        return new JaxoDefaultGrid(JaxoPrefs.getIntPref(
                JaxoPrefs.PREF_GRIDSIZE),
            "rectangular".equals(JaxoPrefs.getStringPref(
                    JaxoPrefs.PREF_GRIDTYPE))
            ? JaxoDefaultGrid.TYPE_RECTANGULAR : JaxoDefaultGrid.TYPE_HEXAGONAL,
            JaxoPrefs.getIntPref(JaxoPrefs.PREF_GRIDSTYLE),
            JaxoColor.getColor(JaxoPrefs.getStringPref(
                    JaxoPrefs.PREF_GRIDCOLOR), JaxoColor.ALL_COLORS_MODE));
    }

    /** {@inheritDoc} */
    public boolean isPainted() {
        return paint;
    }

    /** {@inheritDoc} */
    public void setPainted(final boolean painted) {
        this.paint = painted;
    }

    /** {@inheritDoc} */
    public boolean isSnapping() {
        return snap;
    }

    /** {@inheritDoc} */
    public void setSnapping(final boolean snapping) {
        this.snap = snapping;
    }
}
