//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// 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.miso.tile;

import java.lang.ref.WeakReference;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.image.BufferedImage;

import com.google.common.collect.Lists;

import com.samskivert.util.CheapIntMap;
import com.samskivert.util.QuickSort;

import com.threerings.media.image.BufferedMirage;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.ImageUtil;
import com.threerings.media.tile.NoSuchTileSetException;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileManager;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileUtil;

import com.threerings.miso.data.MisoSceneModel;

import static com.threerings.miso.Log.log;

/**
 * Automatically fringes a scene according to the rules in the supplied fringe configuration.
 */
public class AutoFringer
{
    public static class FringeTile extends BaseTile
    {
        public FringeTile (long[] fringeId, boolean passable)
        {
            setPassable(passable);
            _fringeId = fringeId;
        }

        @Override
        public boolean equals (Object obj)
        {
            if (!(obj instanceof FringeTile)) {
                return false;
            }
            FringeTile fObj = (FringeTile)obj;
            return _passable == fObj._passable && Arrays.equals(_fringeId, fObj._fringeId);
        }

        @Override
        public int hashCode ()
        {
            int result = 33; // can't use Arrays.hashCode(long) as it's 1.5 only
            for (long key : _fringeId) {
                result = result * 37 + (int)(key ^ (key >>> 32));
            }
            if (_passable) {
                result++;
            }
            return result;
        }

        /** The fringe keys of the tiles that went into this tile in the order they were drawn. */
        protected long[] _fringeId;
    }

    /**
     * Constructs an instance that will fringe according to the rules in the supplied fringe
     * configuration.
     */
    public AutoFringer (FringeConfiguration fringeconf, ImageManager imgr, TileManager tmgr)
    {
        _fringeconf = fringeconf;
        _imgr = imgr;
        _tmgr = tmgr;
    }

    /**
     * Returns the fringe configuration used by this fringer.
     */
    public FringeConfiguration getFringeConf ()
    {
        return _fringeconf;
    }

    /**
     * Compute and return the fringe tile to be inserted at the specified location.
     */
    public BaseTile getFringeTile (MisoSceneModel scene, int col, int row,
        Map<FringeTile, WeakReference<FringeTile>> fringes, Map<Long, BufferedImage> masks)
    {
        // get the tileset id of the base tile we are considering
        int underset = adjustTileSetId(scene.getBaseTileId(col, row) >> 16);

        // start with a clean temporary fringer map
        _fringers.clear();
        boolean passable = true;

        // walk through our influence tiles
        for (int y = row - 1, maxy = row + 2; y < maxy; y++) {
            for (int x = col - 1, maxx = col + 2; x < maxx; x++) {
                // we sensibly do not consider ourselves
                if ((x == col) && (y == row)) {
                    continue;
                }

                // determine the tileset for this tile
                int btid = scene.getBaseTileId(x, y);
                int baseset = adjustTileSetId((btid <= 0) ?
                    scene.getDefaultBaseTileSet() : (btid >> 16));

                // determine if it fringes on our tile
                int pri = _fringeconf.fringesOn(baseset, underset);
                if (pri == -1) {
                    continue;
                }

                FringerRec fringer = (FringerRec)_fringers.get(baseset);
                if (fringer == null) {
                    fringer = new FringerRec(baseset, pri);
                    _fringers.put(baseset, fringer);
                }

                // now turn on the appropriate fringebits
                fringer.bits |= FLAGMATRIX[y - row + 1][x - col + 1];

                // See if a tile that fringes on us kills our passability,
                // but don't count the default base tile against us, as
                // we allow users to splash in the water.
                if (passable && (btid > 0)) {
                    try {
                        BaseTile bt = (BaseTile)_tmgr.getTile(btid);
                        passable = bt.isPassable();
                    } catch (NoSuchTileSetException nstse) {
                        log.warning("Autofringer couldn't find a base set while attempting to " +
                            "figure passability", nstse);
                    }
                }
            }
        }

        // if nothing fringed, we're done
        int numfringers = _fringers.size();
        if (numfringers == 0) {
            return null;
        }

        // otherwise compose a FringeTile from the specified fringes
        FringerRec[] frecs = new FringerRec[numfringers];
        for (int ii = 0, pp = 0; ii < 16; ii++) {
            FringerRec rec = (FringerRec)_fringers.getValue(ii);
            if (rec != null) {
                frecs[pp++] = rec;
            }
        }

        return composeFringeTile(frecs, fringes, TileUtil.getTileHash(col, row), passable, masks);
    }

