package jamaica;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

/** 
 * The Plotter class which draw a curve in a window.
 * 
 * @author Hanhua Feng - hanhua@cs.columbia.edu
 * @version $Id: Plotter.java,v 1.15 2003/05/20 16:14:45 hanhua Exp $
 */
public class Plotter extends JPanel {
    JFrame frame;
    Font tick_font;
    Font label_font;

    final static int nr_curve = 16;

    Insets border = new Insets( 32, 64, 32, 64 );

    int tick_width = 6;
    int grid_density = 100;
    int tick_density = 5;

    Color tick_color = new Color( 0x555555 );
    Color grid_color = new Color( 0xAAAAAA );
    Color border_color = new Color( 0xBBBB00 );
    Color[] def_color = new Color[nr_curve];

    String xlabel = null;
    String ylabel = null;

    float[] range;

    float[][] curve = new float[nr_curve][];
    Color[][] curve_color = new Color[nr_curve][];
    int[] curve_flags = new int[nr_curve];

    final static int FL_STYLEMASK = 0xf0;
    final static int FL_LINESTYLE = 0x80;
    final static int FL_POINTSTYLE = 0x40;
    final static int FL_RANGEMASK = 3;

    private static int frame_counter = 0;

    /** the constructor
     * @param frame   the window that contains this plotter panel
     */
    public Plotter( JFrame frame ) {
        this.frame = frame;
        tick_font = new Font( "Monospaced", Font.PLAIN, 10 );
        label_font = new Font( null, Font.PLAIN, 12 );
        range = new float[24];
        for ( int i=0; i<24; i+=2 )
        {
            range[i] = 0f;
            range[i+1] = 1f;
        }
    }

    /** create a plotter window with title and drawing area size
     * @param title    the title of the window
     * @param width    the width of drawing area
     * @param height   the height of drawing area
     */
    public static Plotter create( String title, int width, int height ) {
        try {
            UIManager.setLookAndFeel(
                UIManager.getCrossPlatformLookAndFeelClassName() );
        } catch (Exception e) {}

        JFrame frame = new JFrame( title );
        Plotter pane = new Plotter( frame );
        frame.getContentPane().add( pane, BorderLayout.CENTER );

        frame_counter++;
        
        frame.addWindowListener( 
            new WindowAdapter() 
            {
                public void windowClosing( WindowEvent e ) {
                    frame_counter--;
                    /* System.exit(0); */
                }
            });

        pane.newsize( width, height );

        frame.setLocationRelativeTo( null );
        frame.setVisible( true );

        return pane;
    }

    /* change the dimensions of the drawing panel */
    private void newsize( int width, int height ) {
        setPreferredSize( new Dimension( width, height ) );
        frame.pack();
    }

    private boolean addData( Matrix mat, int idx ) {
        int m = mat.height(), n = mat.width();
        if ( 1 == n )
            return false;
        curve[idx] = new float[m+m+m];
        for ( int i=0; i<m; i++ )
        {
            curve[idx][i+i+i] = (float) mat.get( i, 0 );
            curve[idx][i+i+i+1] = (float) mat.get( i, 1 );
            if ( n == 3 )
                curve[idx][i+i+i+2] = (float) mat.get( i, 2 );
        }
        return true;
    }

    private boolean addData( double[] x, Matrix mat, int idx ) {
        int m = mat.height(), n = mat.width();
        for ( int j=0; j<n; j++ )
        {
            curve[idx+j] = new float[m+m+m];
            for ( int i=0; i<m; i++ )
            {
                curve[idx+j][i+i+i] = (float) x[i];
                curve[idx+j][i+i+i+1] = (float) mat.get( i, j );
            }
        }
        return true;
    }

    private void addColorTable( int[] colortbl, int idx ) {
        if ( null != colortbl )
        {
            curve_color[idx] = new Color[colortbl.length];
            for ( int i=0; i<colortbl.length; i++ )
                curve_color[idx][i] = new Color( colortbl[i] );
        }
        else
            curve_color[idx] = null;
    }

    /** plotting a matrix that has two or three columns, over the
     * current plotting.  Refer to draw(Matrix,double[],int,int[])
     * @param mat   the matrix to be drawn
     * @param def_color the default color, if z-coordinates are not supplied
     * @param color_tbl the color table for z-coordinates
     * @param idx   the curve index
     */
    public void overdraw( Matrix mat, int def_color, int[] colortbl, int idx ) {
        if ( !addData( mat, idx ) )
             return;
        addColorTable( colortbl, idx );
        this.def_color[idx] = new Color( def_color );
        curve_flags[idx] = FL_LINESTYLE;
        repaint();
    }

