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.  Several static methods however are provided to assist
 * in creating instances, in particular doing the non-trivial work to
 * determine how much external space is required for legends etc.
 *
 * <p>Note instances of this class are not immutable, since the decoration
 * list may be changed.
 *
 * @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;

    /**
     * Constructs a placement with no decorations.
     *
     * @param   bounds   external bounds within which plot is to be placed
     * @param   surface  plot surface
     */
    public PlotPlacement( Rectangle bounds, Surface surface ) {
        this( bounds, surface, new Decoration[ 0 ] );
    }

    /**
     * Constructs a placement with an initial list of decorations.
     *
     * @param   bounds   external bounds within which plot is to be placed
     * @param   surface  plot surface
     * @param   decorations  initial list of decorations;
     *          note more can be added later
     */
    public PlotPlacement( Rectangle bounds, Surface surface,
                          Decoration[] decorations ) {
        bounds_ = bounds;
        surface_ = surface;
        decorations_ =
            new ArrayList<Decoration>( Arrays.asList( decorations ) );
    }

    /**
     * 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;
    }

    /**
     * Convenience method to create a plot placement given various inputs.
     * In particular it works out how much space is required for
     * decorations like axis annotations, legend etc.
     *
     * @param   extBounds  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   title   title text, or null
     * @param   shadeAxis  shader axis if required, or null
     * @return   new plot placement
     */
    public static <P,A> PlotPlacement
            createPlacement( Rectangle extBounds, SurfaceFactory<P,A> surfFact,
                             P profile, A aspect, boolean withScroll,
                             Icon legend, float[] legPos, String title,
                             ShadeAxis shadeAxis ) {
        Rectangle dataBounds =
            calculateDataBounds( extBounds, surfFact, profile, aspect,
                                 withScroll, legend, legPos, title, shadeAxis );
        Surface surf = surfFact.createSurface( dataBounds, profile, aspect );
        Decoration[] decs =
            createPlotDecorations( surf, legend, legPos, title, shadeAxis );
        return new PlotPlacement( extBounds, surf, decs );
    }

    /**
     * Determines the bounds for the data part of a plot given its
     * external dimensions and other information about it.
     * It does this by assessing how much space will be required for
     * axis annotations etc.
     *
     * @param   extBounds  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   title   title text, or null
     * @param   shadeAxis  shader axis if required, or null
     * @return  data bounds rectangle
     */
    public static <P,A> Rectangle
            calculateDataBounds( Rectangle extBounds,
                                 SurfaceFactory<P,A> surfFact, P profile,
                                 A aspect, boolean withScroll, Icon legend,
                                 float[] legPos, String title,
                                 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. */

        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, extBounds.height );
            shadeExtWidth = rampBox.width
                          + shadeAxis.getRampInsets( rampBox ).right
                          + EXTERNAL_LEGEND_GAP;
        }
        Rectangle surfRect = new Rectangle( extBounds );
        surfRect.width = Math.max( MIN_DIM, surfRect.width - legExtWidth );

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

        /* Make space for the title, if present. */
        if ( title != null ) {
            insets.top += new CaptionIcon( title, surf.getCaptioner() )
                         .getIconHeight();
        }

        /* Work out available space given padding required by first guess. */
        return PlotUtil.subtractInsets( extBounds, insets );
    }

    /**
     * Returns a list of plot decorations for things like the legend
     * and shade colour ramp.
     *
     * @param  surf  plot surface
     * @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   title   title text, or null
     * @param   shadeAxis  shader axis if required, or null
     * @return   list of decorations (may have zero elements)
     */
    public static Decoration[] createPlotDecorations( Surface surf,
                                                      Icon legend,
                                                      float[] legPos,
                                                      String title,
                                                      ShadeAxis shadeAxis ) {
        Rectangle dataBounds = surf.getPlotBounds();
        Insets insets = surf.getPlotInsets( false );
        List<Decoration> decList = new ArrayList<Decoration>();
        int gxlo = dataBounds.x;
        int gylo = dataBounds.y;
        int gxhi = dataBounds.x + dataBounds.width;
        int gyhi = dataBounds.y + dataBounds.height;
        
        /* 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 ] );

                /* Invert the sense of the y component so that up is positive,
                 * like for data coordinates. */
                ly = gylo + Math.round( ( gyhi - gylo - legend.getIconHeight() )
                                      * ( 1f - legPos[ 1 ] ) );
            }
            decList.add( new Decoration( legend, lx, ly ) );
        }

        /* Work out shader axis position. */
        if ( shadeAxis != null ) {
            int vpad = shadeAxis.getEndPadding();
            int topPad = Math.max( 0, vpad - insets.top );
            int botPad = Math.max( 0, vpad - insets.bottom );
            int sx = gxhi + EXTERNAL_LEGEND_GAP;
            int sy = ylo1 + topPad;
            Rectangle rampBox =
                new Rectangle( sx, sy, SHADE_RAMP_WIDTH, gyhi - sy - botPad );
            Icon shadeIcon = shadeAxis.createAxisIcon( rampBox );
            decList.add( new Decoration( shadeIcon, sx, sy ) );
        }

        /* Position title. */
        if ( title != null ) {
            Captioner captioner = surf.getCaptioner();
            Icon titleIcon = new CaptionIcon( title, captioner );
            int px = dataBounds.x
                   + dataBounds.width / 2
                   - titleIcon.getIconWidth() / 2;
            int py = dataBounds.y - titleIcon.getIconHeight()
                                  - captioner.getPad();
            decList.add( new Decoration( titleIcon, px, py ) );
        }

        return decList.toArray( new Decoration[ 0 ] );
    }

    /**
     * Icon which draws a caption.
     */
    @Equality
    private static class CaptionIcon implements Icon {
        private final String text_;
        private final Captioner captioner_;
        private final int width_;
        private final int height_;
        private final int x_;
        private final int y_;

        /**
         * Constructor.
         *
         * @param  text  caption text
         * @param  captioner   caption painter
         */ 
        CaptionIcon( String text, Captioner captioner ) {
            text_ = text;
            captioner_ = captioner;
            Rectangle bounds = captioner.getCaptionBounds( text );
            x_ = 0;
            y_ = - bounds.y;
            width_ = bounds.width;
            height_ = bounds.height;
        }

        public int getIconWidth() {
            return width_;
        }

        public int getIconHeight() {
            return height_;
        }

        public void paintIcon( Component c, Graphics g, int x, int y ) {
            int xoff = x + x_;
            int yoff = y + y_;
            g.translate( xoff, yoff );
            captioner_.drawCaption( text_, g );
            g.translate( -xoff, -yoff );
        }

        @Override
        public int hashCode() {
            int code = 4432;
            code = 23 * text_.hashCode();
            code = 23 * captioner_.hashCode();
            return code;
        }

        @Override
        public boolean equals( Object o ) {
            if ( o instanceof CaptionIcon ) {
                CaptionIcon other = (CaptionIcon) o;
                return this.text_.equals( other.text_ )
                    && this.captioner_.equals( other.captioner_ );
            }
            else {
                return false;
            }
        }
    }
}
