package org.forkalsrud.album.editor;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

/**
 * Editor
 *
 * @author knut@forkalsrud.org
 */
public class Editor
    extends JComponent
    implements MouseListener, MouseMotionListener, MouseWheelListener
{
    // Given from input image
    BufferedImage img;
    double imgScale;     // coordinates per bitmap pixel

    // What we choose
    double rotation;     // radians
    Rect roi;            // Measured in coordinates
    double scale;
    int side;

    // other
    AffineTransform screenToCoord;
    AffineTransform coordToScreen;
    boolean dirty = true;


    public Editor(BufferedImage img) {
        this.img = img;
        int w = img.getWidth();
        int h = img.getHeight();
        imgScale = 1d / Math.sqrt(w * w + h * h);

        roi = new Rect();
        roi.top = 0.4f;
        roi.bottom = -0.4f;
        roi.left = -0.4f;
        roi.right = 0.4f;

        rescale(imgScale * 680, null);
/*
        Dimension size = new Dimension(680, 680);
        setPreferredSize(size);
        setMinimumSize(size);
        setMaximumSize(size);
        rotation = 0.0;

        Rectangle initialMask = new Rectangle(100, 100, 200, 200);
        mask = new CropFrame();
        mask.setBounds(initialMask);
        mask.setVisible(true);
        mask.addMouseListener(mask);
        mask.addMouseMotionListener(mask);
        add(mask);

        SelectionEditor ml = new SelectionEditor();
*/
        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
    }



    void rescale(double newScale, Point center) {

        // TODO Get the visible rect
        // TODO find "center" in visible rect. If null or not there make it the actual center
        // TODO Make up a virtual visible rect by scaling actual visible rect
        // TODO calculate top left coordinate in new virtual visible rect
        // TODO translate visible rect to same top left as virtual
        // TODO scroll to visible rect
        scale = newScale;
        side = (int)Math.round(scale / imgScale);
        Dimension size = new Dimension(side, side);
        dirty = true;
        roi.cache = null;
//        System.out.println("Side: " + side);
        setSize(size);
        setPreferredSize(size);
        setMinimumSize(size);
        setMaximumSize(size);
        revalidate();
        repaint();
    }


    @Override
    protected void paintComponent(Graphics g1) {
        super.paintComponent(g1);

        if (dirty) {
            AffineTransform coordToScreen = new AffineTransform();
            coordToScreen.translate(side/2d, side/2d);
            coordToScreen.scale(side, -side); 

            AffineTransform screenToCoord = new AffineTransform();
            screenToCoord.scale(1d/side, -1d/side); 
            screenToCoord.translate(-side/2d, -side/2d);

            this.screenToCoord = screenToCoord;
            this.coordToScreen = coordToScreen;
            dirty = false;
        }

        Graphics2D g = (Graphics2D)g1;
        AffineTransform orig = g.getTransform();
        g.transform(this.coordToScreen);

        AffineTransform imageToCoord = new AffineTransform();
        imageToCoord.rotate(rotation * Math.PI / 180d);
        imageToCoord.scale(imgScale, -imgScale);
        imageToCoord.translate(-img.getWidth()/2d, -img.getHeight()/2d);

        g.drawImage(img, imageToCoord, this);

        // Crosshair
/*
        g.setStroke(new BasicStroke(0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[] { 0.005f, 0.005f }, 0));
        g.draw(new Line2D.Double(-0.5, 0, 0.5, 0));
        g.draw(new Line2D.Double(0, -0.5, 0, 0.5));
*/
        g.setTransform(orig);

        g.setXORMode(Color.white); //Color of line varies
        g.setStroke(new BasicStroke(0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[] { 5f, 5f }, 0));
        g.draw(roi.getScreenRectangle());
//        roi.cache = null;
    }






    JComponent createControlPanel() {
        BevelBorder border = new BevelBorder(BevelBorder.LOWERED);
        JPanel hp = new JPanel(); 
        hp.setLayout(new BoxLayout(hp, BoxLayout.Y_AXIS)); 
        hp.setBorder(
                new TitledBorder(border,  "Controls", TitledBorder.LEFT, 
                        TitledBorder.ABOVE_TOP)); 
        hp.add(new JLabel("Rotation"));
        final JSlider rotationSlider = new JSlider(-180, 180, 0);
        rotationSlider.setPaintTicks(true);
        rotationSlider.setMajorTickSpacing(15); 
        rotationSlider.addChangeListener(new ChangeListener() {
            int previousValue = 0;
            @Override
            public void stateChanged(ChangeEvent e) {
                int value = rotationSlider.getValue();
                if (value != previousValue) {
                    previousValue = value;
                    rotation = value;
                    repaint();
                }
            }
        });
        hp.add(rotationSlider);


        final JCheckBox renderQualityControl = new JCheckBox("Render Quality", false);
        renderQualityControl.addChangeListener(new ChangeListener() {
                boolean previousValue = false;
                @Override
                public void stateChanged(ChangeEvent e) {
                   boolean value = renderQualityControl.isSelected();
                    if (value != previousValue) {
                        previousValue = value;
                        repaint();
                    }
                }
            });
        hp.add(renderQualityControl);


        hp.add(new JLabel("Zoom"));
        final JSlider zoomSlider = new JSlider(1, 300, (int) Math.round(scale * 100));
        zoomSlider.setPaintTicks(true);
        zoomSlider.setMajorTickSpacing(10); 
        zoomSlider.addChangeListener(new ChangeListener() {
            int previousValue = 0;
            @Override
            public void stateChanged(ChangeEvent e) {
                int value = zoomSlider.getValue();
                if (value != previousValue) {
                    previousValue = value;
                    rescale(value * 0.01, null);
                }
            }
        });
        hp.add(zoomSlider);


        hp.add(new JPanel());

        return hp;
    }




    public static void main(String args[])
        throws Exception
    {
        Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpg");
        ImageReader reader = readers.next();
        ImageInputStream iis = ImageIO.createImageInputStream(
            new File(args[0]));
        reader.setInput(iis, true);
        ImageReadParam param = reader.getDefaultReadParam();
//                param.setSourceSubsampling(8, 8, 0, 0);
        final BufferedImage raw = reader.read(0, param);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                Editor x  = new Editor(raw);

                JFrame frame = new JFrame("Editor");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                Container c = frame.getContentPane();
                c.setLayout(new BorderLayout());
                c.add(x.createControlPanel(), BorderLayout.EAST);
                JScrollPane scroller = new JScrollPane(x);
                scroller.setWheelScrollingEnabled(false);
                c.add(scroller, BorderLayout.CENTER);

                frame.pack();
                frame.setVisible(true);
            }
        });
    }



    final int resizeBand = 3;
    boolean isDragging = false;
    Stroke stroke = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[] { 5f, 5f }, 0);



    @Override
    public void mouseClicked(MouseEvent e) {
        // noop
    }



    @Override
    public void mouseEntered(MouseEvent e) {
        // noop
    }



    @Override
    public void mouseExited(MouseEvent e) {
        // noop
    }


    @Override
    public void mousePressed(MouseEvent e) {
        int r = findRegion(e.getX(), e.getY());
        if (r < 8) {
            edge = r;
            isDragging = true;
        } else {
            isDragging = false;
        }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        if (isDragging) {
            updateFrame(e.getPoint());
        }
    }



    @Override
    public void mouseDragged(MouseEvent e) {
        if (isDragging) {
            updateFrame(e.getPoint());
        }
    }


    boolean is(int who, int alt0, int alt1, int alt2) {
        return who == alt0 || who == alt1 || who == alt2;
    }

    int[] oppositeX = new int[] { NE, N, NW, W, SW, S, SE, E };
    int[] oppositeY = new int[] { SW, S, SE, E, NE, N, NW, W };
    Cursor[] cursors = new Cursor[] {
            Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR),
            Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR),
    };
    String[] edgeName = new String[] { "NW", "N", "NE", "E", "SE", "S", "SW", "W" };
    static final int NW = 0;
    static final int N  = 1;
    static final int NE = 2;
    static final int E  = 3;
    static final int SE = 4;
    static final int S  = 5;
    static final int SW = 6;
    static final int W =  7;
    int edge = SE;
    Rectangle frame = new Rectangle();

    void updateFrame(Point p) {
        frame = roi.getScreenRectangle();
        if (is(edge, NW, W, SW)) {
            frame.width += frame.x - p.x;
            frame.x = p.x;
        }
        if (is(edge, NE, E, SE)) {
            frame.width = p.x - frame.x;
        }
        if (is(edge, NW, N, NE)) {
            frame.height += frame.y - p.y;
            frame.y = p.y;
        }
        if (is(edge, SW, S, SE)) {
            frame.height = p.y - frame.y;
        }
        if (frame.width < 0) {
            frame.x += frame.width;
            frame.width = -frame.width;
            edge = oppositeX[edge];
        }
        if (frame.height < 0) {
            frame.y += frame.height;
            frame.height = -frame.height;
            edge = oppositeY[edge];
        }
        roi.setScreenRectangle(frame);
        repaint();
    }


    boolean within(int x, int target) {
        return x >= target - resizeBand && x <= target + resizeBand;
    }

    int findRegion(int ex, int ey) {
        Rectangle r = roi.getScreenRectangle();
        int c;

        if (within(ex, r.x + r.width)) {
            if (within(ey, r.y + r.height)) {
                c = SE;
            } else if (within(ey, r.y)) {
                c = NE;
            } else {
                c = E;
            }
        } else if (within(ex, r.x)) {
            if (within(ey, r.y + r.height)) {
                c = SW;
            } else if (within(ey, r.y)) {
                c = NW;
            } else {
                c = W;
            }
        } else {
            if (within(ey, r.y + r.height)) {
                c = S;
            } else if (within(ey, r.y)) {
                c = N;
            } else {
                c = 8;
            }
        }
        return c;
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        setCursor(cursors[findRegion(e.getX(), e.getY())]);
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        int units = e.getUnitsToScroll();
        System.out.println(units);
        rescale(scale * (1f + units / 100f), e.getPoint());
        //        this.
    }





    static void log(String x) {
        System.out.println(x);
    }


    class Rect {
        float top;
        float left;
        float bottom;
        float right;
        Rectangle cache = null;

        public void setScreenRectangle(Rectangle frame) {
            Point2D.Float screenTopLeft = new Point2D.Float(frame.x, frame.y);
            Point2D.Float coordTopLeft = new Point2D.Float();
            Point2D.Float screenBottomRight = new Point2D.Float(frame.x + frame.width, frame.y + frame.height);
            Point2D.Float coordBottomRight = new Point2D.Float();

            if (dirty) {
                new Exception().printStackTrace(System.out);
            }
            screenToCoord.transform(screenTopLeft, coordTopLeft);
            screenToCoord.transform(screenBottomRight, coordBottomRight);

            top = coordTopLeft.y;
            left = coordTopLeft.x;
            bottom = coordBottomRight.y;
            right = coordBottomRight.x;
/*
            System.out.println("rect: " + left + ", " + top + ", " + right + ", " + bottom);
            Rectangle stored = calculateScreenRectangle();
            Rectangle delta = new Rectangle();
            delta.x = frame.x - stored.x;
            delta.y = frame.y - stored.y;
            delta.width = frame.width - stored.width;
            delta.height = frame.height - stored.height;
            System.out.println("delta: " + delta);
*/
            cache = frame;
        }

        Rectangle calculateScreenRectangle() {
            Point2D.Float coordTopLeft = new Point2D.Float(left, top);
            Point2D.Float screenTopLeft = new Point2D.Float();
            Point2D.Float coordBottomRight = new Point2D.Float(right, bottom);
            Point2D.Float screenBottomRight = new Point2D.Float();
        
            coordToScreen.transform(coordTopLeft, screenTopLeft);
            if (dirty) {
                new Exception().printStackTrace(System.out);
            }
            coordToScreen.transform(coordBottomRight, screenBottomRight);

            int x = Math.round(screenTopLeft.x);
            int y = Math.round(screenTopLeft.y);
            int w = Math.round(screenBottomRight.x - screenTopLeft.x);
            int h = Math.round(screenBottomRight.y - screenTopLeft.y);
            return new Rectangle(x, y, w, h);
        }

        public Rectangle getScreenRectangle() {
            if (cache == null) {
                cache = calculateScreenRectangle();
            }
            return cache;
        }
    }

}

// eof
