package ac.essex.ooechs.problems.woods;

import ac.essex.ooechs.lcs.Environment;
import ac.essex.ooechs.lcs.Payoff;
import ac.essex.ooechs.lcs.Action;
import ac.essex.ooechs.lcs.*;
import ac.essex.ooechs.lcs.representation.binary.BitStringInput;
import ac.essex.ooechs.lcs.representation.binary.BitStringCondition;
import ac.essex.ooechs.lcs.representation.Condition;

import java.util.Vector;
import java.awt.image.BufferedImage;
import java.awt.*;

/**
 * <p/>
 * Woods2 is a Gridworld that was introduced in (Wilson, 1995) to test
 * XCS in Multistep Environments. The Woods2 environment contains blank
 * field (marked by '.') where the Agent can move freely, two kinds of
 * rocks ('O' and 'Q') that mark inaccessible fields, and two types of
 * food ('F' and 'G') that indicate Reward and terminates the problem.
 * </p>
 * <p/>
 * <p/>
 * 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,
 * provided that any use properly credits the author.
 * 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 at http://www.gnu.org
 * </p>
 *
 * @author Olly Oechsle, University of Essex, Date: 14-Jan-2008
 * @version 1.0
 */
public abstract class WoodsEnvironment extends Environment {

    /**
     * The reward for finding food.
     */
    public static final double Rimm = 1000;

    // map boundaries
    public int MAP_WIDTH;
    public int MAP_HEIGHT;

    // environmental features
    public static final char BLANK = '.';
    public static final char ROCK1 = 'O';
    public static final char ROCK2 = 'Q';
    public static final char FOOD1 = 'F';
    public static final char FOOD2 = 'G';

    // directions
    public static final int N = 0;
    public static final int NE = 1;
    public static final int E = 2;
    public static final int SE = 3;
    public static final int S = 4;
    public static final int SW = 5;
    public static final int W = 6;
    public static final int NW = 7;

    // how many bits represent one object?
    int alleleSize = 3;

    // represent the map using an array
    char[][] map;

    // animat's position on the map
    int x;
    int y;

    public WoodsEnvironment() {
        MAP_WIDTH = getWidth();
        MAP_HEIGHT = getHeight();
        parseMap(getMap());
    }

    public abstract String getMap();

    public abstract String getName();

    public abstract int getWidth();

    public abstract int getHeight();

    public boolean isMultiStep() {
        return true;
    }

    /**
     * Parses the map (represented by a multiline string) and
     * prepares the environment for use.
     */
    private void parseMap(String s) {
        map = new char[getWidth()][getHeight()];
        char[] data = s.toCharArray();
        int x = 0, y = 0;
        for (int i = 0; i < data.length; i++) {
            char c = data[i];
            if (data[i] == '\n') {
                y++;
                x = 0;
                continue;
            }
            map[x][y] = c;
            x++;
        }
    }

    public void initialise() {
        while (true) {
            chooseRandomPosition();
            if (map[x][y] == BLANK) break;
        }
    }

    private void chooseRandomPosition() {
        x = (int) (Math.random() * MAP_WIDTH);
        y = (int) (Math.random() * MAP_HEIGHT);
    }

    public String encode() {
        char c = map[x][y];
        switch (c) {
            case BLANK:
                return "000";
            case ROCK1:
                return "010";
            case ROCK2:
                return "011";
            case FOOD1:
                return "110";
            case FOOD2:
                return "111";
        }
        return null;
    }


    public InputVector getInput() {

        StringBuffer s = new StringBuffer();

        goNorth();
        s.append(encode());
        goEast();
        s.append(encode());
        goSouth();
        s.append(encode());
        goSouth();
        s.append(encode());
        goWest();
        s.append(encode());
        goWest();
        s.append(encode());
        goNorth();
        s.append(encode());
        goNorth();
        s.append(encode());

        goEast();
        goSouth();

        return new BitStringInput(s.toString());

    }

    public int getFeatureCount() {
        return 3 * 8;
    }

