package inf101.v18.rogue101.objects;

import inf101.v18.gfx.textmode.Printer;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.Main;
import inf101.v18.rogue101.game.Game;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.items.*;
import inf101.v18.rogue101.shared.NPC;
import inf101.v18.rogue101.states.Attack;
import javafx.scene.input.KeyCode;
import java.util.List;

public class Player implements IPlayer {
    private int hp = getMaxHealth();
    private int attack = 40;
    private int defence = 20;
    private final Backpack equipped = new Backpack();
    private boolean dropping = false; //True if the player wants to drop something using the digit keys
    private boolean picking = false; //True if the player wants to pick up something using the digit keys
    private boolean writing = false; //True if the user is writing a name
    private  boolean exploringChest = false;
    private String text = "";
    private String name = "Player";

    @Override
    public boolean keyPressed(IGame game, KeyCode key) {
        boolean turnConsumed = false;
        if (writing) {
            write(game, key);
            return false;
        }
        if (key == KeyCode.LEFT || key == KeyCode.A) {
            tryToMove(game, GridDirection.WEST);
            turnConsumed = true;
        } else if (key == KeyCode.RIGHT || key == KeyCode.D) {
            tryToMove(game, GridDirection.EAST);
            turnConsumed = true;
        } else if (key == KeyCode.UP || key == KeyCode.W) {
            tryToMove(game, GridDirection.NORTH);
            turnConsumed = true;
        } else if (key == KeyCode.DOWN || key == KeyCode.S) {
            tryToMove(game, GridDirection.SOUTH);
            turnConsumed = true;
        } else if (key == KeyCode.E) {
            turnConsumed = interactInit(game);
        } else if (key == KeyCode.Q) {
            turnConsumed = dropInit(game);
        } else if (key == KeyCode.C) {
            turnConsumed = useConsumable(game);
        } else if (key.isDigitKey()) {
            //Takes care of all digit keys.
            int keyValue = Integer.parseInt(key.getName());
            if (keyValue == 0) {
                keyValue = 10;
            }
            if (dropping) {
                drop(game, keyValue - 1);
                dropping = false;
                turnConsumed = true;
            } else if (picking) {
                pickUp(game, keyValue - 1);
                picking = false;
                turnConsumed = true;
            } else if (exploringChest) {
                loot(game, keyValue - 1);
                exploringChest = false;
                turnConsumed = true;
            }
        } else if (key == KeyCode.N) {
            game.displayMessage("Please enter your name: ");
            writing = true;
        } else if (key == KeyCode.R) {
            turnConsumed = nonMeleeAttack(game, getVision(), Attack.RANGED, "ranged");
        } else if (key == KeyCode.F) {
            turnConsumed = nonMeleeAttack(game, getVision() / 2 + getVision() % 2 == 0 ? 0 : 1, Attack.MAGIC, "magic");
        }
        showStatus(game);
        return turnConsumed;
    }

    /**
     * Attacks if the user is able to use the desired attack.
     *
     * @param game      An IGame object
     * @param range     The range of the weapon
     * @param type      The type of the attack
     * @param weapon    The required weapon type
     * @return          True if an attack was possible. False otherwise
     */
    private boolean nonMeleeAttack(IGame game, int range, Attack type, String weapon) {
        boolean turnConsumed = false;
        IItem item = null;
        switch (type) {
            case RANGED:
                item = getItem(IRangedWeapon.class);
                break;
            case MAGIC:
                item = getItem(IMagicWeapon.class);
        }
        if (item == null) {
            game.displayMessage("You do not have a " + weapon + " weapon.");
        } else {
            turnConsumed = rangedAttack(game, range, type);
            if (!turnConsumed) {
                game.displayMessage("No enemies within range.");
            }
        }
        return turnConsumed;
    }

