package inf101.v18.rogue101.shared;

import inf101.v18.gfx.textmode.BlocksAndBoxes;
import inf101.v18.grid.GridDirection;
import inf101.v18.grid.ILocation;
import inf101.v18.rogue101.game.IGame;
import inf101.v18.rogue101.items.*;
import inf101.v18.rogue101.map.IMapView;
import inf101.v18.rogue101.objects.IActor;
import inf101.v18.rogue101.objects.IItem;
import inf101.v18.rogue101.objects.IPlayer;
import inf101.v18.rogue101.states.Attack;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.List;

/**
 * A holder class for methods used by IActors.
 */
public class NPC {

    private NPC() {}

    /**
     * Performs a ranged attack on the closest actor (if any) observable by the current actor if range is > 1.
     * Performs a melee attack on an actor (if any) on a neighbouring tile if range == 1.
     * Moves closer to an enemy if an attack is not possible.
     *
     * @param game      An IGame object
     * @param range     The range of the equipped weapon
     * @return          True if an attack or a move towards an enemy was successful. False otherwise
     */
    public static boolean tryAttack(IGame game, int range, Attack type) {
        List<ILocation> neighbours = game.getVisible();
        for (ILocation neighbour : neighbours) {
            IMapView map = game.getMap();
            if (map.hasActors(neighbour) && map.getActors(neighbour).get(0) instanceof IPlayer) {
                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.
                for (GridDirection dir : dirs) {
                    if (type == Attack.MELEE
                            && current.gridDistanceTo(neighbour) <= 1
                            && current.canGo(dir)
                            && game.getMap().has(current.go(dir), actor)
                            ) {
                        game.attack(dir, actor);
                        return true;
                    }
                    if (range > 1 && current.gridDistanceTo(neighbour) <= range) {
                        game.rangedAttack(dir, actor, type);
                        return true;
                    }
                }
                for (GridDirection dir : dirs) {
                    if (game.canGo(dir)) {
                        game.move(dir);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Tries to find and reach an item of a specified class.
     *
     * @param game          An IGame object
     * @param validItems    A list of classes the NPC should look for
     * @return              True if an appropriate item was found in the range of the current actor. False otherwise.
     */
    public static boolean trackItem(IGame game, List<Class<?>> validItems) {
        List<ILocation> neighbours = game.getVisible();
        for (ILocation neighbour : neighbours) {
            for (IItem item : game.getMap().getAll(neighbour)) {
                for (Class<?> validItem : validItems) {
                    if (validItem.isInstance(item)) {
                        ILocation current = game.getLocation();
                        List<GridDirection> dirs = game.locationDirection(current, neighbour);
                        for (GridDirection dir : dirs) {
                            if (game.canGo(dir)) {
                                game.move(dir);
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * Picks up an item of one of the specified class types.
     *
     * @param game          An IGame object
     * @param validItems    A list of classes the NPC should accept
     * @return              An item if it was successfully picked up. Null otherwise
     */
    public static IItem pickUp(IGame game, List<Class<?>> validItems) {
        for (IItem item : game.getLocalItems()) {
            for (Class<?> validItem : validItems) {
                if (validItem.isInstance(item)) {
                    return game.pickUp(item);
                }
            }
        }
        return null;
    }

    /**
     * Makes an IActor go to the opposite direction of the closest enemy.
     * Will return false if there are no visible enemies.
     *
     * @param game  An IGame object
     * @return      True if the IActor fled. False otherwise
     */
    public static boolean flee(IGame game) {
        List<ILocation> neighbours = game.getVisible();
        for (ILocation neighbour : neighbours) {
            if (game.getMap().hasActors(neighbour)) {
                ILocation current = game.getLocation();
                List<GridDirection> dirs = reverseDir(game.locationDirection(current, neighbour));
                for (GridDirection dir : dirs) {
                    if (game.canGo(dir)) {
                        game.move(dir);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Reverses a list of directions.
     *
     * @param dirs  A list directions
     * @return      A list of the opposite directions
     */
    private static List<GridDirection> reverseDir(List<GridDirection> dirs) {
        for (int i = 0; i < dirs.size(); i++) {
            GridDirection dir = dirs.get(i);
            switch (dir) {
                case SOUTH:
                    dirs.set(i, GridDirection.NORTH);
                    break;
                case NORTH:
                    dirs.set(i, GridDirection.SOUTH);
                    break;
                case WEST:
                    dirs.set(i, GridDirection.EAST);
                    break;
                case EAST:
                    dirs.set(i, GridDirection.WEST);
            }
        }
        return dirs;
    }

    /**
     * Plays a sound.
     *
     * @param filename  The String path of the audio file to play
     */
    public static void playSound(String filename) {
        URL url = NPC.class.getClassLoader().getResource(filename);
        if (url != null) {
            Media sound = new Media(url.toString());
            MediaPlayer mediaPlayer = new MediaPlayer(sound);
            mediaPlayer.play();
        } else {
            System.out.println("Could not play audio file " + filename);
        }
    }

    /**
     * Generates a hp bar based on item's current hp
     *
     * @return  HP bar as string
     */
    public static String hpBar(IItem item) {
        StringBuilder str = new StringBuilder(BlocksAndBoxes.BLOCK_HALF);
        int hp = item.getCurrentHealth();
        int hpLen = (int)((float)hp/item.getMaxHealth() * 10.0);
        if (hp > 0 && hp < 100) {
            str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_BOTTOM + "\u001b[0m");
            hpLen++;
        } else {
            for (int i = 0; i < hpLen; i++) {
                str.append("\u001b[91m" + BlocksAndBoxes.BLOCK_HALF + "\u001b[0m");
            }
        }
        for (int i = 0; i < 10 - hpLen; i++) {
            str.append(" ");
        }
        str.append(BlocksAndBoxes.BLOCK_HALF);
        return str.toString();
    }

    /**
     * Gets the weapon appropriate for the used attack.
     *
     * @param type	The attack type
     * @return		An IWeapon or null
     */
    public static IWeapon getWeapon(Attack type, IActor actor) {
        IWeapon weapon = null;
        switch (type) {
            case MELEE:
                weapon = (IWeapon)actor.getItem(IMeleeWeapon.class);
                break;
            case RANGED:
                weapon = (IWeapon)actor.getItem(IRangedWeapon.class);
                break;
            case MAGIC:
                weapon = (IWeapon)actor.getItem(IMagicWeapon.class);
        }
        return weapon;
    }
}