    /** plotting a matrix that has two or three columns.  Each row of
     * a matrix corresponds to the x-, y-, and optional z-coordinates
     * of a point.  z-coordinate will be drawn in colors.  
     * @param mat    the matrix to be drawn
     * @param rgs    the double array containing 4 or 6 elements
     * indicating the ranges of each coordinate
     * @param def_color the default color, if z-coordinates are not supplied
     * @param color_tbl the color table for z-coordinates
     */
    public void draw( Matrix mat, double[] rgs,
                      int def_color, int[] colortbl  ) {
        removeAllCurve();
        setAllRanges( (float) rgs[0], (float) rgs[1], 0 );
        setAllRanges( (float) rgs[2], (float) rgs[3], 1 );
        if ( mat.width() >= 3 )
            setAllRanges( (float) rgs[4], (float) rgs[5], 2 );

        overdraw( mat, def_color, colortbl, 0 );
    }

    /** plotting a matrix.  Refer to draw(Matrix,rgs,int,int[]); */
    public void draw( Matrix mat, int def_color, int[] colortbl ) {
        double[] rgs = new double[6];
        int n = mat.width();
        if ( n > 3 )
            n = 3;
        for ( int i=0; i<n; i++ )
        {
            rgs[i+i] = mat.min( i );
            rgs[i+i+1] = mat.max( i );
        }

        draw( mat, rgs, def_color, colortbl );
    }

    /** plotting a matrix.  Refer to draw(Matrix,rgs,int,int[]); */
    public void draw( Matrix mat, double[] rgs, int def_color ) {
        draw( mat, rgs, def_color, null );
    }

    /** plotting a matrix.  Refer to draw(Matrix,rgs,int,int[]); */
    public void draw( Matrix mat, int def_color ) {
        draw( mat, def_color, null );
    }


    /** plotting a matrix that has multiple columns.  The x-coordinates
     * is specified separately, and each column of the matrix represent
     * an individual curve by specifying y-coordinates.
     * @param x      the x-coordinates having the same dimension as the
     *               number of rows of the matrix
     * @param mat    the multiple-column matrix to be draw
     * @param min    the minimum value
     * @param max    the maximum value
     * @param colors array of colors of curves, having the same dimension
     *               as the number of columns of the matrix
     */
    public void multidraw( double[] x, Matrix mat, double min, double max,
                           int[] colors ) {
        removeAllCurve();
        if ( !addData( x, mat, 0 ) )
            return;
        if ( x[0] < x[x.length-1] )
            setAllRanges( (float) x[0], (float) x[x.length-1], 0 );
        else
            setAllRanges( (float) x[x.length-1], (float) x[0], 0 );
        setAllRanges( (float) min, (float) max, 1 );
        setAllRanges( 0, 0, 2 );
        for ( int i=0; i<mat.width(); i++ )
        {
            this.def_color[i] = new Color( colors[i] );
            addColorTable( null, i );
            curve_flags[i] = FL_LINESTYLE;
        }
        repaint();
    }

    /** plot a matrix columns with multiple curves.
     * Refer to multidraw( double[], Matrix, double, double, in[] )
     */
    public void multidraw( double[] x, Matrix mat, int[] colors ) {
        multidraw( x, mat, mat.min(), mat.max(), colors );
    }

    /** set line/point style
     * @param idx    the line index
     * @param flags  the line/point style, either FL_LINESTYLE or 
     *               FL_POINTSTYLE or both
     */
    public void setStyle( int idx, int flags ) {
        if ( idx < 0 )
        {
            for ( int i=0; i<nr_curve; i++ )
            {
                curve_flags[i] &= ~FL_STYLEMASK;
                curve_flags[i] |= flags & FL_STYLEMASK;
            }
        }
        else
        {
            curve_flags[idx] &= ~FL_STYLEMASK;
            curve_flags[idx] |= flags & FL_STYLEMASK;
        }
    }

    /** set the drawing range
     * @param min    the minimum value
     * @param max    the maximum value
     * @param dimid  the dimension index: 0:x, 1:y, 2:z
     * @param rangeid the range index: 0:main(grid), 1:first axis, 
     *               2:second axis, 3: the hidden axis
     */
    final void setRange( float min, float max, 
                         int dimid, int rangeid ) {
        range[dimid*8+rangeid*2] = min;
        range[dimid*8+rangeid*2+1] = max;
    }

    /** let some curve use specified range
     * @param idx    the curve index
     * @param rangeid the range index: 0:main(grid), 1:first axis, 
     *               2:second axis, 3: the hidden axis
     */
    final void useRange( int idx, int rangeid ) {
        curve_flags[idx] &= ~FL_RANGEMASK;
        curve_flags[idx] |= rangeid & FL_RANGEMASK;
    }