    /**
     * Lets the user write his/her name.
     *
     * @param game  An IGame object
     * @param key   The key pressed
     */
    private void write(IGame game, KeyCode key) {
        if (key == KeyCode.BACK_SPACE) {
            if (text.length() > 0) {
                text = text.substring(0, text.length() - 1);
                game.displayMessage("Please enter your name: " + text);
            }
        } else if (key != KeyCode.ENTER) {
            if (key == KeyCode.SPACE) {
                text += " ";
            } else {
                text += key.getName();
            }
            game.displayMessage("Please enter your name: " + text);
        } else {
            name = text.toLowerCase();
            text = "";
            game.displayMessage("Name set.");
            writing = false;
        }
    }

    /**
     * Initializes the interaction with an item, and does what is needed.
     *
     * @param game  An IGame object
     * @return      True if a turn was consumed. False otherwise
     */
    private boolean interactInit(IGame game) {
        List<IItem> items = game.getLocalItems();
        if (items.size() < 1) {
            game.displayMessage("There is nothing to pick up");
        } else {
            IItem item = items.get(0);
            if (item instanceof IStatic && item instanceof IContainer) { //A Static item is always the biggest (and hopefully the only) item on a tile.
                openChest(game, items);
            } else if (item instanceof IStatic && item instanceof StairsUp) {
                ((Game)game).goUp();
                return true;
            } else if (item instanceof IStatic && item instanceof StairsDown) {
                ((Game)game).goDown();
                return true;
            } else {
                if (items.size() == 1) {
                    pickUp(game, 0);
                    return true;
                } else {
                    game.displayMessage("Items on this tile:" + niceList(items, 5));
                    picking = true;
                }
            }
        }
        return false;
    }

    /**
     * Uses the first consumable from the player's inventory
     *
     * @param game  An IGame object
     * @return      True if a potion was used. False otherwise
     */
    private boolean useConsumable(IGame game) {
        IConsumable consumable = (IConsumable) equipped.getFirst(IConsumable.class);
        if (consumable != null) {
            hp += consumable.hpIncrease();
            if (hp > getMaxHealth()) {
                hp = getMaxHealth();
            }
            attack += consumable.attackIncrease();
            defence += consumable.defenceIncrease();
            equipped.remove(consumable);
            game.displayMessage("Used " + consumable.getName());
            return true;
        } else {
            game.displayMessage("You don't have any consumables.");
            return false;
        }
    }

    /**
     * Lists the contents of a chest, an makes the player ready to loot.
     *
     * @param game  An IGame object
     * @param items A list of items on the current tile
     */
    private void openChest(IGame game, List<IItem> items) {
        IContainer container = (IContainer) items.get(0);
        items = container.getContent();
        game.displayMessage(container.getInteractMessage() + niceList(items, 10));
        exploringChest = true;
    }

    /**
     * Creates a string containing a set number of item names from a list.
     *
     * @param list  The list of items
     * @param max   The maximum number of items to print
     * @return      A string
     */
    private String niceList(List<IItem> list, int max) {
        StringBuilder msg = new StringBuilder();
        for (int i = 0; i < Math.min(list.size(), max); i++) {
            msg.append(" [").append(i + 1).append("] ").append(firstCharToUpper(list.get(i).getName()));
        }
        return msg.toString();
    }

    /**
     * Initialized the dropping of an item, and does what is needed.
     *
     * @param game  An IGame object
     * @return      True if a turn was consumed. False otherwise
     */
    private boolean dropInit(IGame game) {
        if (equipped.size() == 1) {
            drop(game, 0);
            return true;
        } else if (equipped.size() < 1) {
            game.displayMessage("You have nothing to drop");
        } else {
            game.displayMessage("Items to drop:" + niceList(equipped.getContent(), equipped.getContent().size()));
            dropping = true;
        }
        return false;
    }

