package adecWatt.view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import misc.StateNotifier;
import misc.DimensionDouble;
import misc.Util;

// XXX candidat "misc"
/**
   Affiche des zones transparentes avec des poignées de déformation (bords ou coins).
*/
@SuppressWarnings ("serial") public class JZones extends JLayeredPane {

    static public final int minPixels = 10;
    static public final String
	BroadcastSelectedLocationChange	= "SelectedLocationChange",
	BroadcastSelectedRateChange	= "SelectedRateChange",
	BroadcastSelectedChange		= "SelectedChange",
	BroadcastCreateZone		= "CreateZone",
	BroadcastChangeScale		= "ChangeScale";
    static public final Color defineColor = new Color (128, 128, 128, 64);

    // ========================================
    public StateNotifier stateNotifier = new StateNotifier ();    

    private DimensionDouble realSize;
    private Rectangle2D.Double backgroundShape;
    private double rate;
    // gestion du zoom
    private double scale = 1, initScale = 1, minScale = 1, maxScale = 1, scaleStep = 1;
    private double minSide;

    /** Fond du panneau. */
    private BufferedImage background;
    private AffineTransform bgat;

    public DimensionDouble	getRealSize ()	{ return realSize; }
    public double		getScale ()	{ return scale; }
    public Rectangle2D.Double getMaxRate (double rate) {
	double width = realSize.width, height = realSize.height;
	if (rate == 0 || rate == this.rate)
	    ;
	else if (rate < this.rate)
	    width = rate*height;
	else 
	    height = width/rate;
	return new Rectangle2D.Double (0, 0, width, height);
    }

    public class Zone {
	private Rectangle2D.Double rectangle;
	private Color color;
	private int rotation;
	private Zone pair;
	private BufferedImage image;
	Rectangle2D.Double imageRectangle;

	public Zone (Rectangle2D.Double rectangle, Color color, BufferedImage image) {
	    this.rectangle = new Rectangle2D.Double (rectangle.x, rectangle.y, rectangle.width, rectangle.height);
	    this.color = color;
	    setImage (image);
	}
	public Zone (Rectangle2D.Double rectangle, Zone pair, BufferedImage image) {
	    this.rectangle = new Rectangle2D.Double (rectangle.x, rectangle.y, rectangle.width, rectangle.height);
	    this.color = pair.color;
	    this.pair = pair;
	    setImage (image);
	}
	public boolean			hasPair ()	{ return pair != null; }
	public Zone			getPair ()	{ return pair; }
	public BufferedImage		getImage ()	{ return image; }
	public DimensionDouble		getRealSize ()	{ return realSize; }
	public Rectangle2D.Double	getRectangle ()	{ return rectangle; }
	public int			getRotation ()	{ return rotation; }
	public void			turnLeft ()	{ setRotation (rotation+1); }
	public void			turnRight ()	{ setRotation (rotation-1); }
	public void setImage (BufferedImage image) {
	    this.image = image;
	    imageRectangle = null;
	    if (image == null)
		return;
	    imageRectangle = new Rectangle2D.Double (0, 0, image.getWidth (), image.getHeight ());
	    repaint ();
	}
	public void drawImage (Graphics2D g2) {
	    if (image == null)
		return;
	    g2.drawImage (image, new AffineTransform (), null);
	}
	public double getScale () {
	    return rotation % 2 == 1 ? rectangle.width/pair.rectangle.height : rectangle.width/pair.rectangle.width;
	}
	private void setRotation (int rotation) {
	    rotation += 4;
	    rotation %= 4;
	    if (this.rotation == rotation)
		return;
	    if ((this.rotation+rotation)%2 == 1) {
		double tmp = rectangle.width;
		rectangle.width = rectangle.height;
		rectangle.height = tmp;
		fitLandscape ();
	    } else if (selectedZone == this)
		jZoneHandler.updateRectangle (rectangle);
	    this.rotation = rotation;
	    repaint ();
	}
	public void updatePairRate () {
	    if (pair == null)
		return;
	    double rate = pair.rectangle.width/pair.rectangle.height;
	    if (rotation % 2 == 1)
		rate = 1/rate;
	    setRate (rate);
	}
	private void setRate (double rate) {
	    double width = rectangle.width, height = rectangle.height;
	    double prevRate = width / height;
	    if (prevRate == rate)
		return;
	    double maxWidth = realSize.width - rectangle.x, maxHeight = realSize.height - rectangle.y;
	    if (prevRate > 1) {
		height = width/rate;
		if (height > maxHeight) {
		    height = maxHeight;
		    width = rate*height;
		}
	    } else {
		width = rate*height;
		if (width > maxWidth) {
		    width = maxWidth;
		    height = width/rate;
		}
	    }
	    setBounds (rectangle.x, rectangle.y, width, height);
	}
	public void setLocation (double x, double y) {
	    x = Math.max (.0, Math.min (realSize.width-rectangle.width, x));
	    y = Math.max (.0, Math.min (realSize.height-rectangle.height, y));
	    rectangle.x = x;
	    rectangle.y = y;
	    if (selectedZone == this)
		jZoneHandler.updateRectangle (rectangle);
	    repaint ();
	    stateNotifier.broadcastUpdate (BroadcastSelectedLocationChange);
	}
	public void setBounds (double x, double y, double width, double height) {
	    rectangle.width = Math.max (minSide, Math.min (realSize.width, width));
	    rectangle.height = Math.max (minSide, Math.min (realSize.height, height));
	    setLocation (x, y);
	    stateNotifier.broadcastUpdate (BroadcastSelectedRateChange);
	}
	public void fitLandscape () {
	    double rate = rectangle.width/rectangle.height;
	    if (rectangle.width > realSize.width) {
		rectangle.width = realSize.width;
		rectangle.height = rectangle.width/rate;
	    }
	    if (rectangle.height > realSize.height) {
		rectangle.height = realSize.height;
		rectangle.width = rate*rectangle.height;
	    }
	    rectangle.x = Math.min (rectangle.x, realSize.width - rectangle.width);
	    rectangle.y = Math.min (rectangle.y, realSize.height - rectangle.height);
	    if (selectedZone == this)
		jZoneHandler.updateRectangle (rectangle);
	}
	public void projection (Graphics2D g2) {
	    Rectangle2D.Double refRectangle = pair == null ? imageRectangle : pair.rectangle;
	    double copyScale = rotation%2 == 1 ? rectangle.width/refRectangle.height : rectangle.width/refRectangle.width;
	    double theta = Math.PI*rotation/2;
	    double halfWidth = refRectangle.width/2, halfHeight = refRectangle.height/2;
	    g2.clip (rectangle);
	    g2.translate (rectangle.x, rectangle.y);
	    g2.scale (copyScale, copyScale);
	    if (rotation % 2 == 1)
		g2.translate (halfHeight, halfWidth);
	    else
		g2.translate (halfWidth, halfHeight);
	    g2.rotate (-theta);
	    g2.translate (-halfWidth, -halfHeight);
	    g2.translate (-refRectangle.x, -refRectangle.y);
	}
    };

    // ========================================
    ArrayList<Zone> zones = new ArrayList<Zone> ();
    private JZoneHandler jZoneHandler;
    private boolean cornerSelection, createZone = true;

    public void setCornerSelection (boolean cornerSelection) {
	if (this.cornerSelection == cornerSelection)
	    return;
	this.cornerSelection = cornerSelection;
	if (jZoneHandler != null)
	    jZoneHandler.setVisible (false);
	jZoneHandler = new JZoneHandler (cornerSelection);
    }
    public void setCreateZone (boolean createZone) {
	this.createZone = createZone;
	repaint ();
    }
    public boolean setLandscape (boolean landscape) {
	if (rate > 1 == landscape)
	    return false;
	realSize = new DimensionDouble (realSize.height, realSize.width);
	backgroundShape = new Rectangle2D.Double (.0, .0, realSize.width, realSize.height);
	rate = realSize.width/realSize.height;
	for (Zone zone : zones)
	    zone.fitLandscape ();
	repaint ();
	return true;
    }
    public void setCenterScale (double scale) {
	JViewport jViewport = getJScrollPane ().getViewport ();
	Rectangle prevView = jViewport.getViewRect ();
	Point2D.Double realCenter = scaleViewToModel (new Point (prevView.x+prevView.width/2, prevView.y+prevView.height/2));
	setScale (scale);
	Rectangle nextView = jViewport.getViewRect ();
	Point viewCenter = scaleModelToView (realCenter);
	viewCenter.translate (-nextView.width/2, -nextView.height/2);
	if (viewCenter.x < 0)
	    viewCenter.x = 0;
	if (viewCenter.y < 0)
	    viewCenter.y = 0;
	jViewport.setViewPosition (viewCenter);
    }
    public void resetScale () { setScale (initScale); }
    public void setScale (double scale) {
	scale = Math.max (minScale, Math.min (maxScale, scale));
	this.scale = scale;
	minSide = minPixels/scale;
	setPreferredSize (scaleModelToView (realSize));
	if (selectedZone != null)
	    jZoneHandler.updateRectangle (selectedZone.rectangle);
	if (background != null)
	    bgat = AffineTransform.getScaleInstance (realSize.width/background.getWidth (), realSize.height / background.getHeight ());
	repaint ();
	stateNotifier.broadcastUpdate (BroadcastChangeScale);
    }

    // ========================================
    public void setBackground (BufferedImage background) {
	this.background = background;
	setScale (scale);
    }

    public JZones (DimensionDouble realSize, int maxSide) {
	this.realSize = realSize;
	setCornerSelection (true);
	backgroundShape = new Rectangle2D.Double (.0, .0, realSize.width, realSize.height);
	rate = realSize.width/realSize.height;
	if (maxSide > 0)
	    initScale = maxSide/Math.max (realSize.getWidth (), realSize.getHeight ());
	scale = initScale;
	minScale = initScale/10;
	maxScale = initScale*10;
	scaleStep = minScale/2;
	addMouseListener (jZonesMouseAdapter);
	addMouseMotionListener (jZonesMouseAdapter);
	addMouseWheelListener (jZonesMouseAdapter);
	setScale (scale);
    }

    // ========================================
    public Zone addZone (Color color) {
	return addZone (new Zone (getMaxRate (0), color, null));
    }
    public Zone addZone (Color color, Rectangle2D.Double rectangle) {
	return addZone (new Zone (rectangle, color, null));
    }
    public Zone addZone (Color color, BufferedImage image) {
	return addZone (new Zone (getMaxRate (((double)image.getWidth ())/image.getHeight ()), color, image));
    }
    public Zone addZone (Zone pair, BufferedImage image) {
	return addZone (new Zone (getMaxRate (pair.rectangle.width/pair.rectangle.height), pair, image));
    }
    public Zone addZone (Zone zone) {
	zones.add (zone);
	repaint ();
	return zone;
    }
    public void turnZoneLeft () {
	if (selectedZone == null)
	    return;
	selectedZone.turnLeft ();
    }
    public void turnZoneRight () {
	if (selectedZone == null)
	    return;
	selectedZone.turnRight ();
    }
    public void removeZone (Zone zone) {
	if (zone == null)
	    return;
	if (selectedZone == zone)
	    selectZone (null);
	zones.remove (zone);
	repaint ();
    }

    public void paint (Graphics g) {
	Graphics2D g2 = (Graphics2D) g;
	AffineTransform iat = g2.getTransform ();
	Shape clip = g2.getClip ();
	g2.scale (scale, scale);
	g2.setColor (Color.WHITE);
	g2.fill (backgroundShape);
	if (background != null)
	    g2.drawImage (background, bgat, null);
	g2.setColor (Color.BLACK);
	g2.setStroke (new BasicStroke ((float) (1/scale)));
	g2.draw (backgroundShape);
	for (Zone zone : zones) {
	    if (zone.pair == null && zone.image == null)
		continue;
	    double refBgScale = zone.pair == null ? 1 : zone.pair.getRealSize ().width/zone.image.getWidth ();
	    g2.setTransform (iat);
	    g2.setClip (clip);
	    g2.scale (scale, scale);
	    zone.projection (g2);
	    AffineTransform bat = new AffineTransform ();
	    bat.scale (refBgScale, refBgScale);
	    g2.drawImage (zone.image, bat, null);
	}
	g2.setTransform (iat);
	g2.setClip (clip);
	g2.scale (scale, scale);
	for (Zone zone : zones) {
	    Color color = zone.color;
	    Color aColor = new Color (color.getRed (), color.getGreen (), color.getBlue (), 64);
	    g2.setColor (aColor);
	    g2.fill (zone.rectangle);
	}
	g2.setTransform (iat);
	g2.setColor (defineColor);
	if (createZone && defineSelectionStart != null) {
	    Rectangle selection = new Rectangle (defineSelectionStart);
	    selection.add (defineSelectionEnd);
	    g2.fill (selection);
	}
	paintChildren (g);
    }

    public DimensionDouble scaleViewToModel (Dimension size) {
	return new DimensionDouble (size.width/scale, size.height/scale);
    }
    public Dimension scaleModelToView (DimensionDouble size) {
	return new Dimension ((int)(size.getWidth ()*scale), (int)(size.getHeight ()*scale));
    }
    public Point2D.Double scaleViewToModel (Point pos) {
    	return new Point2D.Double (pos.x/scale, pos.y/scale);
    }
    public Point scaleModelToView (Point2D pos) {
    	return new Point ((int)(pos.getX ()*scale), (int)(pos.getY ()*scale));
    }

    private JScrollPane getJScrollPane () {
	for (Container parent = getParent () ; parent != null; parent = parent.getParent ())
	    if (parent instanceof JScrollPane)
		return (JScrollPane) parent;
	return null;
    }
    public void scrollView (boolean horizontal, int delta) {
	JScrollPane jScrollPane = getJScrollPane ();
	JScrollBar jScrollBar = horizontal ? jScrollPane.getHorizontalScrollBar () : jScrollPane.getVerticalScrollBar ();
	int min = jScrollBar.getMinimum ();
	int max = jScrollBar.getMaximum ();
	int step = (max-min)/10;
	jScrollBar.setValue (Math.min (max, Math.max (min, jScrollBar.getValue ()+delta*step)));
    }

    public void changeScale (Point mousePos, int delta) {
	JViewport jViewport = getJScrollPane ().getViewport ();
	Point viewPosA = jViewport.getViewPosition ();
	Point2D.Double realPos = scaleViewToModel (mousePos);
	setScale (delta < 0 ? scale+scaleStep : scale-scaleStep);
	Point viewPosB = scaleModelToView (realPos);
	viewPosB.translate (-mousePos.x+viewPosA.x, -mousePos.y+viewPosA.y);
	if (viewPosB.x < 0)
	    viewPosB.x = 0;
	if (viewPosB.y < 0)
	    viewPosB.y = 0;
	jViewport.setViewPosition (viewPosB);
    }

    // ========================================
    private Zone selectedZone;
    public Zone getSelectedZone () {
	    return selectedZone;
    }
    public void selectLastZone () {
	if (zones.size () < 0)
	    return;
	selectZone (zones.get (zones.size ()-1));
    }
    public void selectZone (Zone zone) {
	if (selectedZone == zone)
	    return;
	selectZoneQuiet (zone);
	stateNotifier.broadcastDisplay (BroadcastSelectedChange, this, zone);
    }
    public Zone findPair (Zone pair) {
	if (pair == null) {
	    return null;
	}
	for (Zone zone : zones)
	    if (zone.pair == pair) {
		return zone;
	    }
	return null;
    }
    public void selectZoneQuiet (Zone zone) {
	selectedZone = zone;
	if (selectedZone != null)
	    jZoneHandler.updateRectangle (selectedZone.rectangle);
	jZoneHandler.setVisible (selectedZone != null);
    }

    private Point defineSelectionStart, defineSelectionEnd;
    private MouseAdapter jZonesMouseAdapter = new MouseAdapter () {
	    Point2D.Double mousePosOnZone;
	    public void mousePressed (MouseEvent e) {
		if (SwingUtilities.isRightMouseButton (e)) {
		    selectZone (null);
		    return;
		}
		Point2D.Double posInPage = scaleViewToModel (e.getPoint ());
		ArrayList<Zone> overlap = new ArrayList<Zone> ();
		for (Zone zone : zones)
		    if (zone.rectangle.contains (posInPage.x, posInPage.y))
			overlap.add (zone);
		if (overlap.size () < 1) {
		    selectZone (null);
		    defineSelectionStart = defineSelectionEnd = e.getPoint ();
		    return;
		}
		JZones.this.selectZone (overlap.get ((zones.indexOf (selectedZone)+1)%overlap.size ()));
		mousePosOnZone = posInPage;
		mousePosOnZone.x -= selectedZone.rectangle.x;
		mousePosOnZone.y -= selectedZone.rectangle.y;
	    }
	    public void mouseReleased (MouseEvent e) {
		if (!createZone || defineSelectionStart == null)
		    return;
		Rectangle selection = new Rectangle (defineSelectionStart);
		selection.add (defineSelectionEnd);
		defineSelectionStart = defineSelectionEnd = null;
		Point2D.Double start = scaleViewToModel (selection.getLocation ());
		DimensionDouble dim = scaleViewToModel (selection.getSize ());
		Rectangle2D.Double area = new Rectangle2D.Double (start.x, start.y, dim.width, dim.height);
		if (area.width > minSide && area.height > minSide)
		    stateNotifier.broadcastDisplay (BroadcastCreateZone, area);
		repaint ();
	    }
	    public void mouseDragged (MouseEvent e) {
		if (defineSelectionStart != null) {
		    defineSelectionEnd = e.getPoint ();
		    repaint ();
		}
		if (selectedZone == null)
		    return;
		Point2D.Double posInPage = scaleViewToModel (e.getPoint ());
		selectedZone.setLocation (posInPage.x - mousePosOnZone.x, posInPage.y - mousePosOnZone.y);
	    }
	    public void mouseWheelMoved (MouseWheelEvent e) {
		try {
		    if (0 != (e.getModifiersEx () & (MouseWheelEvent.SHIFT_MASK|MouseWheelEvent.SHIFT_DOWN_MASK)))
			scrollView (true, e.getWheelRotation ());
		    else if (0 != (e.getModifiersEx () & (MouseWheelEvent.CTRL_MASK|MouseWheelEvent.CTRL_DOWN_MASK)))
			changeScale (e.getPoint (), e.getWheelRotation ());
		    else
			scrollView (false, e.getWheelRotation ());
		} catch (Exception e2) {
		}
	    }
	};

    // ========================================
    static public final ImageIcon handlerImage = Util.loadActionIcon ("handler");
    public class JZoneHandler {
	private boolean cornerSelection;
	private double[] x = new double [4], y = new double [4];
	private JLabel[] handlers = new JLabel [4];
	private double oposedX, oposedY;

	public JZoneHandler (final boolean cornerSelection) {
	    this.cornerSelection = cornerSelection;
	    for (int i = 0; i < 4; i++) {
		final JLabel handler = new JLabel (handlerImage);
		final int index = i;
		handlers [i] = handler;
		handler.setSize (8, 8);
		MouseAdapter mouseAdapter = new MouseAdapter () {
			Point mousePosOnHandler;
			public void mousePressed (MouseEvent e) {
			    if (SwingUtilities.isRightMouseButton (e)) {
				selectZone (null);
				return;
			    }
			    oposedX = x[(index+2)%4];
			    oposedY = y[(index+2)%4];
			    mousePosOnHandler = e.getPoint ();
			}
			public void mouseDragged (MouseEvent e) {
			    Point deltaMousePos = e.getPoint ();
			    deltaMousePos.x -= mousePosOnHandler.x;
			    deltaMousePos.y -= mousePosOnHandler.y;
			    Point2D.Double deltaInPage = scaleViewToModel (deltaMousePos);
			    double newX = Math.max (.0, Math.min (realSize.width, x[index]+deltaInPage.x));
			    double newY = Math.max (.0, Math.min (realSize.height, y[index]+deltaInPage.y));
			    if (cornerSelection) {
				double cx = (oposedX+newX)/2;
				double cy = (oposedY+newY)/2;
				double width = Math.abs (oposedX-newX);
				double height = Math.abs (oposedY-newY);
				selectedZone.setBounds (cx - width/2, cy - height/2, width, height);
				return;
			    }
			    Rectangle2D.Double rectangle = selectedZone.rectangle;
			    double rate = rectangle.width/rectangle.height;
			    if (index%2 == 0) {
				double height = Math.max (minSide, index == 0 ? oposedY-newY : newY-oposedY);
				double width = Math.max (minSide, Math.min (rate*height, Math.min (x[0], realSize.width - x[0])*2));
				height = width/rate;
				selectedZone.setBounds (x[0]-width/2, index == 0 ? y[2]-height : y[0], width, height);
			    } else {
				double width = Math.max (minSide, index == 3 ? oposedX-newX : newX-oposedX);
				double height = Math.max (minSide, Math.min (width/rate, Math.min (y[1], realSize.height - y[1])*2));
				width = rate*height;
				selectedZone.setBounds (index == 3 ? x[1]-width : x[3], y[1]-height/2, width, height);
			    }
			}
		    };
		handler.addMouseListener (mouseAdapter);
		handler.addMouseMotionListener (mouseAdapter);
		JZones.this.add (handler);
		JZones.this.setLayer (handler, 10+i);
		handler.setVisible (false);
	    }
	}
	public void updateRectangle (Rectangle2D.Double rectangle) {
	    if (cornerSelection) {
		x[0] = x[3] = rectangle.x;
		y[0] = y[1] = rectangle.y;
		x[1] = x[2] = rectangle.x+rectangle.width;
		y[2] = y[3] = rectangle.y+rectangle.height;
	    } else {
		x[3] = rectangle.x;
		x[0] = x[2] = rectangle.x+rectangle.width/2;
		x[1] = rectangle.x+rectangle.width;
		y[0] = rectangle.y;
		y[1] = y[3] = rectangle.y+rectangle.height/2;
		y[2] = rectangle.y+rectangle.height;
	    }
	    for (int i = 0; i < 4; i++)
		handlers[i].setLocation ((int) (x[i]*scale-4), (int) (y[i]*scale-4));
	}
	public void setVisible (boolean visible) {
	    for (int i = 0; i < 4; i++)
		handlers[i].setVisible (visible);
	}
    };

    public class SizeSlider extends JSlider implements ChangeListener {
	public SizeSlider () {
	    super (0, 200, 100);
	    setMajorTickSpacing (50);
	    setMinorTickSpacing (10);
	    setPaintTicks (true);
	    setSnapToTicks (true);
	    addChangeListener (this);
	    stateNotifier.addUpdateObserver (this, JZones.BroadcastChangeScale);
	}
	public void stateChanged (ChangeEvent e) {
	    setCenterScale (Math.pow (10, getValue ()/100.-1)*initScale);
	}
	public void updateChangeScale () {
	    removeChangeListener (this);
	    setValue ((int) (100*(Math.log10 (scale/initScale)+1)));
	    addChangeListener (this);
	}
    }

    // ========================================
}
