package kiyut.sketsa.modules.extratool;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
import javax.swing.SwingUtilities;
import kiyut.sketsa.canvas.CanvasModel;
import kiyut.sketsa.canvas.VectorCanvas;
import kiyut.sketsa.canvas.tool.AbstractTool;
import kiyut.sketsa.event.MessageEvent;
import kiyut.sketsa.options.CanvasOptions;
import kiyut.sketsa.undo.DOMUndoManager;
import kiyut.sketsa.util.ColorConvertion;
import kiyut.sketsa.util.DOMUtilities;
import kiyut.sketsa.util.SVGConstants;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGPoint;
import org.w3c.dom.svg.SVGPointList;
import org.w3c.dom.svg.SVGPolygonElement;
import org.w3c.dom.svg.SVGPolylineElement;
import org.w3c.dom.svg.SVGSVGElement;
import org.w3c.dom.svg.SVGStylable;

/**
 * Implementation of Draw Arrow Tool
 *
 * @author  KIYUT
 */
public class Arrow01Tool extends AbstractTool {
    protected VectorCanvas canvas;
    protected Line2D line;
    protected Rectangle2D bounds;

    protected Paint strokePaint;
    protected Paint fillPaint;
    protected Stroke stroke;
    protected float strokeWidth;

    private int x;
    private int y;
    private int lastX;
    private int lastY;

    protected Rectangle2D initHandle;

    private SVGElement element;

    /** Creates a new instance of Arrow01Tool */
    public Arrow01Tool() {
        bounds = new Rectangle2D.Double();

        strokeWidth = 1;
        stroke = new BasicStroke(strokeWidth);
        strokePaint = Color.BLACK;
        fillPaint = null;

        reset();
    }

    @Override
    public void paint(Graphics2D g2d) {
        Paint oldPaint = g2d.getPaint();
        Stroke oldStroke = g2d.getStroke();

        g2d.setPaint(strokePaint);
        g2d.setStroke(stroke);

        if (initHandle != null) {
            g2d.fill(initHandle);
        }
        g2d.draw(line);

        g2d.setPaint(oldPaint);
        g2d.setStroke(oldStroke);
    }

    @Override
    public void setVectorCanvas(VectorCanvas canvas) {
        this.canvas = canvas;
    }

    public void setStrokePaint(Paint strokePaint) {
        this.strokePaint = strokePaint;
    }

    public void setFillPaint(Paint fillPaint) {
        this.fillPaint = fillPaint;
    }

    public void setStroke(Stroke stroke) {
        this.stroke = stroke;
    }

    @Override
    public void endTool() {
        addArrowHead();
        reset();
    }

    @Override
    public void startTool() {
        reset();
        canvas.getCanvasSelection().setEnabledVisible(false,true,false);
        canvas.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        fireMessageReceived(MessageEvent.INFORMATION_MESSAGE, "Click and drag to draw arrow.");
    }

    private void reset() {
        initHandle = null;
        line = new Line2D.Float();
        element = null;
    }

    @Override
    public void mouseClicked(MouseEvent evt) {
        if (!SwingUtilities.isLeftMouseButton(evt)) { return; }
        if (evt.getClickCount() >= 2) {
            endTool();
            startTool();
            canvas.refresh();
        }
    }

    @Override
    public void mousePressed(MouseEvent evt) {
        if (!SwingUtilities.isLeftMouseButton(evt)) { return; }

        lastX = x;
        lastY = y;
        x = evt.getX();
        y = evt.getY();

        if (canvas.isSnapToGrid() == true) {
            int gridSize = canvas.getCanvasGrid().getGridSize();
            x = gridSize * Math.round((float)x/ gridSize);
            y = gridSize * Math.round((float)y/ gridSize);
        }

        if (initHandle == null) {
            CanvasOptions prefs = CanvasOptions.getInstance();
            int handleSize = prefs.getHandleSize() / 2;

            initHandle = new Rectangle2D.Float();
            initHandle.setFrameFromCenter(x,y,x-handleSize,y-handleSize);
            line.setLine(x,y,x,y);

            canvas.getCanvasSelection().setVisible(false,false);
        } else {
            line.setLine(line.getX2(),line.getY2(),x,y);
        }
        canvas.repaint();
    }

