/*
 * @(#)JDisplay.java
 *
 * Summary: Displays a serialised pre-parsed Java program in fancy fonts and colours.
 *
 * Copyright: (c) 2004-2009 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.5+
 *
 * Created with: IntelliJ IDEA IDE.
 *
 * Version History:
 *  1.2 2004-05-15 - split off calcPreferredSize into its own
 *                   class
 *  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.
 *  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 2008-08-08 - add vanilla text parser for text files. No changes needed to JDisplay itself, just the bundle.
 *  4.0 2009-04-12 - shorter style names, improved highlighting.
 *  4.1 2009-08-30 - tone down colour for keywords.
 *  4.2 2009-09-30 - fine tune size of fonts, adjusting large or small fonts to normal size, shrink keyword size.
 */
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;

import static java.lang.System.err;
import static java.lang.System.out;

/**
 * Displays a serialised pre-parsed Java program in fancy fonts and colours.
 * <p/>
 * 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 4.2 2009-09-30 - fine tune size of fonts, adjusting large or small fonts to normal size, shrink keyword size.
 * @since 2004
 */
public final class JDisplay extends JApplet
    {
// ------------------------------ CONSTANTS ------------------------------

    /**
     * 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;

    /**
     * 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 = "2009-09-30";

    /**
     * Title
     */
    private static final String TITLE_STRING = "JDisplay";

    /**
     * Version, is no About box.
     */
    private static final String VERSION_STRING = "4.2";

    /**
     * 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 );

// ------------------------------ FIELDS ------------------------------

    /**
     * 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
        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 )
            {
            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" );
                }
            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 )
                {
                err.println( "\007Stale "
                             + snippetName
                             + " *.ser files are version  "
                             + fileVersion
                             + ". JDisplay is expecting  "
                             + 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 )
            {
            err.println( "\007Stale " + snippetName );
            }
        catch ( ClassNotFoundException e )
            {
            err.println( "\007Bug: Token class files missing from jar " + e.getMessage() );
            }
        catch ( IOException e )
            {
            e.printStackTrace();
            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
    }
