import StateMachineAI from "../../Wolfie2D/AI/StateMachineAI";
import GameNode, { TweenableProperties } from "../../Wolfie2D/Nodes/GameNode";
import Vec2 from "../../Wolfie2D/DataTypes/Vec2";
import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite";
import OrthogonalTilemap from "../../Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap";
import { Player_Events } from "../sword_enums";
import Fall from "./PlayerStates/Fall";
import Idle from "./PlayerStates/Idle";
import InAir from "./PlayerStates/InAir";
import Jump from "./PlayerStates/Jump";
import Walk from "./PlayerStates/Walk";
import Debug from "../../Wolfie2D/Debug/Debug";
import Item from "../GameSystems/items/Item";
import InventoryManager from "../GameSystems/InventoryManager";
import BattlerAI from "../AI/BattlerAI";
import MathUtils from "../../Wolfie2D/Utils/MathUtils";
import Weapon from "../GameSystems/items/Weapon";
import AnimatedSprite from "../../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import InputWrapper from "../Tools/InputWrapper";
import EnemyAI from "../AI/EnemyAI";
import Timer from "../../Wolfie2D/Timing/Timer";
import PlayerState from "./PlayerStates/PlayerState";
import { GameEventType } from "../../Wolfie2D/Events/GameEventType";

export enum PlayerType {
    PLATFORMER = "platformer",
    TOPDOWN = "topdown"
}

export enum PlayerStates {
    IDLE = "idle",
    WALK = "walk",
	JUMP = "jump",
    FALL = "fall",
	PREVIOUS = "previous"
}

export enum BuffType {
    FLAT_ATK  = "attack",
    PERCENT_ATK = "percent_attack",
    DEF = "defence",
    FLAT_HEALTH = "health",
    PERCENT_HEALTH = "percent_health",
    SPEED = "speed",
    RANGE = "range",
    ATKSPEED = "attackspeed",
    POISON = "poison",
    BLEED = "bleed",
    BURN = "burn",
    EXTRA_DOT = "extradot",
    SHIELD = "shield",
    SHIELD_DMG = "shielddmg",   //increase shield dmg ratio
    LIFESTEAL = "lifesteal",
    LIFESTEALBUFF = "lifestealbuff",
    EXTRALIFE= "extralife",
    ONESHOT = "oneshot",
    FULLHPBONUSDMG = "fullhpbonusdmg"
}


export class Buff  {
    "type": BuffType;
    "value": number;
    //"bonus": boolean,         //need to determine what bonus gives
    "string"? : string;
    "category" : BuffCategory
}



//TODO - need better names 
export enum BuffCategory{
    ATTACK = "ATTACK",
    DOT = "DOT",
    SHIELD = "SHIELD",
    HEALTH = "HEALTH",
    EXTRA = "EXTRA"
}

//TODO - discuss max stats during refinement, unused for now
export default class PlayerController extends StateMachineAI implements BattlerAI{
    owner: GameNode;
    velocity: Vec2 = Vec2.ZERO;
    //will need to discuss redundant stats
	speed: number = 200;
	MIN_SPEED: number = 200;
    MAX_SPEED: number = 300;
    BASE_HP: number = 100;
    MAX_HP: number = 100;
    CURRENT_HP: number = 100;
    BASE_ATK: number = 100;
    CURRENT_ATK: number = 100;
    damage_multiplier: number = .5; // percent of damage taken
    CURRENT_EXP : number = 0;
    MAX_EXP : number = 100;
    CURRENT_SHIELD : number =0;
    MAX_SHIELD : number = 20;
    invincible : boolean = false;
    static level : number = 1;

    static godMode: boolean = false;

    tilemap: OrthogonalTilemap;

    //for doublejumps maybe = # of jumps in air allowed
    MAX_airjumps: number = 1;
    airjumps:number = 0;
    
    private lookDirection: Vec2;
    /** A list of items in the game world */
    private items: Array<Item>;

    // The inventory of the player
    inventory: InventoryManager;

	static invincibilityTimer: Timer;
    
    static buffPool : Array<BuffCategory> = new Array();