    @Override
    public void mouseReleased(MouseEvent evt) {
        if (!SwingUtilities.isLeftMouseButton(evt)) { return; }

        if (line.getP1().equals(line.getP2())) {
            return;
        }

        if (element == null) {
            SVGElement gElt = createSVGElement();
            element = (SVGElement)gElt.getFirstChild();  // return the body of arrow

            addPoint((float) line.getX1(), (float) line.getY1());
            CanvasModel model = canvas.getModel();
            DOMUndoManager undoManager = canvas.getUndoManager();
            undoManager.start("Draw Arrow");
            try {
                model.appendChild(gElt);
            } finally {
                undoManager.end();
            }
        }

        addPoint((float) line.getX2(), (float) line.getY2());
        if (element != null) {
            java.util.List<SVGElement> selectionList = new java.util.ArrayList<SVGElement>(1);
            selectionList.add(element);
            canvas.getCanvasSelection().setSelectionList(selectionList);
        }  // if element == null (should not happen, it is bug

        canvas.refresh();
    }

    @Override
    public void mouseDragged(MouseEvent evt) {
        if (!SwingUtilities.isLeftMouseButton(evt)) { return; }

        lastX = x;
        lastY = y;
        x = evt.getX();
        y = evt.getY();

        // constraint
        if ((evt.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) == InputEvent.SHIFT_DOWN_MASK) {
            int startX = (int)line.getX1();
            int startY = (int)line.getY1();

            int dx = x-startX;
            int dy = y-startY;
            // calculate angle
            double angle = 0;
            angle = Math.atan2(y - startY, x - startX);
            angle = Math.toDegrees(angle);
            if (angle >= -22.5 &&  angle < 22.5 ) {
                // East
                y = startY;
            } else if (angle >= 22.5 && angle < 67.5) {
                // South East
                if (angle <= 45) {
                    x = startX + dx;
                    y = startY + dx;
                } else {
                    x = startX + dy;
                    y = startY + dy;
                }
            } else if (angle >= 67.5 && angle < 112.5) {
                // South
                x = startX;
            } else if (angle >= 112.5 && angle < 157.5) {
                // South West
                if (angle <= 135 ) {
                    x = startX - dy;
                    y = startY + dy;
                } else {
                    x = startX + dx;
                    y = startY - dx;
                }

            } else if ((angle >= 157.5 && angle <= 180) || (angle >= -180 && angle < -157.5)) {
                // West
                x = startX + dx;
                y = startY;
            } else if (angle >= -157.5 && angle < -112.5) {
                // North West
                if (angle <= -135 ) {
                    x = startX + dy;
                    y = startY + dy;
                } else {
                    x = startX + dx;
                    y = startY + dx;
                }

            } else if (angle >= -112.5  && angle < -67.5) {
                // North
                x = startX;
            } else if (angle >= -67.5  && angle < -22.5) {
                // North East
                if (angle <= -45 ) {
                    x = startX - dy;
                    y = startY + dy;
                } else {
                    x = startX + dx;
                    y = startY - dx;
                }
            }
        }

        if (canvas.isSnapToGrid() == true) {
            int gridSize = canvas.getCanvasGrid().getGridSize();
            x = gridSize * Math.round((float)x/ gridSize);
            y = gridSize * Math.round((float)y / gridSize);
        }

        bounds.setRect(line.getBounds());

        line.setLine(line.getX1(),line.getY1(),line.getX2()+x-lastX,line.getY2()+y-lastY);

        bounds = bounds.createUnion(line.getBounds());
        canvas.repaint((int)bounds.getX()-1,(int)bounds.getY()-1,(int)bounds.getWidth()+2,(int)bounds.getHeight()+2);

        //line.setLine(line.getX1(),line.getY1(),line.getX2()+arrowX-lastX,line.getY2()+arrowY-lastY);
        //canvas.repaint();
    }

    protected SVGElement createSVGElement() {
        CanvasModel model = canvas.getModel();
        SVGDocument svgDocument = model.getSVGDocument();
        String svgNS = SVGConstants.SVG_NAMESPACE_URI;

        // poly or arrow body
        SVGElement arrowBody = (SVGElement)svgDocument.createElementNS(svgNS, SVGConstants.SVG_POLYLINE_TAG);

        SVGStylable stylable = (SVGStylable)arrowBody;

        String str;

        if (strokePaint != null) {
            if (strokePaint instanceof Color) {
                str = ColorConvertion.toHexString((Color) strokePaint);
                DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_STROKE_ATTRIBUTE, str);
            }
        } else {
            DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
        }