    /**
     * Picks up an item at index i.
     *
     * @param game  An IGame object
     * @param i     The wanted index
     */
    private void pickUp(IGame game, int i) {
        if (equipped.hasSpace()) {
            List<IItem> items = game.getLocalItems();
            if (items.size() >= i) {
                IItem pickedUp = game.pickUp(items.get(i));
                if (pickedUp != null) {
                    equipped.add(pickedUp);
                    game.displayMessage("Picked up " + pickedUp.getName());
                } else {
                    game.displayMessage("The item could not be picked up.");
                }
            } else {
                game.displayMessage("That item does not exist.");
            }
        } else {
            game.displayMessage("Your inventory is full.");
        }
    }

    /**
     * Takes an item from a chest, and gives it to the player.
     */
    private void loot(IGame game, int i) {
        if (equipped.hasSpace()) {
            IContainer container = getStaticContainer(game);
            if (container != null && i < container.getContent().size()) {
                IItem loot = container.getContent().get(i);
                equipped.add(loot);
                container.remove(i);
                game.displayMessage("Looted " + loot.getName());
            }
        } else {
            game.displayMessage("Your inventory is full.");
        }
    }

    /**
     * Gets any static containers on the current tile.
     *
     * @param game  An IGame object
     * @return      A static container
     */
    private IContainer getStaticContainer(IGame game) {
        List<IItem> items = game.getLocalItems();
        if (items.size() < 1) {
            return null;
        }
        IItem item = items.get(0);
        if (item instanceof IStatic && item instanceof IContainer) {
            return (IContainer) item;
        } else {
            return null;
        }
    }

    /**
     * Drops an item at index i.
     *
     * @param game  An IGame object
     * @param i     The wanted index
     */
    private void drop(IGame game, int i) {
        if (!equipped.isEmpty() && equipped.size() > i) {
            IContainer container = getStaticContainer(game);
            if (container != null) {
                if (container.addItem(equipped.get(i))) {
                    game.displayMessage(equipped.get(i).getName() + " stored in " + container.getName());
                    equipped.remove(i);
                    return;
                } else {
                    game.displayMessage(container.getName() + " is full.");
                    return;
                }
            }
            if (game.drop(equipped.get(i))) {
                equipped.remove(i);
                game.displayMessage("Item dropped.");
            } else {
                game.displayMessage("The ground rejected the item.");
            }
        } else {
            game.displayMessage("You have nothing to drop.");
        }
    }

    /**
     * Updates the status bar with the Player's current stats.
     *
     * @param game  An IGame object
     */
    private void showStatus(IGame game) {
        game.formatStatus("HP: %s ATK: %d RATK: %d MATK: %d DEF: %s DMG: %s RDMG: %s MDMG: %s",
                NPC.hpBar(this),
                getAttack(Attack.MELEE),
                getAttack(Attack.RANGED),
                getAttack(Attack.MAGIC),
                getDefence(),
                getDamage(Attack.MELEE),
                getDamage(Attack.RANGED),
                getDamage(Attack.MAGIC)
        );
        printInventory(game);
    }

    /**
     * Prints all items in the player's inventory.
     *
     * @param game An IGame object
     */
    private void printInventory(IGame game) {
        Printer printer = game.getPrinter();
        List<IItem> items = equipped.getContent();
        printer.clearRegion(Main.COLUMN_RIGHTSIDE_START, 13, 45, 5);
        printer.printAt(Main.COLUMN_RIGHTSIDE_START, 12, "Inventory:");
        for (int i = 0; i < items.size(); i++) {
            printer.printAt(Main.COLUMN_RIGHTSIDE_START, 13 + i, items.get(i).getName());
        }
    }

    /**
     * Changes the first character in a string to uppercase.
     *
     * @param input The input string
     * @return      The input string with the first character uppercased
     */
    private String firstCharToUpper(String input) {
        if (input.length() < 1) {
            return  input;
        } else {
            return Character.toUpperCase(input.charAt(0)) + input.substring(1);
        }
    }

