//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.puzzle.client;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;

import javax.swing.UIManager;

import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.samskivert.util.StringUtil;

import com.threerings.media.VirtualMediaPanel;
import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationAdapter;
import com.threerings.media.image.Mirage;
import com.threerings.media.sprite.Sprite;

import com.threerings.puzzle.Log;
import com.threerings.puzzle.data.Board;
import com.threerings.puzzle.data.PuzzleCodes;
import com.threerings.puzzle.data.PuzzleConfig;
import com.threerings.puzzle.util.PuzzleContext;

/**
 * The puzzle board view displays a view of a puzzle game.
 */
public abstract class PuzzleBoardView extends VirtualMediaPanel
{
    /**
     * Constructs a puzzle board view.
     */
    public PuzzleBoardView (PuzzleContext ctx)
    {
        super(ctx.getFrameManager());

        // keep this for later
        _ctx = ctx;
    }

    /**
     * Initializes the board with the board dimensions.
     */
    public void init (PuzzleConfig config)
    {
	// save off our bounds
        Dimension bounds = getPreferredSize();
	_bounds = new Rectangle(0, 0, bounds.width, bounds.height);
    }

    /**
     * Sets the board to be displayed.
     */
    public void setBoard (Board board)
    {
        _board = board;
    }

    /**
     * Provides the board view with a reference to its controller so that
     * it may communicate directly rather than by posting actions up the
     * interface hierarchy which sometimes fails if the puzzle board view
     * is hidden before we get a chance to post our actions.
     */
    public void setController (PuzzleController pctrl)
    {
        _pctrl = pctrl;
    }

    /**
     * Sets the background image displayed by the board view.
     */
    public void setBackgroundImage (Mirage image)
    {
        _background = image;
    }

    /**
     * Set whether this puzzle is paused or not.
     */
    public void setPaused (boolean paused)
    {
        if (paused) {
            String pmsg = _pctrl.getPauseString();
            pmsg = _ctx.getMessageManager().getBundle(
                PuzzleCodes.PUZZLE_MESSAGE_BUNDLE).xlate(pmsg);
            _pauseLabel = new Label(pmsg, Label.BOLD | Label.OUTLINE,
                                    Color.WHITE, Color.BLACK,
                                    _fonts[_fonts.length - 2]);
            _pauseLabel.setTargetWidth(_bounds.width);
            _pauseLabel.layout(this);
        } else {
            _pauseLabel = null;
        }
        repaint();
    }

    /**
     * Adds the given animation to the set of animations currently present
     * on the board.  The animation will be added to a list of action
     * animations whose count can be queried with {@link
     * #getActionAnimationCount}. The animation will automatically be
     * removed from the action list when it completes.
     */
    public void addActionAnimation (Animation anim)
    {
	super.addAnimation(anim);

        // remember the animation's existence
        _actionAnims.add(anim);

        // and listen for it to finish so that we can clear it out
        anim.addAnimationObserver(_actionAnimObs);
    }

    // documentation inherited
    public void abortAnimation (Animation anim)
    {
        super.abortAnimation(anim);

        // always check to see if it was action-y
        animationFinished(anim);
    }

    /**
     * Called when a potential action animation is finished.
     */
    protected void animationFinished (Animation anim)
    {
        if (DEBUG_ACTION) {
            Log.info("Animation cleared " + StringUtil.shortClassName(anim) +
                     ":" + _actionAnims.contains(anim));
        }

        // if it WAS an action animation, check for a clear
        if (_actionAnims.remove(anim)) {
            maybeFireCleared();
        }
    }

    /**
     * Adds the given sprite to the set of sprites currently present on
     * the board.  The sprite will be added to a list of action sprites
     * whose count can be queried with {@link #getActionSpriteCount}.  Callers
     * should be sure to remove the sprite when their work with it is done
     * via {@link #removeSprite}.
     */
    public void addActionSprite (Sprite sprite)
    {
	// add the piece to the sprite manager
        addSprite(sprite);

        // note that this piece is interesting
        _actionSprites.add(sprite);
    }

    /**
     * Removes the given sprite from the board.
     */
    public void removeSprite (Sprite sprite)
    {
        super.removeSprite(sprite);

        if (DEBUG_ACTION) {
            Log.info("Sprite cleared " + StringUtil.shortClassName(sprite) +
                     ":" + _actionSprites.contains(sprite));
        }

        // we just always check to see if it was action-y
	if (_actionSprites.remove(sprite)) {
            maybeFireCleared();
        }
    }

