package uk.ac.starlink.ttools.plot2;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;

/**
 * Aggregates a Surface and the Rectangle that it is placed within.
 * It may also store decorations to be painted on top of the plot.
 * Class instances themselves may be compared for equality, but don't
 * do much else.  The {@link #createPlacement} factory method however
 * does some work in determining what external space is required for
 * legends etc.
 *
 * @author   Mark Taylor
 * @since    12 Feb 2013
 */
@Equality
public class PlotPlacement {

    private final Rectangle bounds_;
    private final Surface surface_;
    private final List<Decoration> decorations_;
    private static final int EXTERNAL_LEGEND_GAP = 10;
    private static final int SHADE_RAMP_WIDTH = 15;
    private static final int MIN_DIM = 24;

    /**
     * Constructor.
     *
     * @param   bounds   extenrnal bounds within which plot is to be placed
     * @param   surface  plot surface
     */
    public PlotPlacement( Rectangle bounds, Surface surface ) {
        bounds_ = bounds;
        surface_ = surface;
        decorations_ = new ArrayList<Decoration>();
    }

    /**
     * Returns the external bounds of this placement.
     *
     * @return   bounds
     */
    public Rectangle getBounds() {
        return bounds_;
    }

    /**
     * Returns the plot surface.
     *
     * @return  surface
     */
    public Surface getSurface() {
        return surface_;
    }

    /**
     * Returns a list of decorations to be painted over the finished plot.
     * This list may be modified to add or remove decoration items.
     *
     * @return   modifiable list of decoration objects
     */
    public List<Decoration> getDecorations() {
        return decorations_;
    }

    /**
     * Takes an icon containing plot background and layers,
     * and turns it into one positioned in an external rectangle
     * with surface foreground (axes) and other decorations.
     *
     * @param  dataIcon  icon as generated by
     *       {@link uk.ac.starlink.ttools.plot2.paper.PaperType#createDataIcon}
     * @return  final plot icon to be drawn at the graphics origin
     */
    public Icon createPlotIcon( final Icon dataIcon ) {
        final Rectangle plotBounds = surface_.getPlotBounds();
        return new Icon() {
            public int getIconWidth() {
                return bounds_.width;
            }
            public int getIconHeight() {
                return bounds_.height;
            }
            public void paintIcon( Component c, Graphics g, int x, int y ) {
                int xoff = x - bounds_.x;
                int yoff = y - bounds_.y;
                g.translate( xoff, yoff );
                paintPlot( c, g );
                g.translate( -xoff, -yoff );
            }
            private void paintPlot( Component c, Graphics g ) {
                Shape clip0 = g.getClip();
                g.clipRect( plotBounds.x, plotBounds.y,
                            plotBounds.width, plotBounds.height );
                dataIcon.paintIcon( c, g, plotBounds.x, plotBounds.y );
                g.setClip( clip0 );
                surface_.paintForeground( g );
                for ( Decoration dec : decorations_ ) {
                    dec.paintDecoration( g );
                }
            }
        };
    }

    @Override
    public boolean equals( Object o ) {
        if ( o instanceof PlotPlacement ) {
            PlotPlacement other = (PlotPlacement) o;
            return this.bounds_.equals( other.bounds_ )
                && this.surface_.equals( other.surface_ )
                && this.decorations_.equals( other.decorations_ );
        }
        else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        int code = 239991;
        code = 23 * code + bounds_.hashCode();
        code = 23 * code + surface_.hashCode();
        code = 23 * code + decorations_.hashCode();
        return code;
    }

    /**
     * Determines a plot placement given various inputs.
     * In particular it works out how much space is required for
     * decorations like axis annotations, legend etc.
     *
     * @param   box  external bounds of plot placement
     * @param   surfFact  surface factory
     * @param   profile  factory-specific surface profile
     * @param   aspect   factory-specific surface aspect
     * @param   withScroll  true if the placement should work well
     *                      with future scrolling
     * @param   legend   legend icon if required, or null
     * @param   legPos  legend position if intenal legend is required;
     *                  2-element (x,y) array, each element in range 0-1
     * @param   shadeAxis  shader axis if required, or null
     * @return   new plot placement
     */
    public static <P,A> PlotPlacement
                        createPlacement( final Rectangle box,
                                         SurfaceFactory<P,A> surfFact,
                                         P profile, A aspect,
                                         boolean withScroll, Icon legend,
                                         float[] legPos, ShadeAxis shadeAxis ) {

        /* This implementation currently places the legend in the top
         * right corner of the plot surface's requested insets,
         * which assumes that this requested inset space is not used.
         * That's true for existing surface implementations (that space
         * is just padding to make room for overflowing X axis labels)
         * but might not be for future implementations (e.g. right-hand
         * axis labels).  Adjust the implementation if that happens. */
         
        /* First guess at available space. */
        boolean hasExtLegend = legend != null && legPos == null;
        int legExtWidth = hasExtLegend
                        ? legend.getIconWidth() + EXTERNAL_LEGEND_GAP
                        : 0;
        final int shadeExtWidth;
        if ( shadeAxis == null ) {
            shadeExtWidth = 0;
        }
        else {
            Rectangle rampBox =
                new Rectangle( 0, 0, SHADE_RAMP_WIDTH, box.height );
            shadeExtWidth = rampBox.width
                          + shadeAxis.getRampInsets( rampBox ).right
                          + EXTERNAL_LEGEND_GAP;
        }
        Rectangle surfRect0 = new Rectangle( box );
        surfRect0.width = Math.max( MIN_DIM, surfRect0.width - legExtWidth );

        /* Get padding for first guess at surface. */
        Surface surf0 = surfFact.createSurface( surfRect0, profile, aspect );
        Insets insets0 = surf0.getPlotInsets( withScroll );
        insets0.right = Math.max( insets0.right, legExtWidth );
        insets0.right = Math.max( insets0.right, shadeExtWidth );

        /* Work out available space given padding required by first guess. */
        int gxlo = box.x + insets0.left;
        int gxhi = box.x + box.width - insets0.right;
        int gylo = box.y + insets0.top;
        int gyhi = box.y + box.height - insets0.bottom;
        Rectangle surfRect1 =
            new Rectangle( gxlo, gylo, gxhi - gxlo, gyhi - gylo );
        final Surface surf1 =
            surfFact.createSurface( surfRect1, profile, aspect );
        PlotPlacement placer = new PlotPlacement( box, surf1 );
        List<Decoration> decList = placer.getDecorations();
        
        /* Work out legend position. */
        int ylo1 = gylo;
        if ( legend != null ) {
            final int lx;
            final int ly;
            if ( legPos == null ) {
                lx = gxhi + EXTERNAL_LEGEND_GAP;
                ly = gylo;
                ylo1 += legend.getIconHeight() + EXTERNAL_LEGEND_GAP;
            }
            else {
                lx = gxlo + Math.round( ( gxhi - gxlo - legend.getIconWidth() )
                                      * legPos[ 0 ] );
                ly = gylo + Math.round( ( gyhi - gylo - legend.getIconHeight() )
                                      * legPos[ 1 ] );
            }
            decList.add( new Decoration( legend, lx, ly ) );
        }

        /* Work out shader axis position. */
        if ( shadeAxis != null ) {
            int sx = gxhi + EXTERNAL_LEGEND_GAP;
            int sy = ylo1;
            Rectangle rampBox =
                new Rectangle( sx, sy, SHADE_RAMP_WIDTH, gyhi - sy );
            Icon shadeIcon = shadeAxis.createAxisIcon( rampBox );
            decList.add( new Decoration( shadeIcon, sx, sy ) );
        }

        return placer;
    }
}
