/**
 JDisplay
 Displays a serialised pre-parsed Java program in fancy fonts and colours
 takes parm snippet = name of snippet without /snippet/ser or .ser.

 copyright (c) 2004-2009 Roedy Green, Canadian Mind Products
 may be copied and used freely for any purpose but military.

 Roedy Green
 Canadian Mind Products
 #101 - 2536 Wark Street
 Victoria, BC Canada
 V8T 4G8
 tel:(250) 361-9093
 roedy g at mindprod dotcom

 Version History

 version 1.2 2004-05-15 split off calcPreferredSize into its own
 class

 version 1.3 2004-05-23 Put all logic about calculating panel and
 frame size in PreferredSize None left to JDisplay macro. no bar
 parameter. Computed automatically. manual control of when ScrollBars
 used.

 version 1.4 2004-05-29 Flip back from Swing to AWT so that Ctrl-C
 Ctrl-V will work. Even with AWT, I need a TextArea, not the
 PrettyCanvas. Downside mainly was losing ability to turn or horizontal
 and vertical scrollbars automatically. Adjust for fact scrollbars are
 all or nothing. Can't have just vertical. Redo all tokenizers with
 lookaheaad, and explicit handled boolean. Eliminate the enter method
 on all tokenizers. Explicit list of all choices on default for
 proofreading. Eliminate flicker with removal of super.paint(). \ in
 bat now show in special font.

 1.5 2004-06-01 slightly larger margins, use new
 BatTokenizer, HTMLTokenizer, JavaTokenizer

 1.6 2004-07-16 better recovery when cannot read *.ser
 file.

 1.7 2005-06-12 destroy, make sure not null before remove.
 Futures implement my own copy/paste that works with Swing or AWT token
 needs to remember where it is on screen.

 1.8 2005-07-28 major overhaul to use new tokenisers..

 1.9 2005-09-07 allow JDisplay to run under Eclipse

 2.0 2005-11-11 make snippet/ optional in Applet url
 parameter.

 2.1 2005-12-25 add parser for *.properties files

 2.2 2005-12-25 add parser for *.csv files

 2.3 2005-12-25 add parser for *.ini files

 2.4 2005-12-25 more robust error handling

 2.5 2006-01-27 prints vm version, more checks.

 2.6 2006-03-06 reformat with IntelliJ and add Javadoc

 2.7 2007-04-29 use a corresponding mono font when turn off colour.

 2.8 2007-05-05 add iformat rendering, use of snippet/ser and snippet/iformat

 2.9 2007-07-12 first public distribution.

 3.0 2007-07-26 add support for annotations.

 3.1 2007-08-20 new colour scheme.

 3.2 2007-09-17. rename snippets -> snippet.  Label *.java and *.javafrag properly.

 3.3 2008-01-11, add support for hex and octal numerics.

 3.4 2008-02-23 bold variable definitions. more robust display of class on dump.

 3.5 2008-02-24 change sizes and spacing

 3.6 2008-03-06 convert to Swing

 3.7 2008-04-18 get JDisplay and CSS font renderings in closer sync

 3.8 2008-04-30 improve way numeric literals are rendered in Java.

 3.9 3008-08-08 add vanilla text parser for text files. No changes needed to JDisplay itself, just the bundle.
 */
package com.mindprod.jdisplay;

import com.mindprod.common11.Build;
import com.mindprod.common11.FontFactory;
import com.mindprod.common11.VersionCheck;
import com.mindprod.common13.Common13;
import com.mindprod.common13.HybridJ;
import com.mindprod.common13.JEButton;
import com.mindprod.jtokens.Token;
import com.mindprod.jtokens.TokenFonts;

import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JScrollPane;
import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.zip.GZIPInputStream;

/**
 * Displays a serialised pre-parsed Java program in fancy fonts and colours takes parm url = relative or absolute url of
 * the *.java file without the .ser
 * <p/>
 * jdisplay  is an applet to render large snippets. jdisplayaux  handles inserting code into the HTML for htmlmacros.
 * jprep parses the snippet.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 3.9 3008-08-08 add vanilla text parser for text files. No changes needed to JDisplay itself, just the bundle.
 * @since 2004
 */
