package engine.core;

public class MarioLevelModel {
    //start and end of the level
    public static final char MARIO_START = 'M';
    public static final char MARIO_EXIT = 'F';
    public static final char EMPTY = '-';

    //game tiles symbols
    public static final char GROUND = 'X';
    public static final char PYRAMID_BLOCK = '#';
    public static final char NORMAL_BRICK = 'S';
    public static final char COIN_BRICK = 'C';
    public static final char LIFE_BRICK = 'L';
    public static final char SPECIAL_BRICK = 'U';
    public static final char SPECIAL_QUESTION_BLOCK = '@';
    public static final char COIN_QUESTION_BLOCK = '!';
    public static final char COIN_HIDDEN_BLOCK = '2';
    public static final char LIFE_HIDDEN_BLOCK = '1';
    public static final char USED_BLOCK = 'D';
    public static final char COIN = 'o';
    public static final char PIPE = 't';
    public static final char PIPE_FLOWER = 'T';
    public static final char BULLET_BILL = '*';
    public static final char PLATFORM_BACKGROUND = '|';
    public static final char PLATFORM = '%';

    //enemies that can be in the level
    public static final char GOOMBA = 'g';
    public static final char GOOMBA_WINGED = 'G';
    public static final char RED_KOOPA = 'r';
    public static final char RED_KOOPA_WINGED = 'R';
    public static final char GREEN_KOOPA = 'k';
    public static final char GREEN_KOOPA_WINGED = 'K';
    public static final char SPIKY = 'y';
    public static final char SPIKY_WINGED = 'Y';

    /**
     * Get array of level tiles that can spawn enemies
     *
     * @return tiles that spawn enemies
     */
    public static char[] getEnemyTiles() {
        return new char[]{BULLET_BILL, PIPE_FLOWER};
    }

    /**
     * list of tiles that can be bumped by the player
     *
     * @return list of tiles that can be bumped by the player
     */
    public static char[] getBumpableTiles() {
        return new char[]{NORMAL_BRICK, COIN_BRICK, LIFE_BRICK, SPECIAL_BRICK,
                SPECIAL_QUESTION_BLOCK, COIN_QUESTION_BLOCK};
    }

    /**
     * list all the tiles that can block the player movement
     *
     * @return array of all tiles that block player movement
     */
    public static char[] getBlockTiles() {
        return new char[]{GROUND, PYRAMID_BLOCK, USED_BLOCK,
                NORMAL_BRICK, COIN_BRICK, LIFE_BRICK, SPECIAL_BRICK,
                SPECIAL_QUESTION_BLOCK, COIN_QUESTION_BLOCK,
                PIPE, PIPE_FLOWER, BULLET_BILL};
    }

    /**
     * tiles that block the player and not interactive
     *
     * @return list of all solid tiles that don't interact
     */
    public static char[] getBlockNonSpecialTiles() {
        return new char[]{GROUND, PYRAMID_BLOCK, USED_BLOCK, PIPE};
    }

    /**
     * list of all tiles that won't block the player movement
     *
     * @return list of all non blocking tiles
     */
    public static char[] getNonBlockingTiles() {
        return new char[]{COIN, COIN_HIDDEN_BLOCK, LIFE_HIDDEN_BLOCK, PLATFORM_BACKGROUND};
    }

    /**
     * Get a list of all scene tiles that could produce something collected by the player
     *
     * @return list of all collectible scene tiles
     */
    public static char[] getCollectablesTiles() {
        return new char[]{COIN,
                COIN_BRICK, LIFE_BRICK, SPECIAL_BRICK,
                SPECIAL_QUESTION_BLOCK, COIN_QUESTION_BLOCK,
                COIN_HIDDEN_BLOCK, LIFE_HIDDEN_BLOCK};
    }

    /**
     * Get the correct version of the enemy char
     *
     * @param enemy  the enemy character
     * @param winged boolean to indicate if its a winged enemy
     * @return correct character based on winged
     */
    public static char getWingedEnemyVersion(char enemy, boolean winged) {
        if (!winged) {
            if (enemy == GOOMBA_WINGED) {
                return GOOMBA;
            }
            if (enemy == GREEN_KOOPA_WINGED) {
                return GREEN_KOOPA;
            }
            if (enemy == RED_KOOPA_WINGED) {
                return RED_KOOPA;
            }
            if (enemy == SPIKY_WINGED) {
                return SPIKY;
            }
            return enemy;
        }
        if (enemy == GOOMBA) {
            return GOOMBA_WINGED;
        }
        if (enemy == GREEN_KOOPA) {
            return GREEN_KOOPA_WINGED;
        }
        if (enemy == RED_KOOPA) {
            return RED_KOOPA_WINGED;
        }
        if (enemy == SPIKY) {
            return SPIKY_WINGED;
        }
        return enemy;
    }