        if (fillPaint != null) {
            if (fillPaint instanceof Color) {
                str = ColorConvertion.toHexString((Color) fillPaint);
                DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_FILL_ATTRIBUTE, str);
            }
        } else {
            DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
        }

        // add group for body
        SVGElement arrow = (SVGElement)svgDocument.createElementNS(svgNS, SVGConstants.SVG_G_TAG);
        arrow.setId("arrow" + System.currentTimeMillis());
        arrow.appendChild(arrowBody);

        return arrow;
    }

    private void addPoint(float x, float y) {
        if (element == null) { return;}

        SVGPointList svgPointList = ((SVGPolylineElement)element).getPoints();

        CanvasModel model = canvas.getModel();
        SVGDocument svgDocument = model.getSVGDocument();
        SVGSVGElement svgElement = svgDocument.getRootElement();

        double[] srcPts = new double[2];
        double[] dstPts = new double[2];
        srcPts[0] = x;
        srcPts[1] = y;

        //canvas.reverseTransform(srcPts,0,dstPts,0,1);
        AffineTransform at = canvas.getTransform(true);
        try {
            at = at.createInverse();
        } catch (NoninvertibleTransformException e) {}
        at.transform(srcPts,0,dstPts,0,1);

        SVGPoint svgPoint = svgElement.createSVGPoint();
        svgPoint.setX((float)dstPts[0]);
        svgPoint.setY((float)dstPts[1]);
        svgPointList.appendItem(svgPoint);
    }

    private void addArrowHead() {
        if (element == null) { return;}
        SVGPointList svgPointList = ((SVGPolylineElement)element).getPoints();
        if (svgPointList.getNumberOfItems() < 2) { return; }

        SVGPoint svgPoint1 = svgPointList.getItem(svgPointList.getNumberOfItems() - 2);
        SVGPoint svgPoint2 = svgPointList.getItem(svgPointList.getNumberOfItems() - 1);

        float arrowX = svgPoint2.getX();
        float arrowY = svgPoint2.getY();
        int size = 8;

        double angle = Math.atan2(svgPoint2.getY() - svgPoint1.getY(), svgPoint2.getX() - svgPoint1.getX());
        angle = Math.toDegrees(angle);
        AffineTransform rotateTx = AffineTransform.getRotateInstance(Math.toRadians((float)angle),arrowX,arrowY);

        // arrow head triangle
        float[] pts = new float[6];
        pts[0] = arrowX;
        pts[1] = arrowY;
        pts[2] = arrowX-size;
        pts[3] = arrowY-size;
        pts[4] = arrowX-size;
        pts[5] = arrowY+size;

        rotateTx.transform(pts, 0, pts, 0, 3);

        // create svg element
        
        CanvasModel model = canvas.getModel();
        SVGDocument svgDocument = model.getSVGDocument();
        String svgNS = SVGConstants.SVG_NAMESPACE_URI;
        SVGSVGElement svgElement = svgDocument.getRootElement();

        SVGPolygonElement arrowHead = (SVGPolygonElement)svgDocument.createElementNS(svgNS, SVGConstants.SVG_POLYGON_TAG);
        svgPointList = arrowHead.getPoints();

        SVGPoint svgPoint;
        for (int i=2;i<=6; i=i+2) {
            svgPoint = svgElement.createSVGPoint();
            svgPoint.setX(pts[i-2]);
            svgPoint.setY(pts[i-1]);
            svgPointList.appendItem(svgPoint);
        }

        SVGStylable stylable = (SVGStylable)arrowHead;
        String str;
        
        if (strokePaint != null) {
            if (strokePaint instanceof Color) {
                str = ColorConvertion.toHexString((Color) strokePaint);
                DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_STROKE_ATTRIBUTE, str);
                DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_FILL_ATTRIBUTE, str);
            }
        } else {
            DOMUtilities.updateProperty(stylable, null, SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
        }

        // append to parent G
        SVGElement arrow = (SVGElement)element.getParentNode();
        arrow.appendChild(arrowHead);
    }
}
