/*
 * Copyright 2004-2010, Thorbjørn Lindeijer <thorbjorn@lindeijer.nl>
 * Copyright 2004-2006, Adam Turk <aturk@biggeruniverse.com>
 *
 * This file is part of libtiled-java.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

package tiled.core;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.util.HashMap;
import java.util.Properties;

/**
 * A TileLayer is a specialized MapLayer, used for tracking two dimensional
 * tile data.
 */
public class TileLayer extends MapLayer
{
    protected Tile[][] map;
    protected HashMap<Object, Properties> tileInstanceProperties = new HashMap<Object, Properties>();

    public Properties getTileInstancePropertiesAt(int x, int y) {
        if (!bounds.contains(x, y)) {
            return null;
        }
        Object key = new Point(x, y);
        return tileInstanceProperties.get(key);
    }

    public void setTileInstancePropertiesAt(int x, int y, Properties tip) {
        if (bounds.contains(x, y)) {
            Object key = new Point(x, y);
            tileInstanceProperties.put(key, tip);
        }
    }

    /**
     * Default constructor.
     */
    public TileLayer() {
    }

    /**
     * Construct a TileLayer from the given width and height.
     *
     * @param w width in tiles
     * @param h height in tiles
     */
    public TileLayer(int w, int h) {
        super(w, h);
    }

    /**
     * Create a tile layer using the given bounds.
     *
     * @param r the bounds of the tile layer.
     */
    public TileLayer(Rectangle r) {
        super(r);
    }

    /**
     * @param m the map this layer is part of
     */
    TileLayer(Map m) {
        super(m);
    }

    /**
     * @param m the map this layer is part of
     * @param w width in tiles
     * @param h height in tiles
     */
    public TileLayer(Map m, int w, int h) {
        super(w, h);
        setMap(m);
    }

    /**
     * Rotates the layer by the given Euler angle.
     *
     * @param angle The Euler angle (0-360) to rotate the layer array data by.
     * @see MapLayer#rotate(int)
     */
    public void rotate(int angle) {
        Tile[][] trans;
        int xtrans = 0, ytrans = 0;

        switch (angle) {
            case ROTATE_90:
                trans = new Tile[bounds.width][bounds.height];
                xtrans = bounds.height - 1;
                break;
            case ROTATE_180:
                trans = new Tile[bounds.height][bounds.width];
                xtrans = bounds.width - 1;
                ytrans = bounds.height - 1;
                break;
            case ROTATE_270:
                trans = new Tile[bounds.width][bounds.height];
                ytrans = bounds.width - 1;
                break;
            default:
                System.out.println("Unsupported rotation (" + angle + ")");
                return;
        }

        double ra = Math.toRadians(angle);
        int cos_angle = (int)Math.round(Math.cos(ra));
        int sin_angle = (int)Math.round(Math.sin(ra));

        for (int y = 0; y < bounds.height; y++) {
            for (int x = 0; x < bounds.width; x++) {
                int xrot = x * cos_angle - y * sin_angle;
                int yrot = x * sin_angle + y * cos_angle;
                trans[yrot + ytrans][xrot + xtrans] = getTileAt(x+bounds.x, y+bounds.y);
            }
        }

        bounds.width = trans[0].length;
        bounds.height = trans.length;
        map = trans;
    }

    /**
     * Performs a mirroring function on the layer data. Two orientations are
     * allowed: vertical and horizontal.
     *
     * Example: <code>layer.mirror(MapLayer.MIRROR_VERTICAL);</code> will
     * mirror the layer data around a horizontal axis.
     *
     * @param dir the axial orientation to mirror around
     */
    public void mirror(int dir) {
        Tile[][] mirror = new Tile[bounds.height][bounds.width];
        for (int y = 0; y < bounds.height; y++) {
            for (int x = 0; x < bounds.width; x++) {
                if (dir == MIRROR_VERTICAL) {
                    mirror[y][x] = map[bounds.height - 1 - y][x];
                } else {
                    mirror[y][x] = map[y][bounds.width - 1 - x];
                }
            }
        }
        map = mirror;
    }