    /**
     * Lets the user move or attack if possible.
     *
     * @param game  An IGame object
     * @param dir   The direction the player wants to go
     */
    private void tryToMove(IGame game, GridDirection dir) {
        ILocation loc = game.getLocation();
        if (game.canGo(dir)) {
            game.move(dir);
        } else if (loc.canGo(dir) && game.getMap().hasActors(loc.go(dir))) {
            game.attack(dir, game.getMap().getActors(loc.go(dir)).get(0));
        } else {
           game.displayMessage("Umm, it is not possible to move in that direction.");
        }
    }


    @Override
    public int getAttack() {
        IBuffItem buff = (IBuffItem)getItem(IBuffItem.class);
        if (buff != null) {
            return attack + buff.getBuffDamage();
        } else {
            return attack;
        }
    }

    /**
     * Gets the attack with added weapon attack
     *
     * @param type  The attack type corresponding to the weapon type.
     * @return      Attack as int
     */
    private int getAttack(Attack type) {
        IWeapon weapon = NPC.getWeapon(type, this);
        if (weapon != null) {
            return getAttack() + weapon.getWeaponAttack();
        } else {
            return getAttack();
        }
    }

    @Override
    public int getDamage() {
        return 10;
    }

    /**
     * Gets the damage with added weapon damage
     *
     * @param type  The attack type corresponding to the weapon type.
     * @return      Damage as int
     */
    private int getDamage(Attack type) {
        IWeapon weapon = NPC.getWeapon(type, this);
        if (weapon != null) {
            return getDamage() + weapon.getWeaponDamage();
        } else {
            return getDamage();
        }
    }

    @Override
    public IItem getItem(Class<?> type) {
        for (IItem item : equipped.getContent()) {
            if (type.isInstance(item)) {
                return item;
            }
        }
        return null;
    }

    @Override
    public int getCurrentHealth() {
        return hp;
    }

    @Override
    public int getDefence() {
        IBuffItem buff = (IBuffItem)getItem(IBuffItem.class);
        if (buff != null) {
            return defence + buff.getBuffDamage();
        } else {
            return defence;
        }
    }

    @Override
    public int getMaxHealth() {
        return 1000;
    }

    @Override
    public String getName() {
        return firstCharToUpper(name);
    }

    @Override
    public int getSize() {
        return 10;
    }

    @Override
    public String getPrintSymbol() {
        return  "\u001b[96m" + "@" + "\u001b[0m";
    }

    @Override
    public String getSymbol() {
        return "@";
    }

    @Override
    public int handleDamage(IGame game, IItem source, int amount) {
        hp -= amount;
        showStatus(game);
        if (hp < 1) {
            game.displayStatus("Game Over");
        }
        return amount;
    }

    @Override
    public int getVision() {
        IBuffItem item = (IBuffItem) getItem(IBuffItem.class);
        if (item != null) {
            return 3 + item.getBuffVisibility();
        } else {
            return 3;
        }
    }

    /**
     * Performs a ranged attack on the nearest visible enemy (if any);
     *
     * @param game      An IGame object
     * @param range     The range of the attack
     * @param type      The attack type
     * @return          True if the player attacked. False otherwise
     */
    private boolean rangedAttack(IGame game, int range, Attack type) {
        List<ILocation> neighbours = game.getVisible();
        for (ILocation neighbour : neighbours) {
            if (game.getMap().hasActors(neighbour)) {
                ILocation current = game.getLocation();
                List<GridDirection> dirs = game.locationDirection(current, neighbour);
                IActor actor = game.getMap().getActors(neighbour).get(0); //We assume there is only one actor.
                if (actor instanceof INonPlayer && current.gridDistanceTo(neighbour) <= range) {
                    GridDirection dir = dirs.get(0); //Only ever has one item
                    game.rangedAttack(dir, actor, type);
                    return true;
                }
            }
        }
        return false;
    }
}