    static appliedBuffs: Array<Buff> = new Array();

    //add to current_buffs later
    hasBleed : Boolean = false;
    hasPoison : Boolean = false;
    hasBurn : Boolean = false;
    hasShield : Boolean = false;
    shieldDamage : number = 1;
    hasLifesteal : Boolean = false;
    lifestealratio : number = 0; //percent of damage to steal
    hasOneShot: Boolean = false;
    extraDotDmg : number =0;
    lives: number = 2;
    cooldownMultiplier : number = 1;
    fullHpBonus: Boolean = false;

    poisonTimer : Timer;
    poisonCounter : number = 0;

    burnTimer : Timer ;
    burnCounter : number =0;

    bleedTimer : Timer;
    bleedCounter :number = 0;

    static enemiesKilled : number =0;


    //TODO - get the correct tilemap
    initializeAI(owner: GameNode, options: Record<string, any>){
        this.owner = owner;

        this.initializePlatformer();

        this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap;
      
        this.inventory  = options.inventory;

        this.lookDirection = new Vec2();
        
        //i frame timer
        PlayerController.invincibilityTimer = new Timer(2000);

        //initialize the buff pool - each has weight of 2 at first 
        PlayerController.buffPool = new Array();
        for( let i=0 ; i< 2; i++){
            PlayerController.buffPool.push(BuffCategory.ATTACK);
            PlayerController.buffPool.push(BuffCategory.DOT);
            PlayerController.buffPool.push(BuffCategory.SHIELD);
            PlayerController.buffPool.push(BuffCategory.HEALTH);
        }
        PlayerController.buffPool.push(BuffCategory.EXTRA); //only give EXTRA category a weight of 1


        //initialize dot timers
        this.burnTimer = new Timer(1000);
        this.bleedTimer = new Timer(1000);
        this.poisonTimer = new Timer(1000);

        if(PlayerController.appliedBuffs.length != 0){
            PlayerController.appliedBuffs.forEach(buff => this.addBuff(buff,true));
        }
        

        
        //to test the buffs
        //this.addBuff( {type:BuffType.HEALTH, value:1} );
        //this.addBuff({type:BuffType.BURN, value:1, category:BuffCategory.DOT});
        //this.addBuff({type:BuffType.BLEED, value:1, category:BuffCategory.DOT});
        //this.addBuff({type:BuffType.POISON, value:1, category:BuffCategory.DOT});
        
    }

    static reset(){
        PlayerController.appliedBuffs = new Array();
        PlayerController.buffPool = new Array();
        PlayerController.enemiesKilled = 0;
        PlayerController.level = 1;
        PlayerController.godMode = false;
        
    }
    initializePlatformer(): void {
        this.speed = 400;

        let idle = new Idle(this, this.owner);
		this.addState(PlayerStates.IDLE, idle);
		let walk = new Walk(this, this.owner);
		this.addState(PlayerStates.WALK, walk);
		let jump = new Jump(this, this.owner);
        this.addState(PlayerStates.JUMP, jump);
        let fall = new Fall(this, this.owner);
        this.addState(PlayerStates.FALL, fall);
        
        this.initialize(PlayerStates.IDLE);
    }

    changeState(stateName: string): void {
        // If we jump or fall, push the state so we can go back to our current state later
        // unless we're going from jump to fall or something
        if((stateName === PlayerStates.JUMP || stateName === PlayerStates.FALL) && !(this.stack.peek() instanceof InAir)){
            this.stack.push(this.stateMap.get(stateName));
        }

        super.changeState(stateName);
    }