    private void removeAllCurve() {
        for ( int i=0; i<nr_curve; i++ )
        {
            curve[i] = null;
            curve_color[i] = null;
        }
    }

    private final void setAllRanges( float min, float max, int dimid ) {
        range[dimid*8+0] = range[dimid*8+2] 
            = range[dimid*8+4] = range[dimid*8+6] =  min;
        range[dimid*8+1] = range[dimid*8+3] 
            = range[dimid*8+5] = range[dimid*8+7] =  max;
    }

    private final static double log10 = Math.log( 10 );

    private final static int closestUnit( double x ) {
        double y = Math.log(x) / log10;
        int order = (int) Math.floor( y );
        y -= (double) order;
        if ( y < 0.1 )
            return ( order << 8 ) + 1;
        if ( y < 0.4 )
            return ( order << 8 ) + 2;
        if ( y < 0.8 )
            return ( order << 8 ) + 5;
        return ( (order+1) << 8 ) + 1;
    }

    private final static double decimalToDouble( int n, int order ) {
        return Math.pow( 10, order ) * n;
    }

    private final static String decimalToString( int n, int order ) {
        if ( 0 == n )
            return new String( "0" );

        while ( 0 == n % 10 )
        {
            n /= 10;
            order++;
        }

        StringBuffer str = new StringBuffer( 64 );

        if ( order > 4 )
            str.append(n).append( 'E' ).append( order );
        else if ( order >= 0 )
        {
            str.append(n);
            for ( int i=0; i<order; i++ )
                str.append( '0' );
        }
        else
        {
            str.append( Math.abs(n) );
            int j = str.length();
            if ( j > -order )
                str.insert( j+order, '.' );
            else if ( j > -order-3 )
            {
                for ( ; j < -order; j++ )
                    str.insert( 0, '0' );
                str.insert( 0, "0." );
            }
            else
            {
                if ( j > 1 )
                    str.insert( 1, "." );
                str.append( 'E' ).append( order+j-1 );
            }
            if ( n < 0 )
                str.insert( 0, '-' );
        }
        return str.toString();
    }

    private void paintRulers( Graphics g, int x, int y, int length, int breath,
                             int breath_extend,
                             double min, double max, double density,
                             boolean is_vert, boolean with_label ) {

        FontMetrics fm = g.getFontMetrics();
        int label_ypos;

        if ( is_vert )
            label_ypos = ( fm.getAscent() - fm.getDescent() ) / 2;
        else
        {
            if ( breath > 0 )
                label_ypos = breath_extend + fm.getAscent();
            else
                label_ypos = breath_extend - fm.getDescent();
        }

        
        double factor = length / (max-min);
        int unit = closestUnit( density / factor );
        int order = unit >> 8;
        int lsd = unit & 0xf;
        int larger1 = ( 5 == lsd ) ? 2 : 5;
        int larger2 = 10;
        double dt = decimalToDouble( lsd, order );

        int i = (int) Math.ceil( min/dt );
        for ( double t = i * dt; t <= max; t += dt, i++ )
        {
            int offset = (int) Math.round( (t-min) * factor );
            int ext = 0, pe = 0, ne = 0;

            if ( 0 == i % larger2 )
            {
                ext = breath_extend;
                if ( with_label )
                {
                    String label = decimalToString( i*lsd, order );
                    if ( is_vert )
                    {
                        int w;
                        
                        if ( breath_extend < 0 )
                            w = -fm.stringWidth( label )-1;
                        else
                            w = 2;
                        g.drawString( label, x + breath_extend + w, 
                                      y + length - 1 - offset + label_ypos );
                    }
                    else
                    {
                        g.drawString( label, 
                                      x + offset - fm.stringWidth( label )/2, 
                                      y + label_ypos );
                    }
                }
            }
            else if ( 0 == i % larger1 )
                ext = (breath_extend+breath)/2;
            else
                ext = breath;

            if ( is_vert )
            {
                int y1 = y + length - 1 - offset;
                g.drawLine( x, y1, x+ext, y1 );
            }
            else
                g.drawLine( x+offset, y, x+offset, y+ext );
        }
    }

    private void paintBorder( Graphics g,  int win_width, int win_height ) {

        int x0 = border.left-1, x1 = win_width-border.right;
        int y0 = border.top-1, y1 = win_height-border.bottom;
        g.drawLine( x0, y0, x1, y0 );
        g.drawLine( x1, y0, x1, y1 );
        g.drawLine( x1, y1, x0, y1 );
        g.drawLine( x0, y1, x0, y0 );
    }

