/* Copyright (C) 2005-2011 Fabio Riccardi */

package com.lightcrafts.ui.crop;

import com.lightcrafts.model.CropBounds;
import com.lightcrafts.ui.mode.AbstractMode;

import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Dimension2D;
import java.util.LinkedList;

public class CropMode extends AbstractMode {

    private CropOverlay overlay;
    private AffineTransform xform;
    private CropControl control;
    private LinkedList<CropListener> listeners;

    public CropMode(boolean isRotateOnly) {
        listeners = new LinkedList<CropListener>();

        ResetAction reset = new ResetAction();

        control = new CropControl(reset,isRotateOnly);
        overlay = new CropOverlay(isRotateOnly);

        // The control imposes aspect ratio constraints on the overlay:
        control.setOverlay(overlay);

        xform = new AffineTransform();

        overlay.addMouseListener(new CropPopupMenu(this, reset));

        // Let Enter commit the crop and ESC cancel it.
        overlay.registerKeyboardAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    doCrop();
                }
            },
            KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
            JComponent.WHEN_IN_FOCUSED_WINDOW
        );
        overlay.registerKeyboardAction(
            new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    CropBounds resetBounds = getResetCropBounds();
                    setCropWithConstraints(resetBounds);
                    doCrop();
                }
            },
            KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
            JComponent.WHEN_IN_FOCUSED_WINDOW
        );
    }

    public void enter() {
        super.enter();
        overlay.modeEntered();
        control.unlock();
    }

    public void setResetCropBounds(CropBounds crop) {
        if (crop != null) {
            crop = CropBounds.transform(xform, crop);
        }
        control.setResetValue(crop);
    }

    public CropBounds getResetCropBounds() {
        CropBounds crop = control.getResetValue();
        if (crop != null) {
            try {
                final AffineTransform inverse = xform.createInverse();
                crop = CropBounds.transform(inverse, crop);
            }
            catch (NoninvertibleTransformException e) {
                crop = null;
            }
        }
        return crop;
    }

    Dimension2D getRotateLimitDimensions() {
        Dimension2D limit = overlay.getRotateLimitDimensions();
        if (limit != null) {
            double scale = xform.getScaleX();
            limit.setSize(
                limit.getWidth() / scale, limit.getHeight() / scale
            );
        }
        return limit;
    }

    void setRotateLimitDimensions(Dimension2D limit) {
        if (limit != null) {
            double scale = xform.getScaleX();
            limit.setSize(limit.getWidth() * scale, limit.getHeight() * scale);
            overlay.setRotateLimitDimensions(limit);
        }
    }

    public void addCropListener(CropListener listener) {
        listeners.add(listener);
    }

    public void removeCropListener(CropListener listener) {
        listeners.add(listener);
    }

    public CropControl getControl() {
        return control;
    }

    /**
     * Get the current crop, in image coordinates.
     */
    public CropBounds getCrop() {
        final CropBounds rect = overlay.getCrop();
        if (rect != null) {
            try {
                final AffineTransform inverse = xform.createInverse();
                return CropBounds.transform(inverse, rect);
            }
            catch (NoninvertibleTransformException e) {
                // An uninvertible transform would create bigger problems
                // than this.
                e.printStackTrace();
                return null;
            }
        }
        else {
            return null;
        }
    }

    /**
     * Set the current CropBounds, in image coordinates.
     * This is intended for undo/redo.
     */
    public void setCrop(CropBounds crop) {
        if (crop != null) {
            crop = CropBounds.transform(xform, crop);
        }
        overlay.setCrop(crop);
    }

    public void setCropWithConstraints(CropBounds crop) {
        if (crop != null) {
            crop = CropBounds.transform(xform, crop);
        }
        overlay.setCropWithConstraints(crop);
    }

    public void resetCrop() {
        for (CropListener listener : listeners) {
            listener.unCrop();
        }
    }

    public void doCrop() {
        if (isIn()) {
            final CropBounds crop = getCrop();
            for (CropListener listener : listeners) {
                listener.cropCommitted(crop);
            }
        }
    }

    public JComponent getOverlay() {
        return overlay;
    }

    public void addMouseInputListener(MouseInputListener listener) {
        overlay.addMouseListener(listener);
        overlay.addMouseMotionListener(listener);
    }

    public void removeMouseInputListener(MouseInputListener listener) {
        overlay.removeMouseListener(listener);
        overlay.removeMouseMotionListener(listener);
    }

    public void setTransform(AffineTransform xform) {
        // Reinitialize the crop, so it scales with the image and follows
        // rotations:
        final CropBounds crop = getCrop();
        final CropBounds reset = getResetCropBounds();
        Dimension2D rotateLimitDimensions = getRotateLimitDimensions();
        this.xform = xform;
        setCrop(crop);
        setResetCropBounds(reset);
        setRotateLimitDimensions(rotateLimitDimensions);
    }

    public void setUnderlayBounds(Rectangle bounds) {
        overlay.setUnderlayRect(bounds);
    }

    public boolean wantsAutocroll() {
        return true;
    }

    public void dispose() {
        overlay.dispose();
    }
}