    /**
     * Compose a FringeTile out of the various fringe images needed.
     */
    protected FringeTile composeFringeTile (FringerRec[] fringers,
        Map<FringeTile, WeakReference<FringeTile>> fringes, int hashValue, boolean passable,
        Map<Long, BufferedImage> masks)
    {
        // sort the array so that higher priority fringers get drawn first
        QuickSort.sort(fringers);

        // Generate an identifier for the fringe tile being created as an array of the keys of its
        // component tiles in the order they'll be drawn in the fringe tile.
        List<Long> keys = Lists.newArrayList();
        for (FringerRec fringer : fringers) {
            int[] indexes = getFringeIndexes(fringer.bits);
            FringeConfiguration.FringeTileSetRecord tsr = _fringeconf.getFringe(
                fringer.baseset, hashValue);
            int fringeset = tsr.fringe_tsid;
            for (int index : indexes) {
                // Add a key for this tile as a long containing its base tile, the fringe set it's
                // working with and the index used in that set.
                keys.add((((long)fringer.baseset) << 32) + (fringeset << 16) + index);
            }
        }
        long[] fringeId = new long[keys.size()];
        for (int ii = 0; ii < fringeId.length; ii++) {
            fringeId[ii] = keys.get(ii);
        }
        FringeTile frTile = new FringeTile(fringeId, passable);

        // If the fringes map contains something with the same fringe identifier, this will pull
        // it out and we can use it instead.
        WeakReference<FringeTile> result = fringes.get(frTile);
        if (result != null) {
            FringeTile fringe = result.get();
            if (fringe != null) {
                return fringe;
            }
        }

        // There's no fringe with he same identifier, so we need to create the tile.
        BufferedImage img = null;
        for (FringerRec fringer : fringers) {
            int[] indexes = getFringeIndexes(fringer.bits);
            FringeConfiguration.FringeTileSetRecord tsr = _fringeconf.getFringe(
                fringer.baseset, hashValue);
            for (int index : indexes) {
                try {
                    img = getTileImage(img, tsr, fringer.baseset, index, hashValue, masks);
                } catch (NoSuchTileSetException nstse) {
                    log.warning("Autofringer couldn't find a needed tileset", nstse);
                }
            }
        }
        frTile.setImage(new BufferedMirage(img));
        fringes.put(frTile, new WeakReference<FringeTile>(frTile));
        return frTile;
    }

    /**
     * Retrieve or compose an image for the specified fringe.
     */
    protected BufferedImage getTileImage (BufferedImage img,
        FringeConfiguration.FringeTileSetRecord tsr, int baseset, int index, int hashValue,
        Map<Long, BufferedImage> masks)
        throws NoSuchTileSetException
    {
        int fringeset = tsr.fringe_tsid;
        TileSet fset = _tmgr.getTileSet(fringeset);
        if (!tsr.mask) {
            // oh good, this is easy
            Tile stamp = fset.getTile(index);
            return stampTileImage(stamp, img, stamp.getWidth(), stamp.getHeight());
        }

        // otherwise, it's a mask..
        Long maskkey = Long.valueOf((((long)baseset) << 32) + (fringeset << 16) + index);
        BufferedImage mask = masks.get(maskkey);
        if (mask == null) {
            BufferedImage fsrc = _tmgr.getTileSet(fringeset).getRawTileImage(index);
            BufferedImage bsrc = _tmgr.getTileSet(baseset).getRawTileImage(0);
            mask = ImageUtil.composeMaskedImage(_imgr, fsrc, bsrc);
            masks.put(maskkey, mask);
        }

        return stampTileImage(mask, img, mask.getWidth(null), mask.getHeight(null));
    }

    /** Helper function for {@link #getTileImage}. */
    protected BufferedImage stampTileImage (Object stamp, BufferedImage ftimg, int width,
        int height)
    {
        // create the target image if necessary
        if (ftimg == null) {
            ftimg = _imgr.createImage(width, height, Transparency.BITMASK);
        }
        Graphics2D gfx = (Graphics2D)ftimg.getGraphics();
        try {
            if (stamp instanceof Tile) {
                ((Tile)stamp).paint(gfx, 0, 0);
            } else {
                gfx.drawImage((BufferedImage)stamp, 0, 0, null);
            }
        } finally {
            gfx.dispose();
        }
        return ftimg;
    }

    /**
     * Get the fringe index specified by the fringebits. If no index is available, try breaking
     * down the bits into contiguous regions of bits and look for indexes for those.
     */
    protected int[] getFringeIndexes (int bits)
    {
        int index = BITS_TO_INDEX[bits];
        if (index != -1) {
            int[] ret = new int[1];
            ret[0] = index;
            return ret;
        }

        // otherwise, split the bits into contiguous components

        // look for a zero and start our first split
        int start = 0;
        while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) {
            start++;
        }

