/**
 * ThisTime 2.0 (2008-03-30)
 * Copyright 2007 Zach Scrivena
 * zachscrivena@gmail.com
 * http://thistime.sourceforge.net/
 *
 * Simple clock and timer program.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package thistime;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JComponent;


/**
 * Clock display.
 */
class ClockDisplay
        extends JComponent
{
    /** unscaled images for digits, colon, AM, and PM (first 10 elements are digits 0-9) */
    private static final BufferedImage[] UNSCALED_IMAGES = new BufferedImage[13];

    /** array index of the colon image in UNSCALED_IMAGES[] */
    private static final int COLON_INDEX = 10;

    /** array index of the AM image in UNSCALED_IMAGES[] */
    private static final int AM_INDEX = 11;

    /** array index of the PM image in UNSCALED_IMAGES[] */
    private static final int PM_INDEX = 12;

    /** width of an unscaled digit image */
    private static int UNSCALED_DIGIT_WIDTH;

    /** height of an unscaled digit image */
    private static int UNSCALED_DIGIT_HEIGHT;

    /** width of the unscaled colon image */
    private static int UNSCALED_COLON_WIDTH;

    /** width of the unscaled AM/PM image */
    private static int UNSCALED_AM_WIDTH;

    /** height of the unscaled AM/PM image */
    private static int UNSCALED_AM_HEIGHT;

    /** scaling ratio for a small digit (should be less than 1) */
    private static final double SMALL_DIGIT_RATIO = 0.6;

    /** horizontal and vertical padding between digits */
    private static final int PADDING = 20;

    /** scaled images for digits, colon, AM, and PM (first 10 elements are digits 0-9) */
    private final Image[] scaledImages = new Image[13];

    /** scaled images for small digits */
    private final Image[] smallScaledImages = new Image[10];

    /** hash code for the unscaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
    private int unscaledSizeHashCode = -1;

    /** hash code for the scaled total width and height corresponding to scaledImages[] and smallScaledImages[] */
    private int scaledSizeHashCode = -1;

    /** clock value to be displayed */
    private final ClockValue value;


    /**
    * Static initialization block for loading image resources.
    * The following assumptions are made:
    * 1. All digit images have the same dimensions.
    * 2. Digits and the colon images have the same height.
    * 3. AM/PM images have the same dimensions.
    */
    static
    {
        try
        {
            for (int i = 0; i <= 9; i++)
            {
                UNSCALED_IMAGES[i] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/digit" + i + ".png"));
            }

            UNSCALED_IMAGES[COLON_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/colon.png"));
            UNSCALED_IMAGES[AM_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/am.png"));
            UNSCALED_IMAGES[PM_INDEX] = ImageIO.read(ClockDisplay.class.getResource("/thistime/resources/pm.png"));

            UNSCALED_DIGIT_WIDTH = UNSCALED_IMAGES[0].getWidth();
            UNSCALED_DIGIT_HEIGHT = UNSCALED_IMAGES[0].getHeight();
            UNSCALED_COLON_WIDTH = UNSCALED_IMAGES[COLON_INDEX].getWidth();
            UNSCALED_AM_WIDTH = UNSCALED_IMAGES[AM_INDEX].getWidth();
            UNSCALED_AM_HEIGHT = UNSCALED_IMAGES[AM_INDEX].getHeight();
        }
        catch (Exception e)
        {
            throw new RuntimeException(e.getMessage(), e.getCause());
        }
    }


    /**
    * Constructor.
    *
    * @param value
    *      "normalized" timer value to be displayed
    * @param setMode
    *      is the timer display in "set" mode initially?
    */
    ClockDisplay(
            final ClockValue value)
    {
        this.value = value;
    }


    /**
    * Paint the clock display. This method is invoked by Swing to draw components.
    *
    * @param g
    *      the Graphics context in which to paint
    */
    @Override
    public void paint(
            Graphics g)
    {
        synchronized (value)
        {
            doAction(Action.PAINT, g);
        }
    }


    /**
    * Perform the specified action on the display.
    * Assumes that value has been externally synchronized.
    * This method must run on the EDT.
    *
    * @param action
    *      action to be performed
    * @param arg
    *      argument value for the action
    */
    private void doAction(
            final Action action,
            final Object arg)
    {
        /************************************************
        * (1) PARSE AGRUMENTS FOR THE SPECIFIED ACTION *
        ************************************************/

        Graphics g = null;

        switch (action)
        {
            case PAINT:
                g = (Graphics) arg;
                break;
        }

        /******************************************************
        * (2) COMPUTE DIMENSIONS OF CLOCK DISPLAY COMPONENTS *
        ******************************************************/

        /* length of the hour-value */
        final int hourValueLength = (value.hour24) ? 2 : String.valueOf(value.h).length();

        /* width and height of the canvas */
        final Rectangle bounds = getBounds();
        final int canvasWidth = (int) bounds.getWidth();
        final int canvasHeight = (int) bounds.getHeight();

        /* total width and height of timer display, in unscaled units */
        final int unscaledTotalWidth = UNSCALED_COLON_WIDTH +
                (int) ((SMALL_DIGIT_RATIO * 2 + hourValueLength + 2) * UNSCALED_DIGIT_WIDTH) +
                (6 + hourValueLength) * PADDING;

        final int unscaledTotalHeight = UNSCALED_DIGIT_HEIGHT + 2 * PADDING;

        /* compute scaled widths and heights */
        final Dimension scaledDimension = GraphicsManipulator.scaleToFitKeepRatio(
                unscaledTotalWidth, unscaledTotalHeight,
                canvasWidth, canvasHeight);

        final int scaledTotalWidth = (int) scaledDimension.getWidth();
        final int scaledTotalHeight = (int) scaledDimension.getHeight();

        final int digitWidth = scaledTotalWidth * UNSCALED_DIGIT_WIDTH / unscaledTotalWidth;
        final int digitHeight = scaledTotalHeight * UNSCALED_DIGIT_HEIGHT / unscaledTotalHeight;
        final int colonWidth = scaledTotalWidth * UNSCALED_COLON_WIDTH / unscaledTotalWidth;
        final int smallDigitWidth = (int) (SMALL_DIGIT_RATIO * digitWidth);
        final int smallDigitHeight = (int) (SMALL_DIGIT_RATIO * digitHeight);
        final int padding = scaledTotalWidth * PADDING / unscaledTotalWidth;

        final Dimension scaledAmDimension = GraphicsManipulator.scaleToFitKeepRatio(
                UNSCALED_AM_WIDTH,
                UNSCALED_AM_HEIGHT,
                2 * smallDigitWidth + padding,
                scaledTotalHeight - smallDigitHeight - 3 * padding);

        final int amWidth = (int) scaledAmDimension.getWidth();
        final int amHeight = (int) scaledAmDimension.getHeight();

        /* initial horizontal and vertical offsets */
        int offsetX = padding + (canvasWidth - scaledTotalWidth) / 2;
        int offsetY = padding + (canvasHeight - scaledTotalHeight) / 2;

        /***************************************************
        * (3) CHECK IF CACHED SCALED IMAGES CAN BE REUSED *
        ***************************************************/

        /* compute new hash codes */
        final int newUnscaledSizeHashCode = unscaledTotalWidth * unscaledTotalHeight;
        final int newScaledSizeHashCode = scaledTotalWidth * scaledTotalHeight;

        if ((newUnscaledSizeHashCode != unscaledSizeHashCode) ||
                (newScaledSizeHashCode != scaledSizeHashCode))
        {
            /* proceed to generate new scaled images */
            for (int i = 0; i <= 9; i++)
            {
                scaledImages[i] = ((digitWidth == 0) || (digitHeight == 0)) ?
                    UNSCALED_IMAGES[i] :
                    UNSCALED_IMAGES[i].getScaledInstance(digitWidth, digitHeight, Image.SCALE_SMOOTH);

                smallScaledImages[i] = ((smallDigitWidth == 0) || (smallDigitHeight == 0)) ?
                    UNSCALED_IMAGES[i] :
                    UNSCALED_IMAGES[i].getScaledInstance(smallDigitWidth, smallDigitHeight, Image.SCALE_SMOOTH);
            }

            scaledImages[COLON_INDEX] = ((colonWidth == 0) || (digitHeight == 0)) ?
                UNSCALED_IMAGES[COLON_INDEX] :
                UNSCALED_IMAGES[COLON_INDEX].getScaledInstance(colonWidth, digitHeight, Image.SCALE_SMOOTH);

            scaledImages[AM_INDEX] = ((amWidth == 0) || (amHeight == 0)) ?
                UNSCALED_IMAGES[AM_INDEX] :
                UNSCALED_IMAGES[AM_INDEX].getScaledInstance(amWidth, amHeight, Image.SCALE_SMOOTH);

            scaledImages[PM_INDEX] = ((amWidth == 0) || (amHeight == 0)) ?
                UNSCALED_IMAGES[PM_INDEX] :
                UNSCALED_IMAGES[PM_INDEX].getScaledInstance(amWidth, amHeight, Image.SCALE_SMOOTH);

            /* update hash codes */
            unscaledSizeHashCode = newUnscaledSizeHashCode;
            scaledSizeHashCode = newScaledSizeHashCode;
        }

        /***********************
         * (4) DISPLAY "HOURS" *
         ***********************/

        /* draw the digits in "hours", starting with the most significant digit */
        long hourValueLeft = value.h;

        for (int i = (hourValueLength - 1); i >= 0; i--)
        {
            final int divisor = (int) Math.pow(10, i);
            final int digit = (int) (hourValueLeft / divisor);
            hourValueLeft -= (digit * divisor);

            if (action == Action.PAINT)
            {
                g.drawImage(scaledImages[digit], offsetX, offsetY, null);
            }

            offsetX += (digitWidth + padding);
        }

        /* draw the colon */
        if (action == Action.PAINT)
        {
            g.drawImage(scaledImages[COLON_INDEX], offsetX, offsetY, null);
        }

        offsetX += (colonWidth + padding);

        /*************************
         * (5) DISPLAY "MINUTES" *
         *************************/

        /* draw the digits in "minutes", starting with the most significant digit */
        int minuteValueLeft = value.m;

        for (int i = 1; i >= 0; i--)
        {
            final int divisor = (int) Math.pow(10, i);
            final int digit = minuteValueLeft / divisor;
            minuteValueLeft -= (digit * divisor);

            if (action == Action.PAINT)
            {
                g.drawImage(scaledImages[digit], offsetX, offsetY, null);
            }

            offsetX += (digitWidth + padding);
        }

        /*******************************************
        * (6) PAINT AM/PM, IF 12-HOUR TIME FORMAT *
        *******************************************/

        if (!value.hour24)
        {
            if (action == Action.PAINT)
            {
                g.drawImage(
                        scaledImages[(value.am) ? AM_INDEX : PM_INDEX],
                        offsetX + (2 * smallDigitWidth + padding - amWidth) / 2,
                        offsetY + (scaledTotalHeight - smallDigitHeight - 3 * padding - amHeight) / 2,
                        null);
            }
        }

        /*************************
         * (7) DISPLAY "SECONDS" *
         *************************/

        /* draw the digits in "seconds", starting with the most significant digit */
        int secondValueLeft = value.s;

        for (int i = 1; i >= 0; i--)
        {
            final int divisor = (int) Math.pow(10, i);
            final int digit = secondValueLeft / divisor;
            secondValueLeft -= (digit * divisor);

            if (action == Action.PAINT)
            {
                g.drawImage(smallScaledImages[digit], offsetX, offsetY + digitHeight - smallDigitHeight, null);
            }

            offsetX += (smallDigitWidth + padding);
        }
    }

    /*****************
    * INNER CLASSES *
    *****************/

    /**
    * Types of action for the doAction() method.
    */
    private static enum Action
    {
        PAINT
    };
}