    /**
     * Returns the number of action animations on the board.
     */
    public int getActionAnimationCount ()
    {
        return _actionAnims.size();
    }

    /**
     * Returns the number of action sprites on the board.
     */
    public int getActionSpriteCount ()
    {
        return _actionSprites.size();
    }

    /**
     * Returns the count of action sprites and animations on the board.
     */
    public int getActionCount ()
    {
        return _actionSprites.size() + _actionAnims.size();
    }

    /**
     * Dumps to the logs, a list of interesting sprites and animations
     * currently active on the puzzle board.
     */
    public void dumpActors ()
    {
        StringUtil.Formatter fmt = new StringUtil.Formatter() {
            public String toString (Object obj) {
                return StringUtil.shortClassName(obj);
            }
        };
        Log.info("Board contents [board=" + StringUtil.shortClassName(this) +
                 ", sprites=" + StringUtil.listToString(_actionSprites, fmt) +
                 ", anims=" + StringUtil.listToString(_actionAnims, fmt) +
                 "].");
    }

    /**
     * Creates and returns an animation displaying the given string with
     * the specified parameters, floating it a short distance up the view.
     *
     * @param score the score text to display.
     * @param color the color of the text.
     * @param fontSize the size of the text; a value between 0 and {@link
     * #getPuzzleFontCount} - 1.
     * @param x the x-position at which the score is to be placed.
     * @param y the y-position at which the score is to be placed.
     */
    public ScoreAnimation createScoreAnimation (
        String score, Color color, int fontSize, int x, int y)
    {
        // create and configure the label
        Label label = new Label(score);
        label.setTargetWidth(_bounds.width);
        label.setStyle(Label.OUTLINE);
        label.setTextColor(color);
        label.setAlternateColor(Color.black);
        label.setFont(getPuzzleFont(fontSize));
        label.setAlignment(Label.CENTER);
        label.layout(this);

        // create the score animation
        ScoreAnimation anim = createScoreAnimation(label, x, y);
        anim.setRenderOrder(getScoreRenderOrder());
        return anim;
    }

    /**
     * Creates a score animation, allowing derived classes to use custom
     * animations that are customized following a call to
     * {@link #createScoreAnimation}.
     */
    protected ScoreAnimation createScoreAnimation (Label label, int x, int y)
    {
        return new ScoreAnimation(label, x, y);
    }

    /**
     * Returns the render order to be assigned to score animations by
     * default.  Derived classes may wish to override this method to
     * change the default render order.
     *
     * @see #createScoreAnimation
     */
    protected int getScoreRenderOrder ()
    {
        return 1;
    }

    /**
     * Returns the puzzle font to be used for the specified score.
     */
    public int getFont (String why, int score, int maxScore)
    {
        int fontSize = (int)(score*getPuzzleFontCount()/maxScore);
        fontSize = Math.min(FONT_SIZES.length-1, fontSize);
//         Log.info("Font for " + why + " (" + score + " of " + maxScore +
//                  ") => " + fontSize + ".");
        return fontSize;
    }

    /**
     * Returns the number of different puzzle font sizes so that those who
     * care to choose a font size out of the range of possible sizes can
     * do so gracefully.
     */
    public int getPuzzleFontCount ()
    {
        return FONT_SIZES.length;
    }

    /**
     * Returns the puzzle font of the specified size.
     *
     * @param size the desired font size; a value between 0 and {@link
     * #getPuzzleFontCount} - 1.
     */
    public static Font getPuzzleFont (int size)
    {
        return _fonts[size];
    }

    /**
     * Positions the supplied animation so as to avoid any active
     * animations previously registered with this method, and adds the
     * animation to the list of animations to be avoided by any future
     * avoid animations.
     */
    public void trackAvoidAnimation (Animation anim)
    {
        // reposition the animation as appropriate
        Rectangle abounds = new Rectangle(anim.getBounds());
        ArrayList avoidables = (ArrayList)_avoidAnims.clone();
        if (SwingUtil.positionRect(abounds, _vbounds, avoidables)) {
            anim.setLocation(abounds.x, abounds.y);
        }

        // add the animation to the list of avoidables
        _avoidAnims.add(anim);
        // keep an eye on it so that we can remove it when it's finished
        anim.addAnimationObserver(_avoidAnimObs);
    }

    // documentation inherited
    public void paintBehind (Graphics2D gfx, Rectangle dirty)
    {
        super.paintBehind(gfx, dirty);

        // render the background
        renderBackground(gfx, dirty);
    }