    /**
     * a list of all enemy characters
     *
     * @return array of all enemy characters
     */
    public static char[] getEnemyCharacters() {
        return new char[]{GOOMBA, GOOMBA_WINGED, RED_KOOPA, RED_KOOPA_WINGED,
                GREEN_KOOPA, GREEN_KOOPA_WINGED, SPIKY, SPIKY_WINGED};
    }

    /**
     * list of all enemy character based on wings
     *
     * @param wings true if the list contain winged enemies and false otherwise
     * @return an array of all wings enemy or not winged
     */
    public static char[] getEnemyCharacters(boolean wings) {
        if (wings) {
            return new char[]{GOOMBA_WINGED, RED_KOOPA_WINGED, GREEN_KOOPA_WINGED, SPIKY_WINGED};
        }
        return new char[]{GOOMBA, RED_KOOPA, GREEN_KOOPA, SPIKY};
    }

    /**
     * map object for helping
     */
    private char[][] map;

    /**
     * create the Level Model
     *
     * @param levelWidth  the width of the level
     * @param levelHeight the height of the level
     */
    public MarioLevelModel(int levelWidth, int levelHeight) {
        this.map = new char[levelWidth][levelHeight];
    }

    /**
     * create a similar clone to the current map
     */
    public MarioLevelModel clone() {
        MarioLevelModel model = new MarioLevelModel(this.getWidth(), this.getHeight());
        for (int x = 0; x < model.getWidth(); x++) {
            for (int y = 0; y < model.getHeight(); y++) {
                model.map[x][y] = this.map[x][y];
            }
        }
        return model;
    }

    /**
     * get map width
     *
     * @return map width
     */
    public int getWidth() {
        return this.map.length;
    }

    /**
     * get map height
     *
     * @return map height
     */
    public int getHeight() {
        return this.map[0].length;
    }

    /**
     * get the value of the tile in certain location
     *
     * @param x x tile position
     * @param y y tile position
     * @return the tile value
     */
    public char getBlock(int x, int y) {
        int currentX = x;
        int currentY = y;
        if (x < 0) currentX = 0;
        if (y < 0) currentY = 0;
        if (x > this.map.length - 1) currentX = this.map.length - 1;
        if (y > this.map[0].length - 1) currentY = this.map[0].length - 1;
        return this.map[currentX][currentY];
    }

    /**
     * set a tile on the map with certain value
     *
     * @param x     the x tile position
     * @param y     the y tile position
     * @param value the tile value to be set
     */
    public void setBlock(int x, int y, char value) {
        if (x < 0 || y < 0 || x > this.map.length - 1 || y > this.map[0].length - 1) return;
        this.map[x][y] = value;
    }

    /**
     * set a rectangle area of the map with a certain tile value
     *
     * @param startX the x tile position of the left upper corner
     * @param startY the y tile position of the left upper corner
     * @param width  the width of the rectangle
     * @param height the height of the rectangle
     * @param value  the tile value
     */
    public void setRectangle(int startX, int startY, int width, int height, char value) {
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                this.setBlock(startX + x, startY + y, value);
            }
        }
    }

    /**
     * Copy the string level to the current map
     *
     * @param level the input string level
     */
    public void copyFromString(String level) {
        this.copyFromString(0, 0, 0, 0, this.getWidth(), this.getHeight(), level);
    }

    /**
     * Copy portion from string to the current map
     *
     * @param targetX the x of the target location
     * @param targetY the y of the target location
     * @param sourceX the x from the level string
     * @param sourceY the y from the level string
     * @param width   the width of the copied portion
     * @param height  the height of the copied protion
     * @param level   the level string
     */
    public void copyFromString(int targetX, int targetY, int sourceX, int sourceY, int width, int height, String level) {
        String[] lines = level.split("\n");
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int maxWidth = lines[0].length();
                int maxHeight = lines.length;
                this.setBlock(x + targetX, y + targetY, lines[Math.min(y + sourceY, maxHeight - 1)].charAt(Math.min(x + sourceX, maxWidth - 1)));
            }
        }
    }

    /**
     * clear the whole map
     */
    public void clearMap() {
        this.setRectangle(0, 0, this.getWidth(), this.getHeight(), EMPTY);
    }

    /**
     * get the string value of the map
     *
     * @return the map in form of string
     */
    public String getMap() {
        String result = "";
        for (int y = 0; y < map[0].length; y++) {
            for (int x = 0; x < map.length; x++) {
                result += map[x][y];
            }
            result += "\n";
        }
        return result;
    }

    /**
     * test the current level using a specific agent
     *
     * @param agent agent to test the level
     * @param timer amount of time allowed to test that level
     * @return statistical results about the level
     */
    public MarioResult testALevelWithAgent(MarioAgent agent, int timer) {
        MarioGame game = new MarioGame();
        return game.runGame(agent, this.getMap(), timer);
    }
}