    public Payoff takeAction(Action a) {
        switch (a.getId()) {
            case N:
                move(0, -1);
                break;
            case E:
                move(1, 0);
                break;
            case S:
                move(0, 1);
                break;
            case W:
                move(-1, 0);
                break;
            case NE:
                move(1, -1);
                break;
            case SE:
                move(1, 1);
                break;
            case SW:
                move(-1, 1);
                break;
            case NW:
                move(-1, -1);
        }
        boolean foundFood = map[x][y] == FOOD1 || map[x][y] == FOOD2;
        return new Payoff(foundFood ? Rimm : 0, foundFood);
    }

    private void move(int dx, int dy) {
        int nx = x + dx;
        int ny = y + dy;
        if (nx < 0) nx = MAP_WIDTH - 1;
        else if (nx >= MAP_WIDTH) nx = 0;
        if (ny < 0) ny = MAP_HEIGHT - 1;
        else if (ny >= MAP_HEIGHT) ny = 0;
        if (map[nx][ny] != ROCK1 && map[nx][ny] != ROCK2) {
            x = nx;
            y = ny;
        }
    }

    private void goNorth() {
        y--;
        if (y < 0) y = MAP_HEIGHT - 1;
    }

    private void goSouth() {
        y++;
        if (y >= MAP_HEIGHT) y = 0;
    }

    private void goEast() {
        x++;
        if (x >= MAP_WIDTH) x = 0;
    }

    private void goWest() {
        x--;
        if (x < 0) x = MAP_WIDTH - 1;
    }

    public Vector<Action> getActions() {
        Vector<Action> actions = new Vector<Action>();
        actions.add(new Action(N));
        actions.add(new Action(NE));
        actions.add(new Action(E));
        actions.add(new Action(SE));
        actions.add(new Action(S));
        actions.add(new Action(SW));
        actions.add(new Action(W));
        actions.add(new Action(NW));
        return actions;
    }

    public Condition getRandomCondition() {
        return new BitStringCondition(getFeatureCount(), alleleSize);
    }

    public Condition getConditionToCover(InputVector i) {
        return new BitStringCondition(i, alleleSize);
    }

    public String toString() {
        StringBuffer b = new StringBuffer();
        for (int y = 0; y < MAP_HEIGHT; y++) {
            for (int x = 0; x < MAP_WIDTH; x++) {
                char obj = map[x][y];
                if (this.x == x && this.y == y) {
                    b.append("*");
                } else {
                    b.append(obj);
                }
            }
            b.append("\n");
        }
        return b.toString();
    }

    public BufferedImage getImage() {

        int tileWidth = 15;
        int tileHeight = 15;

        Color grassColor = new Color(0, 149, 0);
        Color gridColor = new Color(23, 85, 23);
        Color food1Color = new Color(223, 34, 65);
        Color food2Color = new Color(246, 111, 12);

        long start = System.currentTimeMillis();

        BufferedImage image = new BufferedImage(MAP_WIDTH * tileWidth, MAP_HEIGHT * tileHeight, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();

        g.setColor(grassColor);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());

        for (int y = 0; y < MAP_HEIGHT; y++) {
            for (int x = 0; x < MAP_WIDTH; x++) {

                char obj = map[x][y];

                int dx = x * tileWidth;
                int dy = y * tileHeight;

                switch (obj) {
                    case ROCK1:
                        g.setColor(Color.BLACK);
                        g.fillRect(dx, dy,tileWidth, tileHeight);
                        break;
                    case ROCK2:
                        g.setColor(Color.DARK_GRAY);
                        g.fillRect(dx, dy,tileWidth, tileHeight);
                        break;
                }


                // draw grid
                g.setColor(gridColor);
                g.drawRect(dx, dy,tileWidth, tileHeight);

               if (obj == FOOD1) {
                    g.setColor(food1Color);
                    g.fillOval(dx, dy, tileWidth, tileHeight);
                }

                if (obj == FOOD2) {
                    g.setColor(food2Color);
                    g.fillOval(dx, dy, tileWidth, tileHeight);
                }

                if (this.x == x && this.y == y) {
                    g.setColor(Color.WHITE);
                    g.fillOval(dx, dy, tileWidth, tileHeight);
                }

            }
        }

        System.out.println(System.currentTimeMillis() - start);
        
        return image;
    }

}