    /**
     * Fills the background of the board with the background color.
     */
    protected void renderBackground (Graphics2D gfx, Rectangle dirty)
    {
        gfx.setColor(getBackground());
  	gfx.fill(dirty);
    }

    // documentation inherited
    public void paintBetween (Graphics2D gfx, Rectangle dirty)
    {
        super.paintBetween(gfx, dirty);
// 	PerformanceMonitor.tick(this, "paint");
        renderBoard(gfx, dirty);
    }

    // documentation inherited
    protected void paintInFront (Graphics2D gfx, Rectangle dirty)
    {
        super.paintInFront(gfx, dirty);

        // if the action is paused, indicate as much
        if (_pauseLabel != null) {
            Dimension d = _pauseLabel.getSize();
            _pauseLabel.render(gfx,
                _vbounds.x + (_vbounds.width - d.width) / 2,
                _vbounds.y + (_vbounds.height - d.height) / 2);
        }
    }

    /**
     * Fires a {@link #ACTION_CLEARED} command iff we have no remaining
     * interesting sprites or animations.
     */
    protected void maybeFireCleared ()
    {
        if (DEBUG_ACTION) {
            Log.info("Maybe firing cleared " +
                     getActionCount() + ":" + isShowing());
        }
        if (getActionCount() == 0) {
            // we're probably in the middle of a tick() in an
            // animationDidFinish() call and we want everyone to finish
            // processing their business before we go clearing the action,
            // so we queue this up to be run after the tick is complete
            _ctx.getClient().getRunQueue().postRunnable(new Runnable() {
                public void run () {
                    _pctrl.boardActionCleared();
                }
            });
        }
    }

    /**
     * Renders the board contents to the given graphics context.
     * Sub-classes should implement this method to draw all of their
     * game-specific business.
     */
    protected abstract void renderBoard (Graphics2D gfx, Rectangle dirty);

    /** Our client context. */
    protected PuzzleContext _ctx;

    /** Our puzzle controller. */
    protected PuzzleController _pctrl;

    /** The board data to be displayed. */
    protected Board _board;

    /** The board's bounding rectangle. */
    protected Rectangle _bounds;

    /** The action animations on the board. */
    protected ArrayList _actionAnims = new ArrayList();

    /** The action sprites on the board. */
    protected ArrayList _actionSprites = new ArrayList();

    /** The animations that other animations may wish to avoid. */
    protected ArrayList _avoidAnims = new ArrayList();

    /** Our background image. */
    protected Mirage _background;

    /** A label to show when the puzzle is paused. */
    protected Label _pauseLabel;

    /** The distance in pixels that score animations float. */
    protected int _scoreDist = DEFAULT_SCORE_DISTANCE;

    /** Listens to our action animations and clears them when they're done. */
    protected AnimationAdapter _actionAnimObs = new AnimationAdapter() {
        public void animationCompleted (Animation anim, long when) {
            animationFinished(anim);
        }
    };

    /** Automatically removes avoid animations when they're done. */
    protected AnimationAdapter _avoidAnimObs = new AnimationAdapter() {
        public void animationCompleted (Animation anim, long when) {
            if (!_avoidAnims.remove(anim)) {
                Log.warning("Couldn't remove avoid animation?! " + anim + ".");
            }
        }
    };

    /** The puzzle fonts. */
    protected static Font[] _fonts;

    /** Temporary action debugging. */
    protected static boolean DEBUG_ACTION = false;

    // action state constants
    protected static final int ACTION_GOING = 0;
    protected static final int CLEAR_PENDING = 1;
    protected static final int ACTION_CLEARED = 2;

    /** The default vertical distance to float score animations. */
    protected static final int DEFAULT_SCORE_DISTANCE = 30;

    /** The mid-sized font used as the default size for score
     * animations. */
    protected static final int MEDIUM_FONT_SIZE = 1;

    /** The puzzle font sizes. */
    protected static final double[] FONT_SIZES = {
        24d, 30d, 36d, 42d, 52d, 68d };

    static {
        // create the puzzle fonts
        Font ofont = UIManager.getFont("Label.serifFont");
        if (ofont == null) {
            ofont = new Font("Helvetica", Font.PLAIN, 16);
        }
        ofont = ofont.deriveFont((float)FONT_SIZES[0]);
        _fonts = new Font[FONT_SIZES.length];
        for (int ii = 0; ii < _fonts.length; ii++) {
            double scale = FONT_SIZES[ii]/FONT_SIZES[0];
            _fonts[ii] = ofont.deriveFont(
                AffineTransform.getScaleInstance(scale * 1.1d, scale));
        }
    }
}
