package ch.epfl.maze.physical;

import ch.epfl.maze.util.Direction;
import ch.epfl.maze.util.Vector2D;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * World that is represented by a labyrinth of tiles in which an {@code Animal}
 * can move.
 *
 * @author EPFL
 * @author Pacien TRAN-GIRARD
 */
public abstract class World {

    /* tiles constants */
    public static final int FREE = 0;
    public static final int WALL = 1;
    public static final int START = 2;
    public static final int EXIT = 3;
    public static final int NOTHING = -1;

    /**
     * Structure of the labyrinth, an NxM array of tiles
     */
    private final int[][] labyrinth;

    private final Vector2D start;
    private final Vector2D exit;

    /**
     * Constructs a new world with a labyrinth. The labyrinth must be rectangle.
     *
     * @param labyrinth Structure of the labyrinth, an NxM array of tiles
     */
    public World(int[][] labyrinth) {
        this.labyrinth = labyrinth;

        this.start = this.findFirstTileOfType(World.START);
        this.exit = this.findFirstTileOfType(World.EXIT);
    }

    /**
     * Finds the coordinates of the first occurrence of the given tile type.
     *
     * @param tileType Type of the tile
     * @return A Vector2D of the first occurrence of the given tile type
     */
    private Vector2D findFirstTileOfType(int tileType) {
        for (int x = 0; x < this.getWidth(); ++x)
            for (int y = 0; y < this.getHeight(); ++y)
                if (this.getTile(x, y) == tileType)
                    return new Vector2D(x, y);

        return null;
    }

    /**
     * Determines whether the labyrinth has been solved by every animal.
     *
     * @return <b>true</b> if no more moves can be made, <b>false</b> otherwise
     */
    abstract public boolean isSolved();

    /**
     * Resets the world as when it was instantiated.
     */
    abstract public void reset();

    /**
     * Returns a copy of the set of all current animals in the world.
     *
     * @return A set of all animals in the world
     */
    public Set<Animal> getAnimalSet() {
        return null;
    }

    /**
     * Returns a copy of the list of all current animals in the world.
     *
     * @return A list of all animals in the world
     * @implNote Not abstract for compatibility purpose (in order not to break tests)
     * @deprecated Use getAnimalSet() instead
     */
    public List<Animal> getAnimals() {
        return new ArrayList<>(this.getAnimalSet());
    }

    /**
     * Checks in a safe way the tile number at position (x, y) in the labyrinth.
     *
     * @param x Horizontal coordinate
     * @param y Vertical coordinate
     * @return The tile number at position (x, y), or the NOTHING tile if x or y is
     * incorrect.
     */
    public final int getTile(int x, int y) {
        if (x < 0 || x >= this.getWidth()) return World.NOTHING;
        if (y < 0 || y >= this.getHeight()) return World.NOTHING;
        return this.labyrinth[y][x];
    }

    /**
     * Determines if coordinates are free to walk on.
     *
     * @param x Horizontal coordinate
     * @param y Vertical coordinate
     * @return <b>true</b> if an animal can walk on tile, <b>false</b> otherwise
     */
    public final boolean isFree(int x, int y) {
        int tile = this.getTile(x, y);
        return !(tile == World.WALL || tile == World.NOTHING);
    }

    /**
     * Determines if coordinates are free to walk on.
     *
     * @param position The position vector
     * @return <b>true</b> if an animal can walk on tile, <b>false</b> otherwise
     */
    public final boolean isFree(Vector2D position) {
        return this.isFree(position.getX(), position.getY());
    }

    /**
     * Computes and returns the available choices for a position in the
     * labyrinth. The result will be typically used by {@code Animal} in
     * {@link ch.epfl.maze.physical.Animal#move(Direction[]) move(Direction[])}
     *
     * @param position A position in the maze
     * @return An array of all available choices at a position
     */
    public final Direction[] getChoices(Vector2D position) {
        List<Direction> choices = new ArrayList<>();
        for (Direction dir : Direction.POSSIBLE_DIRECTIONS)
            if (this.isFree(position.addDirectionTo(dir)))
                choices.add(dir);

        return choices.isEmpty() ? new Direction[]{Direction.NONE} : choices.toArray(new Direction[choices.size()]);
    }

    /**
     * Returns horizontal length of labyrinth.
     *
     * @return The horizontal length of the labyrinth
     */
    public final int getWidth() {
        return this.labyrinth[0].length;
    }

    /**
     * Returns vertical length of labyrinth.
     *
     * @return The vertical length of the labyrinth
     */
    public final int getHeight() {
        return this.labyrinth.length;
    }

    /**
     * Returns the entrance of the labyrinth at which animals should begin when
     * added.
     *
     * @return Start position of the labyrinth, null if none.
     */
    public final Vector2D getStart() {
        return this.start;
    }

    /**
     * Returns the exit of the labyrinth at which animals should be removed.
     *
     * @return Exit position of the labyrinth, null if none.
     */
    public final Vector2D getExit() {
        return this.exit;
    }

}