public final class JDisplay extends JApplet
    {
    // ------------------------------ FIELDS ------------------------------

    /**
     * true if want extra debug output
     */
    private static final boolean DEBUGGING = false;

    /**
     * height of applet box in pixels. Does not include surrounding frame. Only useful when run as an application.
     */
    private static final int APPLET_HEIGHT = 720;

    /**
     * Width of applet box in pixels. Only useful when run as an application.
     */
    private static final int APPLET_WIDTH = 960;

    /**
     * version number for this class
     */
    public static final long serialVersionUID = 30;

    /**
     * used for background of bar colour
     */
    private static final Color APPLET_BACKGROUND = Build.BLEND_BACKGROUND;

    /**
     * used for background button colour.
     */
    private static final Color DARK_GREEN = new Color( 0x008000 );

    /**
     * undisplayed copyright notice
     *
     * @noinspection UnusedDeclaration
     */
    public static final String EMBEDDED_COPYRIGHT =
            "copyright (c) 2004-2009 Roedy Green, Canadian Mind Products, http://mindprod.com";

    /**
     * @noinspection UnusedDeclaration
     */
    private static final String RELEASE_DATE = "2008-08-08";

    /**
     * Title
     */
    private static final String TITLE_STRING = "JDisplay";

    /**
     * Version, is no About box.
     */
    private static final String VERSION_STRING = "3.9";

    /**
     * plain B & W TextArea to display on for copy/paste.
     */
    private CodeJTextArea plainText;

    /**
     * contentPane for JApplet, not local as usual.
     */
    private Container contentPane;

    /**
     * payload size information.
     */
    private Footprint footprint;

    /**
     * button to click to download source.
     */
    private JButton download;

    /**
     * check to display in colour/B&W.
     */
    private JCheckBox colour;

    /**
     * use Line numbers?
     */
    private JCheckBox lineNumbers;

    /**
     * scrolls prettyCanvas
     */
    private JScrollPane prettyCanvasScroller;

    /**
     * Display with colours, as bit image.
     */
    private PrettyCanvas prettyCanvas;

    /**
     * e.g.  Myprog.java  (no lead snippet/ser or trail .ser .
     */
    private String snippetName;

    /**
     * The list of tokens
     */
    private Token[] tokens;

    /**
     * true if running as Applet, false as as application.
     */
    private final boolean asApplet;

    /**
     * true if want control bar on top. Controlled by an Applet bar parameter. Nearly always true.
     */
    private boolean hasBar = true;

    /**
     * should we use line numbers?  We always do for now.
     */
    private final boolean hasLineNumbers = true;

    /**
     * true after plainText has been loaded with tokens
     */
    private boolean plainTextLoaded = false;

    // -------------------------- PUBLIC INSTANCE  METHODS --------------------------
    /**
     * Default constructor when started as an Applet.
     */
    public JDisplay()
        {
        this.asApplet = true;
        this.hasBar = true;
        }

    /**
     * Standard Java destroy
     */
    public void destroy()
        {
        if ( colour != null )
            {
            contentPane.remove( colour );
            colour = null;
            }
        if ( download != null )
            {
            contentPane.remove( download );
            download = null;
            }
        if ( lineNumbers != null )
            {
            contentPane.remove( lineNumbers );
            lineNumbers = null;
            }
        if ( plainText != null )
            {
            contentPane.remove( plainText );
            plainText = null;
            }
        if ( prettyCanvasScroller != null && prettyCanvas != null )
            {
            prettyCanvasScroller.remove( prettyCanvas );
            prettyCanvas = null;
            }
        if ( prettyCanvasScroller != null )
            {
            contentPane.remove( prettyCanvasScroller );
            prettyCanvasScroller = null;
            }
        }

    /**
     * usual Applet init
     */
    public void init()
        {
        if ( !VersionCheck.isJavaVersionOK( 1, 5, 0, this ) )
            {
            // effectively abort
            return;
            }
        Common13.setLaf();
        // helps track bugs to know version customer was using
        System.out
                .println( "initialising "
                          + TITLE_STRING
                          + " "
                          + VERSION_STRING
                          + " released:"
                          + RELEASE_DATE
                          + " build:"
                          + Build.BUILD_NUMBER
                          + " in Java "
                          + System.getProperty( "java.version",
                        "unknown" ) );
        if ( asApplet )
            {
            getParams();
            }
        contentPane = this.getContentPane();
        contentPane.setLayout( new GridBagLayout() );
        contentPane.setBackground( APPLET_BACKGROUND );// make it blend into CMP background

        buildComponents();

        // add components:
        layoutGridBag();
        addListeners();
        }// end init

    /**
     * make sure the pretty version is displaying.
     */
    public void start()
        {
        if ( colour != null )
            {
            colour.setSelected( true );
            redisplay();
            }
        }

    // --------------------------- CONSTRUCTORS ---------------------------

    /**
     * Constructor for when running from command line.
     *
     * @param snippetName bare name of snippet, no lead snippet/ser or trailing .ser
     */
    private JDisplay( String snippetName )
        {
        this.snippetName = snippetName;
        this.asApplet = false;
        this.hasBar = true;
        }

    // -------------------------- OTHER METHODS --------------------------

    /**
     * hook up the listeners
     */
    private void addListeners()
        {
        // if click on pretty image, get plain textarea can be cut/pasted
        prettyCanvas.addMouseListener( new MouseAdapter()
        {
        /**
         * close down the Dialog when user clicks Dismiss
         *
         * @param event details of event
         */
        public void mouseClicked( MouseEvent event )
            {
            colour.setSelected( false );
            lineNumbers.setSelected( false );
            redisplay();
            }// end mouseClicked
        }// end anonymous class
        );// end addMouseListener line

        ItemListener theListener = new ItemListener()
        {
        /**
         * Notice any change to one of the list box selectors.
         *
         * @param event details of just what the user clicked.
         */
        public void itemStateChanged( ItemEvent event )
            {
            redisplay();
            }
        };

        // hook up so display will change if any widgets touched.
        colour.addItemListener( theListener );

        // hook up so display will change if any widgets touched.
        lineNumbers.addItemListener( theListener );

        download.addActionListener( new ActionListener()
        {
        /**
         * Notice any change to one of the list box selectors.
         *
         * @param event details of just what the user clicked.
         */
        public void actionPerformed( ActionEvent event )
            {
            download();
            }// end actionPerformed
        } );
        }

    /**
     * allocate GUI components
     */
    private void buildComponents()
        {
        lineNumbers = new JCheckBox( "line numbers", false );

        colour = new JCheckBox( "colour", true );

        download = new JEButton( "download" );
        // leave background default, smaller text that usual
        download.setFont( FontFactory.build( "Dialog", Font.BOLD, 12 ) );

        // Allocate the CodeTextArea now, but don't populate it with tokens unless
        // we have to.
        plainText = new CodeJTextArea();
        plainText.setBackground( Color.white );
        // AWT does not support TokenFonts.MONO_FONTS; only PrettyCanvas does.
        plainText.setFont( FontFactory.build( "monospaced",
                Font.PLAIN,
                TokenFonts.NORMAL_FONT_SIZE_IN_POINTS ) );
        // don't load plainText with tokens until until needed.
        plainText.setVisible( false );

        prettyCanvas = new PrettyCanvas();
        prettyCanvas.setBackground( Color.white );
        prettyCanvas.setVisible( false );

        // gets tokens
        fetchTokens();
        prettyCanvas.setTokens( tokens, footprint.totalLines );
        // Decide if we need scrollbars
        // See if it will fit without:
        // Recompute with our font metrics
        footprint.s2CalcPayloadFootprint( tokens, this );
        //  footprint.s3CalcFat( tokens ) not needed since we are rendering with Applet.
        footprint.s4CalcScrollableFootprint( Rendering.APPLET );
        footprint.s5CalcIdealAppletFootPrint( Rendering.APPLET,
                hasBar,
                hasLineNumbers,
                false
                /* hscroll */,
                false
                /* vscroll */,
                1.0f
                /* no safety factor, we know exact metrics now */ );
        boolean horBars = this.getWidth() < footprint.idealAppletWidth;
        boolean vertBars = this.getHeight() < footprint.idealAppletHeight;
        // we dont set values in the plainText until later, in
        // start/redisplay.

        prettyCanvasScroller =
                new JScrollPane( prettyCanvas, vertBars ?
                                               JScrollPane.VERTICAL_SCROLLBAR_ALWAYS :
                                               JScrollPane.VERTICAL_SCROLLBAR_NEVER,
                        horBars ?
                        JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS :
                        JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
        // controls how fast you scroll with the wheelmouse.
        prettyCanvasScroller.getVerticalScrollBar().setUnitIncrement( Geometry.LEADING_PX );
        prettyCanvasScroller.setVisible( false );

        // both b&w and colour are invisible at this point.
        }

    /**
     * download java source, DOWNLOAD not display. Let user capture to disk. cannot do FileChooser and Write in unsigned
     * applet. so get browser to do it.
     */
    private void download()
        {
        try
            {
            final URL url;
            if ( asApplet )
                {
                // get original document, not the *.ser
                url = new URL( getDocumentBase(), "snippet/" + snippetName );
                getAppletContext().showDocument( url );
                }
            else
                {
                // url = new URL( snippetName );
                // we ignore the download if running standalone.
                // Ideally we should do some download dialog here.
                }
            }
        catch ( MalformedURLException e )
            {
            System.err.println( "\007problem downloading " + snippetName
                                /*
                                * no
                                * .ser,
                                * this
                                * is
                                * the
                                * human
                                * readable
                                * fragment
                                */ + " : " + e.getMessage() );
            e.printStackTrace();
            }
        }// end download

    /**
     * Gets an array of preparsed serialized tokens from website representing this program. Gets from snippetName.
     * Leaves the array in tokens.
     */
    private void fetchTokens()
        {
        // Read serialiased tokens from a compressed URL.
        tokens = null;
        // O P E N
        // Generate an HTTP GET Command
        final ObjectInputStream ois;
        footprint = null;
        try
            {
            // O P E N

            final URL url;
            // get corresponding *.ser
            if ( asApplet )
                {
                url =
                        new URL( getDocumentBase(),
                                "snippet/ser/" + snippetName + ".ser" );
                }
            else
                {
                url = new URL( "file:snippet/ser/" + snippetName + ".ser" );
                }
            System.out.println( "fetching: " + url );
            final URLConnection urlc = url.openConnection();
            if ( urlc == null )
                {
                throw new IOException(
                        "\007ailed to connect to document server." );
                }
            urlc.setAllowUserInteraction( false );
            urlc.setDoInput( true );
            urlc.setDoOutput( false );
            urlc.setUseCaches( false );
            urlc.connect();
            final InputStream is = urlc.getInputStream();
            final GZIPInputStream gzis =
                    new GZIPInputStream( is, 4096/* buffsize */ );
            ois = new ObjectInputStream( gzis );

            // R E A D, footprintversion, footprint, tokens
            long expectedVersion = Footprint.serialVersionUID;
            long fileVersion = ( Long ) ois.readObject();
            if ( fileVersion != expectedVersion )
                {
                System.err
                        .println( "\007Stale "
                                  + snippetName
                                  + " Version  "
                                  + fileVersion
                                  + " should be "
                                  + expectedVersion );
                ois.close();
                tokens = new Token[0];
                return;
                }

            // we have to recompute it with our font metrics, but we want the
            // totalLines count.
            footprint = ( Footprint ) ois.readObject();

            tokens = ( Token[] ) ois.readObject();

            // C L O S E
            ois.close();
            }
        catch ( InvalidClassException e )
            {
            System.err.println( "\007Stale " + snippetName );
            }
        catch ( ClassNotFoundException e )
            {
            System.err
                    .println( "\007Bug: Token class files missing from jar " + e
                            .getMessage() );
            }
        catch ( IOException e )
            {
            e.printStackTrace();
            System.err
                    .println( "\007Problem getting compacted source document "

                              + snippetName + " : " + e.getMessage() );
            }

        if ( tokens == null )
            {
            tokens = new Token[0];
            }
        }

    /**
     * Get applet optional boolean parameter
     *
     * @param paramName    Name of the parameter. Case insensitive.
     * @param defaultValue default if param is missing.
     * @return Value of the parameter from the applet true or false
     * @noinspection SameParameterValue
     */
    private boolean getBooleanParameter( String paramName,
                                         boolean defaultValue )
        {
        String boolString = getParameter( paramName );
        if ( boolString == null )
            {
            return defaultValue;
            }
        else
            {
            if ( boolString.equalsIgnoreCase( "true" )
                 || boolString.equalsIgnoreCase( "yes" )
                 || boolString.equalsIgnoreCase( "t" )
                 || boolString.equalsIgnoreCase( "y" ) )
                {
                return true;
                }
            else if ( boolString.equalsIgnoreCase( "false" )
                      || boolString.equalsIgnoreCase( "no" )
                      || boolString.equalsIgnoreCase( "yes" )
                      || boolString.equalsIgnoreCase( "f" )
                      || boolString.equalsIgnoreCase( "n" ) )
                {
                return false;
                }
            else
                {
                throw new IllegalArgumentException( "JDisplay: "
                                                    + paramName
                                                    + " param: "
                                                    + boolString
                                                    + " should be true or false." );
                }
            }
        }// getBooleanParameter

    /**
     * Get parameters from Applet, but only when running as Applet
     */
    private void getParams()
        {
        // We are in Applet, don't have parms yet.

        this.snippetName = getParameter( "snippet" );

        if ( this.snippetName == null )
            {
            throw new IllegalArgumentException( "missing snippet parameter" );
            }
        // should not have leading snippet/ser/
        if ( this.snippetName.startsWith( "snippet/" ) )
            {
            this.snippetName =
                    this.snippetName.substring( "snippet/".length() );
            }
        if ( this.snippetName.startsWith( "ser/" ) )
            {
            this.snippetName = this.snippetName.substring( "ser/".length() );
            }
        // in Java 1.6 the above code fails to prepend.
        // can't use assert. This code has to compile under JDK 1.2

        hasBar = getBooleanParameter( "bar", true );
        }

    /**
     * layout the components
     */

    /**
     * layout components
     */
    private void layoutGridBag()
        {
        /*  layout
        * ----0--------1--------2--------
        * colour--lineNumbers--download-- 0
        * pretty ------------------------ 1
        * plain ------------------------- 2
        */
        if ( hasBar )
            {
            /*
             * The bar is not a component, just the camouflage Applet background
             * showing through.
             */

            // x y w h wtx wty anchor fill T L B R padx pady
            contentPane.add( colour,
                    new GridBagConstraints( 0,
                            0,
                            1,
                            1,
                            0.0,
                            0.0,
                            GridBagConstraints.NORTHWEST,
                            GridBagConstraints.NONE,
                            new Insets( 0, 0, 2, 0 ),
                            0,
                            0 ) );

            // x y w h wtx wty anchor fill T L B R padx pady
            contentPane.add( lineNumbers,
                    new GridBagConstraints( 1,
                            0,
                            1,
                            1,
                            0.0,
                            0.0,
                            GridBagConstraints.NORTH,
                            GridBagConstraints.NONE,
                            new Insets( 0, 10, 2, 0 ),
                            0,
                            0 ) );

            // x y w h wtx wty anchor fill T L B R padx pady
            contentPane.add( download,
                    new GridBagConstraints( 2,
                            0,
                            1,
                            1,
                            0.0,
                            0.0,
                            GridBagConstraints.NORTHEAST,
                            GridBagConstraints.NONE,
                            new Insets( 0, 10, 2, 0 ),
                            0,
                            0 ) );
            }
        // x y w h wtx wty anchor fill T L B R padx pady
        contentPane.add( prettyCanvasScroller,
                new GridBagConstraints( 0,
                        1,
                        3,
                        1,
                        1.0,
                        1.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 0, 0, 0, 0 ),
                        0,
                        0 ) );

        // x y w h wtx wty anchor fill T L B R padx pady
        contentPane.add( plainText,
                new GridBagConstraints( 0,
                        2
                        /* place it beside canvas, though actually only one visible at a time */,
                        3,
                        1,
                        1.0,
                        1.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 0, 0, 0, 0 ),
                        0,
                        0 ) );
        }

    /**
     * refresh the display based on whether should use colour and line numbers.
     */
    private void redisplay()
        {
        final boolean useColour = colour.isSelected();
        final boolean useLineNumbers = lineNumbers.isSelected();
        // can't have line numbers in B&W state

        final int width =
                useLineNumbers
                ? footprint.scrollableWidthWithLineNumbers
                : footprint.scrollableWidthWithoutLineNumbers;
        final int height = footprint.scrollableHeight;
        /*
         * Tell the prettyCanvas how we want the tokens rendered.
         */
        prettyCanvas.set( width,
                height,
                useLineNumbers,
                footprint.lineNumberWidthInPixels );

        if ( useColour )
            {
            // leave B&W connected, just not visible.
            // disconnect B&W component and connect colour

            lineNumbers.setEnabled( true );
            lineNumbers.setVisible( true );
            plainText.setVisible( false );
            prettyCanvas.setVisible( true );
            prettyCanvasScroller.setVisible( true );
            // invalidate all outer containers
            prettyCanvas.revalidate();
            prettyCanvasScroller.repaint();
            }
        else
            {
            // Leave colour connected, just not visible.
            lineNumbers.setEnabled( false );
            lineNumbers.setVisible( false );
            lineNumbers.setSelected( false );
            if ( !plainTextLoaded )
                {
                plainText.setTokens( tokens );
                plainTextLoaded = true;
                }
            prettyCanvasScroller.setVisible( false );
            prettyCanvas.setVisible( false );
            plainText.setVisible( true );
            // invalidate all outer containers
            plainText.revalidate();
            plainText.repaint();
            }
        }

    // --------------------------- main() method ---------------------------

    /**
     * Allow this applet to run as as application as well.
     *
     * @param args url of text file to display e.g. abs.example1.javafrag . CWD must be E:\mindprod\jgloss\
     */
    public static void main( String args[] )
        {
        if ( args.length == 0 )
            {
            throw new IllegalArgumentException( "missing snippet parameter" );
            }

        HybridJ.fireup( new JDisplay( args[ 0 ] ),
                TITLE_STRING + " " + VERSION_STRING,
                APPLET_WIDTH,
                APPLET_HEIGHT );
        }// end main
    }