        if (start == NUM_FRINGEBITS) {
            // we never found an empty fringebit, and since index (above)
            // was already -1, we have no fringe tile for these bits.. sad.
            return new int[0];
        }

        ArrayList<Integer> indexes = Lists.newArrayList();
        int weebits = 0;
        for (int ii = (start + 1) % NUM_FRINGEBITS; ii != start; ii = (ii + 1) % NUM_FRINGEBITS) {

            if (((1 << ii) & bits) != 0) {
                weebits |= (1 << ii);
            } else if (weebits != 0) {
                index = BITS_TO_INDEX[weebits];
                if (index != -1) {
                    indexes.add(index);
                }
                weebits = 0;
            }
        }
        if (weebits != 0) {
            index = BITS_TO_INDEX[weebits];
            if (index != -1) {
                indexes.add(index);
            }
        }

        int[] ret = new int[indexes.size()];
        for (int ii = 0; ii < ret.length; ii++) {
            ret[ii] = indexes.get(ii);
        }
        return ret;
    }

    /**
     * Allow subclasses to apply arbitrary modifications to tileset ids for whatever nefarious
     * purposes they may have.
     */
    protected int adjustTileSetId (int tileSetId)
    {
        // by default, nothing.
        return tileSetId;
    }

    /**
     * A record for holding information about a particular fringe as we're computing what it will
     * look like.
     */
    static protected class FringerRec
        implements Comparable<FringerRec>
    {
        int baseset;
        int priority;
        int bits;

        public FringerRec (int base, int pri)
        {
            baseset = base;
            priority = pri;
        }

        public int compareTo (FringerRec o)
        {
            return priority - o.priority;
        }

        @Override
        public String toString ()
        {
            return "[base=" + baseset + ", pri=" + priority + ", bits="
                + Integer.toString(bits, 16) + "]";
        }
    }

    // fringe bits
    // see docs/miso/fringebits.png
    //
    protected static final int NORTH     = 1 << 0;
    protected static final int NORTHEAST = 1 << 1;
    protected static final int EAST      = 1 << 2;
    protected static final int SOUTHEAST = 1 << 3;
    protected static final int SOUTH     = 1 << 4;
    protected static final int SOUTHWEST = 1 << 5;
    protected static final int WEST      = 1 << 6;
    protected static final int NORTHWEST = 1 << 7;

    protected static final int NUM_FRINGEBITS = 8;

    // A matrix mapping adjacent tiles to which fringe bits they affect.
    // (x and y are offset by +1, since we can't have -1 as an array index)
    // again, see docs/miso/fringebits.png
    //
    protected static final int[][] FLAGMATRIX = {
        { NORTHEAST, (NORTHEAST | EAST | SOUTHEAST), SOUTHEAST },
        { (NORTHWEST | NORTH | NORTHEAST), 0, (SOUTHEAST | SOUTH | SOUTHWEST) },
        { NORTHWEST, (NORTHWEST | WEST | SOUTHWEST), SOUTHWEST }
    };

    /**
     * The fringe tiles we use. These are the 17 possible tiles made up of continuous fringebits
     * sections. Huh? see docs/miso/fringebits.png
     */
    protected static final int[] FRINGETILES = {
        SOUTHEAST,
        SOUTHWEST | SOUTH | SOUTHEAST,
        SOUTHWEST,
        NORTHEAST | EAST | SOUTHEAST,
        NORTHWEST | WEST | SOUTHWEST,
        NORTHEAST,
        NORTHWEST | NORTH | NORTHEAST,
        NORTHWEST,

        SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST,
        NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST,
        NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
        SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,

        NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
        SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST,
        SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST,
        NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,

        // all the directions!
        NORTH | NORTHEAST | EAST | SOUTHEAST |
                SOUTH | SOUTHWEST | WEST | NORTHWEST
    };

    // A reverse map of the above array, for quickly looking up which tile
    // we want.
    protected static final int[] BITS_TO_INDEX;

    // Construct the BITS_TO_INDEX array.
    static {
        int num = (1 << NUM_FRINGEBITS);
        BITS_TO_INDEX = new int[num];

        // first clear everything to -1 (meaning there is no tile defined)
        for (int ii=0; ii < num; ii++) {
            BITS_TO_INDEX[ii] = -1;
        }

        // then fill in with the defined tiles.
        for (int ii=0; ii < FRINGETILES.length; ii++) {
            BITS_TO_INDEX[FRINGETILES[ii]] = ii;
        }
    }

    protected ImageManager _imgr;
    protected TileManager _tmgr;
    protected FringeConfiguration _fringeconf;
    protected CheapIntMap _fringers = new CheapIntMap(16);
}