    update(deltaT: number): void {
		super.update(deltaT);
        if(PlayerController.invincibilityTimer.isStopped()){
            this.invincible = false;
        }

		if(this.currentState instanceof Jump){
			Debug.log("playerstate", "Player State: Jump");
		} else if (this.currentState instanceof Walk){
			Debug.log("playerstate", "Player State: Walk");
		} else if (this.currentState instanceof Idle){
			Debug.log("playerstate", "Player State: Idle");
		} else if(this.currentState instanceof Fall){
            Debug.log("playerstate", "Player State: Fall");
        }
        Debug.log("player speed", "player speed: x: " + this.velocity.x + ", y:" + this.velocity.y);
        Debug.log("player Coords:", "Player Coords:" +this.owner.position );

        //testing the attacks here, may be moved to another place later
        if(InputWrapper.isAttackJustPressed()){
            let item = this.inventory.getItem();
            (<AnimatedSprite>this.owner).animation.play("ATTACK", true);
            //TODO - get proper look direction 
            this.lookDirection.x = (<Sprite>this.owner).invertX ? -1 : 1;
            // If there is an item in the current slot, use it
            if (item) {
                if((<Weapon>item).use(this.owner, "player", this.lookDirection))
                    this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "sword", loop: false, holdReference: false});
            }
        }

        //check dot effects
        if(this.burnTimer.isStopped() && this.burnCounter >0){
            this.burnCounter --;
            this.burnTimer.start();
            this.damage(5);
        }
        if(this.poisonTimer.isStopped() && this.poisonCounter >0){
            this.poisonCounter --;
            this.poisonTimer.start();
            this.damage( Math.round(this.CURRENT_HP/33) );
        }
        if(this.bleedTimer.isStopped() && this.bleedCounter >0){
            this.bleedCounter --;
            this.bleedTimer.start();
            this.damage( 2 + Math.round(this.MAX_HP/50) );
        }
	}

    
    // TODO - figure out attacker 
    damage(damage: number, attacker?: GameNode): void {
        if (PlayerController.godMode) {
            //console.log("godmode");
            return;
        }
        if( !this.invincible && PlayerState.dashTimer.isStopped()){
            //console.log("take damage");
            //i frame here
            PlayerController.invincibilityTimer.start();
            this.invincible = true;
            //shield absorbs the damage and sends dmg back to attacker
            if(this.CURRENT_SHIELD > 0){
                let newshield = Math.max(0, this.CURRENT_SHIELD - damage ); //calculate the new shield value
                if( attacker !== undefined){
                    (<EnemyAI>attacker._ai).damage((this.CURRENT_SHIELD - newshield) * this.shieldDamage); //damage the attacker the dmg taken to shield
                }
                this.CURRENT_SHIELD = newshield; //update shield value
            }
            else{
                //i frame here
                PlayerController.invincibilityTimer.start();
                this.invincible = true;
                //console.log("hurt anim");
                (<AnimatedSprite>this.owner).animation.play("HURT" );
                damage *= this.damage_multiplier;
                damage = parseFloat(damage.toPrecision(2));
                this.CURRENT_HP -= damage;
                this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "hurt", loop: false, holdReference: false});

                //if player has shield buff give them shield when damaged
                if(this.hasShield){
                    this.CURRENT_SHIELD += damage * .5;
                }
               
            }
            
        }
        else{
            //console.log("player is invincible");
        }

        if(this.CURRENT_HP <= 0){
            this.lives --;
            (<AnimatedSprite>this.owner).animation.play("DYING");
            (<AnimatedSprite>this.owner).animation.queue("DEAD", true, Player_Events.PLAYER_KILLED);
            this.emitter.fireEvent(Player_Events.PLAYER_KILLED);
        }
    }

    /**
     * gives the player a certain amount of shield
     * @param shield amount of shield to add to player
     */
    addShield(shield : number){
        this.CURRENT_SHIELD += shield;
    }

    /**
     * gives health to the player
     * @param health health to give player
     */
    addHealth(health : number){
        this.CURRENT_HP += health;
        if(this.CURRENT_HP > this.MAX_HP ){
            this.CURRENT_HP = this.MAX_HP ;
        }
    }

    /**
     * gives the player exp
     * @param exp amount of exp to give the player
     */
    giveExp(exp: number){
        this.CURRENT_EXP += exp;
        //if > than max exp level up (give buff)
        if(this.CURRENT_EXP >= this.MAX_EXP){
            this.CURRENT_EXP -= this.MAX_EXP;
            this.MAX_EXP += 50; //increase max exp needed for level up
            PlayerController.level++ ;
            this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "level_up", loop: false, holdReference: false});
            this.emitter.fireEvent(Player_Events.GIVE_REGULAR_BUFF);
        }
    }
    

    /**
     * generates an array of regular buffs
     * @param val optional value to give buff
     * @returns array of three buffs
     */
    generateRegularBuffs( val? : number) : Buff[]{

        //random number from 5 to 15 if no value given
        let num = Math.floor(Math.random() *10) +5;
        num = Math.round(num);
        if(typeof val !== 'undefined'){
            num = val;
        }

        let buffs = new Array();
        buffs.push({type:BuffType.FLAT_ATK, value:num, category: BuffCategory.EXTRA},
            {type:BuffType.SPEED, value:num * 2, category: BuffCategory.EXTRA},
            {type:BuffType.FLAT_HEALTH, value:num, category: BuffCategory.SHIELD},
            {type:BuffType.RANGE, value:num/100, category: BuffCategory.ATTACK, string: "\n\nIncrease range \nby "+num+"%"},
            {type:BuffType.ATKSPEED, value:num, category: BuffCategory.ATTACK},
        );
        
        
        //shuffle pool of buffs
        buffs.sort(() => 0.5 - Math.random());
        // Get sub-array of first 3 elements after shuffled
        let selected = buffs.slice(0, 3); //3 buff categories
        return selected;
    }

    /**
     * generates an array of special buffs
     * @param val optional value to give the buff
     * @returns array of 3 Buffs
     */
    generateSpecialBuffs( val? : number) : Buff[]{
        //shuffle pool of buff categories 
        PlayerController.buffPool.sort(() => 0.5 - Math.random());

        // Get sub-array of first 3 elements after shuffled
        //let shuffled = PlayerController.buffPool.slice(0, 3); //3 buff categories

        let shuffled = new Set<BuffCategory>();
        while(shuffled.size < 3){
            shuffled.add(PlayerController.buffPool.slice(0,1)[0]);
            PlayerController.buffPool.sort(() => 0.5 - Math.random());
        }


        //random number from 5 to 15 if no value given
        let num = Math.floor(Math.random() *10) +5;
        num = Math.round(num);
        if(typeof val !== 'undefined'){
            num = val;
        }

        //TODO - implement better buff genertion - some buffs dont want multiple of
        let attackBuffs : Buff[] = [
            {type:BuffType.PERCENT_ATK, value:num/100, category: BuffCategory.ATTACK, string:"\n\nIncrease Attack \nby "+num+"%"}
        ];

        let dotBuffs : Buff[] = [
        ];
        if(!this.hasBleed){
            dotBuffs.push({type:BuffType.BLEED, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply Bleed"});
        }
        if(!this.hasBurn){
            dotBuffs.push({type:BuffType.BURN, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply Burn"});
        }
        if(!this.hasPoison){
            dotBuffs.push({type:BuffType.POISON, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply poison"});
        }

        //only add extra dot if at least one dot is acquired
        for(let i=dotBuffs.length; i< 3 ; i++){
            dotBuffs.push({type:BuffType.EXTRA_DOT, value:num, category: BuffCategory.DOT, string: "\n\nIncrease your \nDOT damage"});
        }

        
        let shieldBuffs : Buff[] = [
            {type:BuffType.PERCENT_HEALTH, value:num/100, category: BuffCategory.SHIELD, string: "\n\nIncrease max hp \nby "+num+"%"},
        ];
        //if player doesnt have shield buff, give them the option, otherwise give buff shield option
        if(!this.hasShield){
            shieldBuffs.push({type:BuffType.SHIELD, value:1, category: BuffCategory.SHIELD, string: "\n\nGain Shield \nWhen Damaged \n Shields return \nthe damage taken \nto attacker"});
        }
        else{
            shieldBuffs.push({type:BuffType.SHIELD_DMG, value:num, category: BuffCategory.SHIELD, string: "\n\nIncrease damage \nreturned by shield"});
        }


        let healthBuffs : Buff[] = [
            {type:BuffType.DEF, value: num/100, category: BuffCategory.HEALTH, string: "\n\nDecrease damage \ntaken by "+num+"%"}
        ];
        if(!this.fullHpBonus){
            healthBuffs.push({type:BuffType.FULLHPBONUSDMG, value:1, category:BuffCategory.HEALTH, string:"\n\nDeal 10x damage \n when at full HP"})

        }
        if(!this.hasLifesteal){
            healthBuffs.push({type:BuffType.LIFESTEAL, value:1, category: BuffCategory.HEALTH, string:"\n\nGain lifesteal"});
        }
        else{
            healthBuffs.push({type:BuffType.LIFESTEALBUFF, value:num/100, category: BuffCategory.HEALTH, string:"\n\nIncrease Lifesteal \nstrength by "+ num+ "%"});
        }


        let extraBuffs : Buff[] = [
            {type:BuffType.EXTRALIFE, value:1, category: BuffCategory.EXTRA, string: "\n\nGain an \nExtra Life"},
        ];
        if(!this.hasOneShot){   //only add oneshot buff if it isnt already included 
            extraBuffs.push({type:BuffType.ONESHOT, value:1, category: BuffCategory.EXTRA, string: "\n\nYour hits hurt \n100x more but \nyour max health \nis set to 1 "});
        };


        let selected = new Array();
        for ( let cat of shuffled){
            switch(cat){
                case BuffCategory.ATTACK:
                    attackBuffs.sort(() => 0.5 - Math.random());
                    if(attackBuffs.length == 0){
                        selected.push({type:BuffType.PERCENT_ATK, value:num/100, category: BuffCategory.ATTACK, string: "\n\nIncrease Attack \nby "+num+"%"});
                    }
                    else{
                        selected.push(attackBuffs.pop());
                    }
                    break;
                case BuffCategory.DOT:
                    dotBuffs.sort(() => 0.5 - Math.random());
                    if(dotBuffs.length == 0){
                        selected.push({type:BuffType.EXTRA_DOT, value:num, category: BuffCategory.DOT, string: "\n\nIncrease your \nDOT damage"});
                    }
                    else{
                        selected.push(dotBuffs.pop());
                    }
                    break;
                case BuffCategory.EXTRA:
                    extraBuffs.sort(() => 0.5 - Math.random());
                    if(extraBuffs.length ==0 ){
                        selected.push({type:BuffType.EXTRALIFE, value:2, category: BuffCategory.EXTRA, string: "\n\nGain 2 \nExtra Lives"});
                    }
                    else{
                        selected.push(extraBuffs.pop());
                    }
                    break;
                case BuffCategory.HEALTH:
                    healthBuffs.sort(() => 0.5 - Math.random());
                    if(healthBuffs.length == 0){
                        selected.push({type:BuffType.DEF, value: num/100, category: BuffCategory.HEALTH, string: "\n\nDecrease damage\n taken by "+num+"%"});
                    }
                    else{
                        selected.push(healthBuffs.pop());
                    }
                    break;
                case BuffCategory.SHIELD:
                    shieldBuffs.sort(() => 0.5 - Math.random());
                    if(shieldBuffs.length ==0 ){
                        selected.push({type:BuffType.FLAT_HEALTH, value:num, category: BuffCategory.SHIELD});
                    }
                    else{
                        selected.push(shieldBuffs.pop());
                    }
                    break;
            }
        }

        return selected;
    }



    
    /**
	 * Add given buff to the player
	 * @param buff Given buff
     * @param init whether or not this is being used during the initialization of the player
	 */
    addBuff(buff: Buff, init? :Boolean ): void {
        
        
        //add buff to array of applied buffs if not being used to init
        if(init === undefined){
            //increase weight of selected buff category
            PlayerController.buffPool.push(buff.category);
            PlayerController.appliedBuffs.push(buff);
        }
        else if (!init){
            //increase weight of selected buff category
            if(buff.category != BuffCategory.EXTRA){
                PlayerController.buffPool.push(buff.category);
            }
            PlayerController.appliedBuffs.push(buff);
        }
        // TODO
        let item = this.inventory.getItem();
        switch(buff.type){
            case BuffType.FLAT_HEALTH:
                //this.CURRENT_BUFFS.hp += buff.value;
                this.CURRENT_HP += buff.value;
                this.MAX_HP += buff.value;
                break;
            case BuffType.PERCENT_HEALTH:
                this.CURRENT_HP *= (1+buff.value);
                this.MAX_HP *= (1+buff.value) ;
                this.CURRENT_HP = Math.round(this.CURRENT_HP);
                this.MAX_HP = Math.round(this.MAX_HP);
                break;
            case BuffType.FLAT_ATK:
                this.CURRENT_ATK +=buff.value;
                break;
            case BuffType.PERCENT_ATK:
                this.CURRENT_ATK *=(1+buff.value);
                this.CURRENT_ATK = Math.round(this.CURRENT_ATK);
                break;
            case BuffType.SPEED:
                this.speed += buff.value;
                this.MIN_SPEED += buff.value;
                break;
            case BuffType.DEF:
                this.damage_multiplier *= (1-buff.value);
                break;
            case BuffType.RANGE:
                
                if (item) {
                    (<Weapon>item).EXTRA_RANGE += buff.value;
                }
                break;
            case BuffType.BLEED:
                this.hasBleed = true;
                break;
            case BuffType.BURN:
                this.hasBurn = true;
                break;
            case BuffType.POISON:
                this.hasPoison = true;
                break;
            case BuffType.EXTRA_DOT:
                this.extraDotDmg += buff.value;
                break;
            case BuffType.SHIELD:
                this.hasShield = true;
                break;

            case BuffType.ATKSPEED:
                if (item) {
                    this.cooldownMultiplier -= buff.value;
                    //reduce cooldowntimer 
                    (<Weapon>item).cooldownTimer = new Timer((<Weapon>item).cooldown * this.cooldownMultiplier )
                }
                break;
            case BuffType.SHIELD_DMG:
                this.shieldDamage += buff.value ;
                break;
            case BuffType.EXTRALIFE:
                this.lives += buff.value;
                PlayerController.appliedBuffs.pop(); //pop- dont want to give extra life in next lvl
                break;
            case BuffType.LIFESTEAL:
                this.hasLifesteal = true;
                this.lifestealratio = .2; //20% lifesteal
                break;
            case BuffType.LIFESTEALBUFF:
                this.lifestealratio += buff.value;
                break;
            case BuffType.ONESHOT:
                this.MAX_HP = 1;
                this.CURRENT_HP = 1;
                this.CURRENT_ATK *= 100;
                break;
            case BuffType.FULLHPBONUSDMG:
                this.fullHpBonus = true;
                break;
        }
    }

    /**
     * 
     * @returns record of the player stats
     */
    getStats(): Record<string, any>{
        let stats = {} as Record<string,any>;
        stats.CURRENT_HP = this.CURRENT_HP;
        stats.CURRENT_ATK = this.CURRENT_ATK;
        stats.CURRENT_SHIELD = this.CURRENT_SHIELD;
        stats.CURRENT_EXP = this.CURRENT_EXP;

        return 
    }
        

    toString(): string{
        let s = "HP: " + this.CURRENT_HP +"/"+this.MAX_HP+
                "\nATK: " + this.CURRENT_ATK +
                "\nSpeed: " + this.speed +
                "\nDamage Ratio: " + this.damage_multiplier +
                "\nEnemies Killed: " + PlayerController.enemiesKilled + 
                "\nSword Effects:" +
                (this.hasBleed ? "\n  Bleed," : "  ") + 
                (this.hasBurn ? " Burn," : "") + 
                (this.hasPoison ? " Poison," : "") + 
                (this.hasShield ? " Shield," :"") + 
                (this.fullHpBonus ? " FullHealth deal x10 Dmg" : "") +
                (this.hasLifesteal ? " Lifesteal," : "") +
                (this.hasOneShot ? " Atk x 100, maxhp set to 1" : "") ;
        return s;
    }

}