    private void paintCurve( 
        Graphics g, float[] curve,
        Color[] color, Color def_color, 
        int width, int height, int flags ) {

        int idx = flags & FL_RANGEMASK;

        if ( range[idx] >= range[idx+1] || range[idx+8] >= range[idx+9] )
            return;

        double xfactor = width / (range[idx+1]-range[idx]);
        double yfactor = height / (range[idx+9]-range[idx+8]);
        double zfactor = 0;
        if ( null != color )
        {
            if ( range[idx+17] <= range[idx+16] )
                color = null;
            else
                zfactor = color.length / (range[idx+17]-range[idx+16]);
        }

        g.setColor( def_color );

        int x = border.left 
            + (int) Math.round( (curve[0]-range[idx])*xfactor );
        int y = border.top + height - 1 
            - (int) Math.round( (curve[1]-range[idx+8])*yfactor );
        if ( 0 != ( flags & FL_POINTSTYLE ) )
            g.drawArc( x-1, y-1, 2, 2, 0, 360 );

        for ( int i=3; i<curve.length-2; i+=3 )
        {
            int x1 = border.left + (int) Math.round( 
                (curve[i]-range[idx])*xfactor );
            int y1 = border.top + height - 1 - (int) Math.round( 
                (curve[i+1]-range[idx+8])*yfactor );
            double z = ( 0 != ( flags & FL_POINTSTYLE ) ) ? 
                curve[i+2] : (curve[i+2]+curve[i-1])*0.5;
            if ( null != color )
            {
                int ci = (int) Math.round( 
                    ( z-range[idx+16] ) * zfactor );
                if ( ci < 0 )
                    ci = 0;
                else if ( ci >= color.length )
                    ci = color.length-1;
                g.setColor( color[ci] );
            }

            if ( 0 != ( flags & FL_LINESTYLE ) )
                g.drawLine( x, y, x1, y1 );
            if ( 0 != ( flags & FL_POINTSTYLE ) )
                g.drawArc( x1-1, y1-1, 2, 2, 0, 360 );
            x = x1;
            y = y1;
        }
    }

    public void paintComponent( Graphics g ) {
        super.paintComponent (g);
        Insets insets = getInsets();
        int win_width = getWidth() - insets.left - insets.right;
        int win_height = getHeight() - insets.top - insets.bottom;
        int width = win_width - border.left - border.right;
        int height = win_height - border.top - border.bottom;

        g.setColor( border_color );
        paintBorder( g, win_width, win_height );

        g.setColor( tick_color );
        g.setFont( tick_font );
        paintRulers( g, border.left, border.top-1, 
                     width, -tick_width, -2*tick_width,
                     range[2], range[3], tick_density, false, true );
        paintRulers( g, border.left, win_height - border.bottom, 
                     width, tick_width, 2*tick_width,
                     range[4], range[5], tick_density, false, true );
        paintRulers( g, border.left-1, border.top,
                     height, -tick_width, -2*tick_width,
                     range[12], range[13], tick_density, true, true );
        paintRulers( g, win_width - border.right, border.top, 
                     height, tick_width, 2*tick_width,
                     range[10], range[11], tick_density, true, true );


        Shape clip = g.getClip();
        g.setClip( border.left, border.top, width, height );

        g.setColor( grid_color );
        paintRulers( g, border.left, border.top, width, height, height,
                     range[0], range[1], grid_density, false, false );
        paintRulers( g, border.left, border.top, height, width, width,
                     range[8], range[9], grid_density, true, false );

        float[] rg = new float[6];

        for ( int i=0; i<nr_curve; i++ ) {
            if ( null == curve[i] )
                continue;
            int idx = (curve_flags[i] & FL_RANGEMASK) << 1;
            paintCurve( g, curve[i], curve_color[i], def_color[i],
                        width, height, 
                        curve_flags[i] );
        }

        g.setClip( clip );
                   
    }

    /** return the number of plotter windows */
    public static int frameCount() {
        return frame_counter;
    }

    public static void main( String[] args ) {
        Matrix mat = new Matrix( 128, 3 );
        double[] x = new double[128];
        for ( int i=0; i<128; i++ ) {
            x[i] = 0.01*i-0.5;
            mat.set( i, 0, Math.sin( 0.1*i )*1 );
            mat.set( i, 1, Math.sin( 0.115*i )*0.75+10 );
            mat.set( i, 2, Math.sin( 0.02*i )*2+5 );
        }
        /*
        create( "test", 512, 384 ).draw(mat,0x678910, 
                                         Painter.colorMap(Painter.CM_DARKCOLORS));
        */
        Plotter p = create( "test", 512, 384 );
        p.multidraw( x, mat, -12, 12, new int[]{ 0, 0xff, 0xff00 } );
        p.setStyle( 1, FL_LINESTYLE | FL_POINTSTYLE );
        p.overdraw( mat, 0x678910, null, 3 );
    }
}