    /**
     * Checks to see if the given Tile is used anywhere in the layer.
     *
     * @param t a Tile object to check for
     * @return <code>true</code> if the Tile is used at least once,
     *         <code>false</code> otherwise.
     */
    public boolean isUsed(Tile t) {
        for (int y = 0; y < bounds.height; y++) {
            for (int x = 0; x < bounds.width; x++) {
                if (map[y][x] == t) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean isEmpty() {
        for (int p = 0; p < 2; p++) {
            for (int y = 0; y < bounds.height; y++) {
                for (int x = p; x < bounds.width; x += 2) {
                    if (map[y][x] != null)
                        return false;
                }
            }
        }
        return true;
    }

    /**
     * Sets the bounds (in tiles) to the specified Rectangle. <b>Caution:</b>
     * this causes a reallocation of the data array, and all previous data is
     * lost.
     *
     * @param bounds new new bounds of this tile layer (in tiles)
     * @see MapLayer#setBounds
     */
    protected void setBounds(Rectangle bounds) {
        super.setBounds(bounds);
        map = new Tile[bounds.height][bounds.width];

        // Tile instance properties is null when this method is called from
        // the constructor of MapLayer
        if (tileInstanceProperties != null) {
            tileInstanceProperties.clear();
        }
    }

    /**
     * Creates a diff of the two layers, <code>ml</code> is considered the
     * significant difference.
     *
     * @param ml
     * @return A new MapLayer that represents the difference between this
     *         layer, and the argument, or <b>null</b> if no difference exists.
     */
    public MapLayer createDiff(MapLayer ml) {
        if (ml == null) { return null; }

        if (ml instanceof TileLayer) {
            Rectangle r = null;

            for (int y = bounds.y; y < bounds.height + bounds.y; y++) {
                for (int x = bounds.x; x < bounds.width + bounds.x; x++) {
                    if (((TileLayer)ml).getTileAt(x, y) != getTileAt(x, y)) {
                        if (r != null) {
                            r.add(x, y);
                        } else {
                            r = new Rectangle(new Point(x, y));
                        }
                    }
                }
            }

            if (r != null) {
                MapLayer diff = new TileLayer(
                        new Rectangle(r.x, r.y, r.width + 1, r.height + 1));
                diff.copyFrom(ml);
                return diff;
            } else {
                return new TileLayer();
            }
        } else {
            return null;
        }
    }

    /**
     * Removes any occurences of the given tile from this map layer. If layer
     * is locked, an exception is thrown.
     *
     * @param tile the Tile to be removed
     */
    public void removeTile(Tile tile) {
        for (int y = 0; y < bounds.height; y++) {
            for (int x = 0; x < bounds.width; x++) {
                if (map[y][x] == tile) {
                    setTileAt(x + bounds.x, y + bounds.y, null);
                }
            }
        }
    }

    /**
     * Sets the tile at the specified position. Does nothing if (tx, ty) falls
     * outside of this layer.
     *
     * @param tx x position of tile
     * @param ty y position of tile
     * @param ti the tile object to place
     */
    public void setTileAt(int tx, int ty, Tile ti) {
        if (bounds.contains(tx, ty)) {
            map[ty - bounds.y][tx - bounds.x] = ti;
        }
    }

    /**
     * Returns the tile at the specified position.
     *
     * @param tx Tile-space x coordinate
     * @param ty Tile-space y coordinate
     * @return tile at position (tx, ty) or <code>null</code> when (tx, ty) is
     *         outside this layer
     */
    public Tile getTileAt(int tx, int ty) {
        return (bounds.contains(tx, ty)) ?
                map[ty - bounds.y][tx - bounds.x] : null;
    }

    /**
     * Returns the first occurrence (using top down, left to right search) of
     * the given tile.
     *
     * @param t the {@link Tile} to look for
     * @return A java.awt.Point instance of the first instance of t, or
     *         <code>null</code> if it is not found
     */
    public Point locationOf(Tile t) {
        for (int y = bounds.y; y < bounds.height + bounds.y; y++) {
            for (int x = bounds.x; x < bounds.width + bounds.x; x++) {
                if (getTileAt(x, y) == t) {
                    return new Point(x, y);
                }
            }
        }
        return null;
    }

    /**
     * Replaces all occurrences of the Tile <code>find</code> with the Tile
     * <code>replace</code> in the entire layer
     *
     * @param find    the tile to replace
     * @param replace the replacement tile
     */
    public void replaceTile(Tile find, Tile replace) {
        for (int y = bounds.y; y < bounds.y + bounds.height; y++) {
            for (int x = bounds.x; x < bounds.x + bounds.width; x++) {
                if(getTileAt(x,y) == find) {
                    setTileAt(x, y, replace);
                }
            }
        }
    }

    /**
     * @inheritDoc MapLayer#mergeOnto(MapLayer)
     */
    public void mergeOnto(MapLayer other) {
        for (int y = bounds.y; y < bounds.y + bounds.height; y++) {
            for (int x = bounds.x; x < bounds.x + bounds.width; x++) {
                Tile tile = getTileAt(x, y);
                if (tile != null) {
                    ((TileLayer) other).setTileAt(x, y, tile);
                }
            }
        }
    }

    /**
     * Like mergeOnto, but will only copy the area specified.
     *
     * @see TileLayer#mergeOnto(MapLayer)
     * @param other
     * @param mask
     */
    public void maskedMergeOnto(MapLayer other, Area mask) {
        Rectangle boundBox = mask.getBounds();

        for (int y = boundBox.y; y < boundBox.y + boundBox.height; y++) {
            for (int x = boundBox.x; x < boundBox.x + boundBox.width; x++) {
                Tile tile = ((TileLayer) other).getTileAt(x, y);
                if (mask.contains(x, y) && tile != null) {
                    setTileAt(x, y, tile);
                }
            }
        }
    }

    /**
     * Copy data from another layer onto this layer. Unlike mergeOnto,
     * copyFrom() copies the empty cells as well.
     *
     * @see MapLayer#mergeOnto
     * @param other
     */
    public void copyFrom(MapLayer other) {
        for (int y = bounds.y; y < bounds.y + bounds.height; y++) {
            for (int x = bounds.x; x < bounds.x + bounds.width; x++) {
                setTileAt(x, y, ((TileLayer) other).getTileAt(x, y));
            }
        }
    }

    /**
     * Like copyFrom, but will only copy the area specified.
     *
     * @see TileLayer#copyFrom(MapLayer)
     * @param other
     * @param mask
     */
    public void maskedCopyFrom(MapLayer other, Area mask) {
        Rectangle boundBox = mask.getBounds();

        for (int y = boundBox.y; y < boundBox.y + boundBox.height; y++) {
            for (int x = boundBox.x; x < boundBox.x + boundBox.width; x++) {
                if (mask.contains(x,y)) {
                    setTileAt(x, y, ((TileLayer) other).getTileAt(x, y));
                }
            }
        }
    }

    /**
     * Unlike mergeOnto, copyTo includes the null tile when merging.
     *
     * @see MapLayer#copyFrom
     * @see MapLayer#mergeOnto
     * @param other the layer to copy this layer to
     */
    public void copyTo(MapLayer other) {
        for (int y = bounds.y; y < bounds.y + bounds.height; y++) {
            for (int x = bounds.x; x < bounds.x + bounds.width; x++) {
                ((TileLayer) other).setTileAt(x, y, getTileAt(x, y));
            }
        }
    }

    /**
     * Creates a copy of this layer.
     *
     * @see Object#clone
     * @return a clone of this layer, as complete as possible
     * @exception CloneNotSupportedException
     */
    public Object clone() throws CloneNotSupportedException {
        TileLayer clone = (TileLayer) super.clone();

        // Clone the layer data
        clone.map = new Tile[map.length][];
        clone.tileInstanceProperties = new HashMap<Object, Properties>();

        for (int i = 0; i < map.length; i++) {
            clone.map[i] = new Tile[map[i].length];
            System.arraycopy(map[i], 0, clone.map[i], 0, map[i].length);

            for (int j = 0; j < map[i].length; j++) {
                Properties p = getTileInstancePropertiesAt(i, j);

                if (p != null) {
                    Integer key = i + j * bounds.width;
                    clone.tileInstanceProperties.put(key, (Properties) p.clone());
                }
            }
        }

        return clone;
    }

    /**
     * @param width  the new width of the layer
     * @param height the new height of the layer
     * @param dx     the shift in x direction
     * @param dy     the shift in y direction
     */
    public void resize(int width, int height, int dx, int dy) {
        Tile[][] newMap = new Tile[height][width];
        HashMap<Object, Properties> newTileInstanceProperties = new HashMap<Object, Properties>();

        int maxX = Math.min(width, bounds.width + dx);
        int maxY = Math.min(height, bounds.height + dy);

        for (int x = Math.max(0, dx); x < maxX; x++) {
            for (int y = Math.max(0, dy); y < maxY; y++) {
                newMap[y][x] = getTileAt(x - dx, y - dy);

                Properties tip = getTileInstancePropertiesAt(x - dx, y - dy);
                if (tip != null) {
                    newTileInstanceProperties.put(new Point(x, y), tip);
                }
            }
        }

        map = newMap;
        tileInstanceProperties = newTileInstanceProperties;
        bounds.width = width;
        bounds.height = height;
    }
}
