package com.mindprod.jdisplay;

import com.mindprod.jtokens.NL;
import com.mindprod.jtokens.Token;

import javax.swing.*;
import java.awt.*;
import java.util.Arrays;

/**
 * Renders a string of tokens, usually representing Java source code. Does not handle its own scrolling.
 *
 * @author Roedy Green
 * @version 1.0
 */
public class PrettyCanvas extends JPanel
    {
// ------------------------------ FIELDS ------------------------------

    /**
     * true if want extra debug output
     */
    private static final boolean DEBUGGING = false;

    /**
     * Dimensions of the scrollable footprint. Start with dummy in case we get queried before set called.
     */
    // private Dimension dimension = new Dimension( 10, 10 );

    /**
     * Array of tokens to render
     */
    private Token[] tokens;

    /**
     * has the accelerator to render only necessary tokens kicked in yet?
     */
    private boolean accelerated = false;

    /**
     * true if want lineNumbers
     */
    private boolean hasLineNumbers;

    /**
     * baseline in pixels down from top of canvas that bandCount renders on. indexed by bandCount. There will be one
     * entry per non-blank line here.
     */
    private int[] baselines;

    /**
     * first line number to render in a given band.
     */
    private int[] firstLineNumbersInBand;

    /**
     * first token to render in a given band.
     */
    private int[] firstTokensInBand;

    /**
     * counts how many bands we have. If there were no blank lines, would be same as number of lines. Normally the value
     * is a little less that the number of lines since a strip of vertical white space counts as one bandCount.
     */
    private int bandCount;

    /**
     * how many pixels wide line numbers are
     */
    private int lineNumberWidth;

    /**
     * top most baseline where we start rendering a bandCount.
     */
    private int startAtBaseline;

    /**
     * 1-based line number to start rendering the current bandCount.
     */
    private int startAtLineNumber;

    /**
     * Total lines of text in the entire array of Tokens, which is considerably smaller than the total number of
     * tokens.
     */
    private int totalLines;
// -------------------------- PUBLIC INSTANCE  METHODS --------------------------
    /**
     * Constructor
     */
    public PrettyCanvas()
        {
        // get swing to clear the background for us, tell it we don't paint all the pixels.
        this.setOpaque( true );
        }

    /**
     * called whenever system has a slice to render
     *
     * @param g Graphics defining where and region to paint.
     */
    public void paintComponent( Graphics g )
        {
        // paintComponent will clear the region to the background colour
        super.paintComponent( g );

        // don't meddle with original
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING,
                              RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
        g2d.setRenderingHint( RenderingHints.KEY_RENDERING,
                              RenderingHints.VALUE_RENDER_QUALITY );
        // if wanted to smooth geometric shapes too
        // g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
        // RenderingHints.VALUE_ANTIALIAS_ON );

        render( g2d );
        }

    /**
     * @param width           in pixels of the scrollable region including room for line numbers and margins, but not
     *                        scrollbars.
     * @param height          in pixel of the sscrollableregion including room for margins, but not scrollbars.
     * @param hasLineNumbers  true if want line numbers applied down the left hand side.
     * @param lineNumberWidth with of the line number column
     */
    public void set( int width,
                     int height,
                     boolean hasLineNumbers,
                     int lineNumberWidth )
        {
        if ( DEBUGGING )
            {
            System.out.println( "PrettyCanvas.set width:" + width
                                + " height:" + height
                                + " hasLineNumbers:" + hasLineNumbers
                                + " lineNumberWidth:" + lineNumberWidth );
            }
        Dimension dimension = new Dimension( width, height );
        // only available in 1.5+
        this.setMinimumSize( dimension );
        this.setPreferredSize( dimension );
        this.setMaximumSize( dimension );
        this.setSize( dimension );
        this.hasLineNumbers = hasLineNumbers;
        this.lineNumberWidth = lineNumberWidth;

        // adding or removing line numbers means new size.
        // caller must setPreferred size
        accelerated = false;
        /*
         * my canvas size has changed, even if only virtual. Warn parent
         * container.
         */
        this.invalidate();
        }

    /**
     * Set tokens to display
     *
     * @param tokens     array of tokens, without lead or trailing NL()
     * @param totalLines number of lines of text to render.
     */
    public void setTokens( Token[] tokens, int totalLines )
        {
        this.tokens = tokens;
        this.totalLines = totalLines;
        this.accelerated = false;
        }

// -------------------------- OTHER METHODS --------------------------

    /**
     * accelerate rendering by computing just which tokens need to be rendered for a given bandCount. Get the index of
     * the first Token
     *
     * @param r clip region to be rendered.
     *
     * @return first token index that needs to be rendered.
     */
    private int firstTokenNeedToRender( Rectangle r )
        {
        int topOfBand = r.y;
        // pick a baseline just prior to the band for safety.
        int firstBaseline = topOfBand - Geometry.LEADING_PX;
        // home in on a unique 0-based band
        int band = Arrays.binarySearch( baselines, firstBaseline );
        if ( band < 0 )
            {
            // convert insertion point to the band below.
            int insert = -band - 1;
            band = insert - 1;
            band = Math.min( Math.max( 0, band ), bandCount - 1 );
            }
        // As side benefit, we get the startAtBaseline and starAtLineNumber
        startAtBaseline = baselines[ band ];
        // startAtLineNumber is 1-based.
        startAtLineNumber = firstLineNumbersInBand[ band ];
        return firstTokensInBand[ band ];
        }

    /**
     * accelerate rendering by computing just which tokens need to be rendered for a given bandCount.
     *
     * @param r clip region to be rendered.
     *
     * @return last token index that needs to be rendered.
     */
    @SuppressWarnings({ "UnusedAssignment" })
    private int lastTokenNeedToRender( Rectangle r )
        {
        int bottomOfBand = r.y + r.height;/* y increases down the screen */
        // pick a baseline just after the band for safety.
        // With multiple nls it could be part way through the band, but with
        // nothing
        // to render after it.
        int lastBaseline = bottomOfBand + Geometry.LEADING_PX;

        // home in on a unique 0-based band
        int band = Arrays.binarySearch( baselines, lastBaseline );
        if ( band < 0 )
            {
            // with an inexact hit we want the conservative choice the band
            // after the insert point.
            band = Math.min( Math.max( 0, -band - 1 ), bandCount - 1 );
            }
        if ( band == bandCount - 1 )
            {
            // we are on the last band, render even the very last token.
            return tokens.length - 1;
            }
        else
            {
            /*
             * the last token of the band is the token just before the token on
             * the start of the next band.
             */
            return firstTokensInBand[ band + 1 ] - 1;
            }
        }

    /**
     * Record where on page we started rendering a given band i.e. line with text on it.
     *
     * @param baseline   y in of baseline in pixels from the top of canvas.
     * @param lineNumber one-based line number being rendered
     * @param tokenIndex index of first token on the line, including possibly NL though normally it would be the last
     *                   token of the previous line.
     */
    private void lineRenderedAt( int baseline, int lineNumber, int tokenIndex )
        {
        baselines[ bandCount ] = baseline;
        firstTokensInBand[ bandCount ] = tokenIndex;
        firstLineNumbersInBand[ bandCount ] = lineNumber;
        bandCount++;
        }

    /**
     * Clear binary search arrays used to accelerate rendering by finding only those tokens we need to render.
     */
    private void prepareAccelerator1()
        {
        if ( tokens == null || tokens.length == 0 )
            {
            return;
            }
        // these are a little bigger than we need.
        bandCount = 0;
        baselines = new int[totalLines];
        firstTokensInBand = new int[totalLines];
        firstLineNumbersInBand = new int[totalLines];
        // debugging, fill with easy to spot invalid values.
        for ( int i = 0; i < totalLines; i++ )
            {
            baselines[ i ] = -10;
            firstTokensInBand[ i ] = -20;
            firstLineNumbersInBand[ i ] = -30;
            }
        }

    /**
     * Prepare to use the accelerator by trimming its arrays back to perfect size. We have collected data on where each
     * band is rendering.
     */
    private void prepareAccelerator2()
        {
        // trim arrays back precisely to size so that binary search will work.
        int[] old = baselines;
        baselines = new int[bandCount];
        System.arraycopy( old, 0, baselines, 0, bandCount );

        old = firstTokensInBand;
        firstTokensInBand = new int[bandCount];
        System.arraycopy( old, 0, firstTokensInBand, 0, bandCount );

        old = firstLineNumbersInBand;
        firstLineNumbersInBand = new int[bandCount];
        System.arraycopy( old, 0, firstLineNumbersInBand, 0, bandCount );
        }

    /**
     * does drawing. similar to logic in Footprint.s2CalcPayloadFootprint
     *
     * @param g where to paint
     */
    @SuppressWarnings({ "PointlessArithmeticExpression" })
    private void render( Graphics2D g )
        {
        // We avoid rendering before or after the clip region.
        // Normally we only render a 4 pixel high band at a time.
        Rectangle r = g.getClipBounds();

        // No need to clear the background of just the clip region
        // because setOpaque handles it
        // g.setColor( this.getBackground() );
        // g.fillRect ( r.x, r.y, r.width, r.height );

        // render all the tokens, some may be offscreen, but no matter.
        if ( tokens == null || tokens.length == 0 )
            {
            return;
            }

        // use rendering hints

        // locals
        // true if this is the first token on theline
        boolean firstTokenOnLine;
        // index of first token to render
        // index of last token to render.
        int firstTokenToRender;
        int lastTokenToRender;
        // x left, for token rendering
        int x;
        // y  basesline for token rendering
        int y;
        // line number rendering
        int lineNumber;

        if ( accelerated )
            {
            firstTokenToRender = firstTokenNeedToRender( r );
            lastTokenToRender = lastTokenNeedToRender( r );
            x = Geometry.LEFT_PADDING_PX;
            y = startAtBaseline;
            lineNumber = startAtLineNumber;
            firstTokenOnLine = true;
            }
        else
            {
            prepareAccelerator1();
            firstTokenToRender = 0;
            lastTokenToRender = tokens.length - 1;
            x = Geometry.LEFT_PADDING_PX;
            y = Geometry.TOP_PADDING_PX + Geometry.LEADING_PX;
            lineNumber = 1;
            firstTokenOnLine = true;
            }
        if ( DEBUGGING )
            {
            System.out.println( "firstToken:"
                                + firstTokenToRender
                                + " lastToken:"
                                + lastTokenToRender
                                + " x:"
                                + x
                                + " ybaseline:"
                                + y
                                + " r.y:"
                                + r.y
                                + " r.height:"
                                + r.height
                                + " ln:"
                                + lineNumber );
            }
        for ( int i = firstTokenToRender; i <= lastTokenToRender; i++ )
            {
            Token t = tokens[ i ];
            if ( !accelerated && firstTokenOnLine )
                {
                // capture base line info of this line for the accelerator.
                lineRenderedAt( y, lineNumber, i );
                }

            if ( t instanceof NL )
                {
                // render blank lines compressed.
                int lines = ( (NL) t ).getCount();
                switch ( lines )
                    {
                    case 1:
                        // single space
                        y += Geometry.LEADING_PX;
                        break;
                    case 2:
                        // 1.5 spacing
                        y += Geometry
                                .LEADING_PX + Geometry
                                .BLANK_LINE_HEIGHT_PX;
                        break;
                    case 3:
                    default:
                        // anything bigger, just double space.
                        y += Geometry.LEADING_PX + ( Geometry
                                .BLANK_LINE_HEIGHT_PX * 2 );
                        break;
                    }
                lineNumber += lines;// leave off line numbers, to avoid
                // scrunching
                x = Geometry.LEFT_PADDING_PX;
                firstTokenOnLine = true;
                }
            else
                {
                // text-rendering or space token
                if ( hasLineNumbers && firstTokenOnLine )
                    {
                    // draw the line number
                    g.setColor( Token.getLineNumberForeground() );
                    g.setFont( Token.getLineNumberFont() );
                    String digits = Integer.toString( lineNumber );
                    // right justify
                    int width = g.getFontMetrics().stringWidth( digits );
                    g.drawString( digits, x + lineNumberWidth - width, y );
                    x += lineNumberWidth + Geometry
                            .LINE_NUMBER_PADDING_PX;
                    }
                g.setColor( t.getForeground() );
                g.setFont( t.getFont() );
                String text = t.getText();
                g.drawString( text, x, y );
                x += g.getFontMetrics().stringWidth( text );
                firstTokenOnLine = false;
                }// end else
            }// end for
        // we now have captured all token baselines, even if bandCount was tiny.
        if ( !accelerated )
            {
            prepareAccelerator2();
            accelerated = true;
            }
        }// end render
    }