/***********************************************************************

GECCO, a platform for command and control gaming

Copyright (C) 2001 Henrik Bäärnhielm, Andreas Enblom, Niklas
Hallenfur, Karl Hasselström, Henrik Hägerström, Oskar Linde, Jing Fu
Zi and Jon Åslund

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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
USA.

***********************************************************************/

package gecco.server.automaton;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.lang.*;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import javax.swing.ImageIcon;

/**
 *  This class manages the map of automatons. It can create a map given an
 *  image.
 *
 * @author     Henrik Bäärnhielm, Karl Hasselström, Henrik Hägerström
 * @created    April 23, 2001
 */
public class AutomatonMap {

    /**
     *  The map
     */
    public Automaton[][] theMap;
    /**
     *  The size of this array is 3*xsize*ysize. It holds the red,
     *  green and blue values for the pixels in the original map in
     *  sequential order. (North-west -> south-east, row by row)
     */
    public byte[] initialMapRGB;

    /**
     *  Description of the Field
     */
    private Class automatonClass;

    /**
     *  Description of the Field
     */
    private int xBound;
    /**
     *  Description of the Field
     */
    private int yBound;


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y) {
        automatonClass = _automatonClass;
        theMap = new Automaton[x][y];
        initialMapRGB = new byte[x * y * 3];
        xBound = x;
        yBound = y;
    }


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  map              The map image.
     * @param  colorToState    The color conversion function.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y, URL map, HashMap colorToState) {
        this(_automatonClass, x, y);
        setImage(map, colorToState);
    }


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  map              The map image.
     * @param  colorToState    The color conversion function.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y, Image map, HashMap colorToState) {
        this(_automatonClass, x, y);
        setImage(map, colorToState);
    }


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  map              The map image.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y, Reader map) {
        this(_automatonClass, x, y);
        setImage(map);
    }


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  map              The map image.
     * @param  colorToState    The color conversion function.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y, byte[] map, HashMap colorToState) {
        this(_automatonClass, x, y);
        setImage(map, colorToState);
    }


    /**
     *  Constructor for the AutomatonMap object
     *
     * @param  x                The width of the map.
     * @param  y                The height of the map.
     * @param  imageFileName    The map image.
     * @param  colorToState    The color conversion function.
     * @param  _automatonClass  The implemented automaton class.
     */
    public AutomatonMap(Class _automatonClass, int x, int y, String imageFileName,
            HashMap colorToState) {
        this(_automatonClass, x, y);
        setImage(imageFileName, colorToState);
    }


    /**
     *  Sets the Image attribute of the AutomatonMap object
     *
     * @param  mapURL         The new Image value
     * @param  colorToState  The color conversion function.
     */
    public void setImage(URL mapURL, HashMap colorToState) {
        setImage((new ImageIcon(mapURL)).getImage(), colorToState);
    }


    /**
     *  Sets the Image attribute of the AutomatonMap object
     *
     * @param  map            The new Image value
     * @param  colorToState  The color conversion function.
     */
    public void setImage(Image map, HashMap colorToState) {
        initMap(map, colorToState);
    }


    /**
     *  Sets the Image attribute of the AutomatonMap object
     *
     * @param  map            The new Image value
     * @param  colorToState  The color conversion function.
     */
    public void setImage(byte[] map, HashMap colorToState) {
        setImage((new ImageIcon(map)).getImage(), colorToState);
    }


    /**
     *  Sets the Image attribute of the AutomatonMap object
     *
     * @param  imageFileName  The new Image value
     * @param  colorToState  The color conversion function.
     */
    public void setImage(String imageFileName, HashMap colorToState) {
        setImage((new ImageIcon(imageFileName)).getImage(), colorToState);
    }


    /**
     *  Sets the Image attribute of the AutomatonMap object, by reading
     *  characters from a stream. This is an alternative method for creating a
     *  map, if a real image is not available.
     *
     * @param  stream  The stream to get characters from
     */
    public void setImage(Reader stream) {
        StringBuffer buffer = new StringBuffer();
        int b;

        while (true) {
            try {
                b = stream.read();
            } catch (IOException e) {
                b = -1;
            }

            if (b < 0) {
                break;
            } else {
                buffer.append((char) b);
            }
        }

        initMap(buffer.toString());
    }


    /**
     *  Gets the InitialMap attribute of the AutomatonMap object
     *
     * @return    The InitialMap
     */
    public byte[] getInitialMap() {
        return initialMapRGB;
    }


    /**
     *  Gets an automaton at a given location.
     *
     * @param  x                             The x coordinate.
     * @param  y                             The y coordinate.
     * @return                               The Automaton
     * @exception  IllegalArgumentException  if the location does not exist.
     */
    public Automaton getAutomaton(int x, int y) throws IllegalArgumentException {
        try {
            return theMap[x][y];
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IllegalArgumentException("Invalid coordinates (" + x + ", " + y + ")");
        }
    }



    /**
     *  Gets the states of the automatons at a 3x3 region around a given
     *  location.
     *
     * @param  centerX  The x coordinate of the center automaton.
     * @param  centerY  The x coordinate of the center automaton.
     * @return          The NeighbourStates
     */
    public int[][] getNeighbourStates(int centerX, int centerY) {
        int[][] neighbourStates = new int[3][3];
        for (int x = 0; x < 3; x++) {
            for (int y = 0; y < 3; y++) {
                try {
                    neighbourStates[x][y] = theMap[centerX - 1 + x][centerY - 1 + y].getState();
                } catch (ArrayIndexOutOfBoundsException e) {
                    neighbourStates[x][y] = -1;
                }
            }
        }
        return neighbourStates;
    }


    /**
     *  Gets the XBound attribute of the AutomatonMap object
     *
     * @return    The XBound value
     */
    public int getXBound() {
        return xBound;
    }


    /**
     *  Gets the YBound attribute of the AutomatonMap object
     *
     * @return    The YBound value
     */
    public int getYBound() {
        return yBound;
    }


    /**
     *  Returns true if the given coordinates exist on the map.
     *
     * @param  x  The x coordinate to check.
     * @param  y  The x coordinate to check.
     * @return    true if the location exist on the map, false otherwise
     */
    public boolean areValidCoordinates(int x, int y) {
        if (x < 0 || x > xBound || y < 0 || y > yBound) {
            return false;
        }
        return true;
    }


    /**
     *  Gets the nearest color that exists on the map, given a certain color.
     *
     * @param  colorToState  The color conversion function.
     * @param  color         The color to check.
     * @return                The nearest color on the map.
     */
    protected Color getNearestColor(HashMap colorToState, Color color) {
        Iterator it = colorToState.keySet().iterator();
        if (!it.hasNext()) {
            return null;
        }
        Color retCol = (Color) it.next();
        int dist = calculateDistance(color, retCol);
        while (it.hasNext()) {
            Color foo = (Color) it.next();
            if (dist > calculateDistance(color, foo)) {
                retCol = foo;
                dist = calculateDistance(color, foo);
            }
        }
        return retCol;

        /*
         * int inc = 1;
         * int[] patterns = {1, 2, 4, 3, 5, 6, 7};
         * do {
         * for (int pattern = 0; pattern < 7; pattern++) {
         * int bits = patterns[pattern];
         * Integer[] colorLookup = new Integer[3];
         * for (int i = 0; i < 3; i++) {
         * if ((bits >> i) == 1) {
         * colorLookup[i] = new Integer(colors[i] + inc);
         * } else {
         * colorLookup[i] = new Integer(colors[i]);
         * }
         * }
         * if (colorToState.containsKey(colorLookup)) {
         * return colorLookup;
         * }
         * }
         * inc++;
         * } while (inc < 256);
         * return null;
         */
    }


    /**
     *  Allocates all automatons on the map.
     *
     * @param  map            The map image.
     * @param  colorToState  The color conversion function.
     */
    protected void initMap(Image map, HashMap colorToState) {
        int[] pixels = new int[xBound * yBound];

        /*
         * PixelGrabber needs an Image that has been loaded into the memory,
         * otherwise grabPixels() will fail. This happens when the map is in
         * a jar-file.
         */
        PixelGrabber grabber = new PixelGrabber(map.getScaledInstance(xBound,
                yBound,
                0),
                0, 0, xBound, yBound, pixels,
                0, xBound);

        try {
            grabber.grabPixels();
        } catch (InterruptedException e) {
            e.printStackTrace(System.out);
            for (int i = 0; i < pixels.length; i++) {
                pixels[i] = 0;
            }
        }

        int curX = 0;
        int curY = 0;
        int currentIndex = 0;

        for (int i = 0; i < pixels.length; i++) {
            Color colorLookup = new Color(pixels[i]);
            Integer state = (Integer) colorToState.get(colorLookup);

            if (state == null) {
                colorLookup = getNearestColor(colorToState, colorLookup);
                if (colorLookup == null) {
                    break;
                } else {
                    state = (Integer) colorToState.get(colorLookup);
                }
            }

            Automaton aut = getNewAutomaton(state.intValue());
            theMap[curX][curY] = aut;
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getRed();
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getGreen();
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getBlue();
            if (++curX == xBound) {
                curX = 0;
                curY++;
            }
            if (curY == yBound) {
                break;
            }
        }

        for (int y = curY; y < yBound; y++) {
            System.out.println("A portion of the map is not covered with image data, " +
			       "possibly due to an error.");
            for (int x = curX; x < xBound; x++) {
                Automaton aut = getNewAutomaton(0);
                theMap[x][y] = aut;
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getRed();
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getGreen();
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getBlue();
            }
            curX = 0;
        }
    }


    /**
     *  Allocates all automatons on the map. This is the alternative way if the
     *  map has been read from a stream.
     *
     * @param  map  The map.
     */
    protected void initMap(String map) {
        char[] chars = map.toCharArray();

        int curX = 0;
        int curY = 0;
        int currentIndex = 0;

        for (int i = 0; i < chars.length; i++) {
            int state = Character.getNumericValue(chars[i]);
            if (state < 0) {
                state = 0;
            }
            Automaton aut = getNewAutomaton(state);
            theMap[curX][curY] = aut;
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getRed();
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getGreen();
            initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getBlue();
            if (++curX == xBound) {
                curX = 0;
                curY++;
            }
            if (curY == yBound) {
                break;
            }
        }

        for (int y = curY; y < yBound; y++) {
            for (int x = curX; x < xBound; x++) {
                Automaton aut = getNewAutomaton(0);
                theMap[curX][curY] = aut;
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getRed();
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getGreen();
                initialMapRGB[currentIndex++] = (byte) aut.getCurrentColor().getBlue();
            }

            curX = 0;
        }
    }


    /**
     *  Allocates a new automaton.
     *
     * @param  initialState  The initial state of the automaton.
     * @return               The newly allocated automaton.
     */
    private Automaton getNewAutomaton(int initialState) {
        Automaton aut = null;
        try {
            aut = (Automaton) automatonClass.newInstance();
            aut.initialize(initialState);
            aut.hasChanged();
        } catch (Exception e) {
            // All exceptions have been tested for earlier (see StartServer)
        }
        return aut;
    }


    /**
     *  Calculates the distance between two colors. The method calculates the
     *  "Euclidian distance", by returning the sum of the differences in each
     *  component of the color.
     *
     * @param  c1  The first color.
     * @param  c2  The second color.
     * @return     The distance between the colors.
     */
    private int calculateDistance(Color c1, Color c2) {
        int rd = (c1.getRed() - c2.getRed()) * (c1.getRed() - c2.getRed());
        int gd = (c1.getGreen() - c2.getGreen()) * (c1.getGreen() - c2.getGreen());
        int bd = (c1.getBlue() - c2.getBlue()) * (c1.getBlue() - c2.getBlue());
        return (rd + gd + bd);
    }


}
