/*
 * Pixel Dungeon
 * Copyright (C) 2012-2014  Oleg Dolya
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
package com.shatteredpixel.shatteredpixeldungeon.actors.mobs;

import java.util.HashSet;

import com.shatteredpixel.shatteredpixeldungeon.Badges;
import com.shatteredpixel.shatteredpixeldungeon.Challenges;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.Statistics;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Sleep;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Terror;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
import com.shatteredpixel.shatteredpixeldungeon.effects.Wound;
import com.shatteredpixel.shatteredpixeldungeon.items.Generator;
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfAccuracy;
import com.shatteredpixel.shatteredpixeldungeon.items.rings.RingOfWealth;
import com.shatteredpixel.shatteredpixeldungeon.levels.Level;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.utils.Bundle;
import com.watabou.utils.Random;

public abstract class Mob extends Char {
	
	private static final String	TXT_DIED	= "You hear something died in the distance";
	
	protected static final String TXT_NOTICE1	= "?!";
	protected static final String TXT_RAGE		= "#$%^";
	protected static final String TXT_EXP		= "%+dEXP";

    public AiState SLEEPING     = new Sleeping();
    public AiState HUNTING		= new Hunting();
    public AiState WANDERING	= new Wandering();
    public AiState FLEEING		= new Fleeing();
    public AiState PASSIVE		= new Passive();
    public AiState state = SLEEPING;
	
	public Class<? extends CharSprite> spriteClass;
	
	protected int target = -1;
	
	protected int defenseSkill = 0;
	
	protected int EXP = 1;
	protected int maxLvl = 30;
	
	protected Char enemy;
	protected boolean enemySeen;
	protected boolean alerted = false;

	protected static final float TIME_TO_WAKE_UP = 1f;
	
	public boolean hostile = true;
	
	// Unreachable target
	public static final Mob DUMMY = new Mob() {
		{
			pos = -1;
		}
	};
	
	private static final String STATE	= "state";
    private static final String SEEN	= "seen";
	private static final String TARGET	= "target";
	
	@Override
	public void storeInBundle( Bundle bundle ) {
		
		super.storeInBundle( bundle );

        if (state == SLEEPING) {
            bundle.put( STATE, Sleeping.TAG );
        } else if (state == WANDERING) {
            bundle.put( STATE, Wandering.TAG );
        } else if (state == HUNTING) {
            bundle.put( STATE, Hunting.TAG );
        } else if (state == FLEEING) {
            bundle.put( STATE, Fleeing.TAG );
        } else if (state == PASSIVE) {
            bundle.put( STATE, Passive.TAG );
        }
        bundle.put( TARGET, target );
	}
	
	@Override
	public void restoreFromBundle( Bundle bundle ) {
		
		super.restoreFromBundle( bundle );

        String state = bundle.getString( STATE );
        if (state.equals( Sleeping.TAG )) {
            this.state = SLEEPING;
        } else if (state.equals( Wandering.TAG )) {
            this.state = WANDERING;
        } else if (state.equals( Hunting.TAG )) {
            this.state = HUNTING;
        } else if (state.equals( Fleeing.TAG )) {
            this.state = FLEEING;
        } else if (state.equals( Passive.TAG )) {
            this.state = PASSIVE;
        }

        target = bundle.getInt( TARGET );
    }
	
	public CharSprite sprite() {
		CharSprite sprite = null;
		try {
			sprite = spriteClass.newInstance();
		} catch (Exception e) {
		}
		return sprite;
	}
	
	@Override
	protected boolean act() {
		
		super.act();
		
		boolean justAlerted = alerted;
		alerted = false;
		
		sprite.hideAlert();
		
		if (paralysed) {
			enemySeen = false;
			spend( TICK );
			return true;
		}
		
		enemy = chooseEnemy();
		
		boolean enemyInFOV = enemy.isAlive() && Level.fieldOfView[enemy.pos] && enemy.invisible <= 0;

        return state.act( enemyInFOV, justAlerted );
	}
	
	protected Char chooseEnemy() {
		
		if (buff( Amok.class ) != null) {
			if (enemy == Dungeon.hero || enemy == null) {
				
				HashSet<Mob> enemies = new HashSet<Mob>();
				for (Mob mob:Dungeon.level.mobs) {
					if (mob != this && Level.fieldOfView[mob.pos]) {
						enemies.add( mob );
					}
				}
				if (enemies.size() > 0) {
					return Random.element( enemies );
				}
				
			} else {
				return enemy;
			}
		}
		
		Terror terror = (Terror)buff( Terror.class );
		if (terror != null) {
			return terror.source;
		}
		
		return Dungeon.hero;
	}

	protected boolean moveSprite( int from, int to ) {

		if (sprite.isVisible() && (Dungeon.visible[from] || Dungeon.visible[to])) {
			sprite.move( from, to );
			return true;
		} else {
			sprite.place( to );
			return true;
		}
	}
	
	@Override
	public void add( Buff buff ) {
		super.add( buff );
		if (buff instanceof Amok) {
			if (sprite != null) {
				sprite.showStatus( CharSprite.NEGATIVE, TXT_RAGE );
			}
			state = HUNTING;
		} else if (buff instanceof Terror) {
			state = FLEEING;
		} else if (buff instanceof Sleep) {
			state = SLEEPING;
            this.sprite().showSleep();
			postpone( Sleep.SWS );
		}
	}
	
	@Override
	public void remove( Buff buff ) {
		super.remove( buff );
		if (buff instanceof Terror) {
			sprite.showStatus( CharSprite.NEGATIVE, TXT_RAGE );
			state = HUNTING;
		}
	}
	
	protected boolean canAttack( Char enemy ) {
		return Level.adjacent( pos, enemy.pos ) && !pacified;
	}
	
	protected boolean getCloser( int target ) {
		
		if (rooted) {
			return false;
		}
		
		int step = Dungeon.findPath( this, pos, target, 
			Level.passable, 
			Level.fieldOfView );
		if (step != -1) {
			move( step );
			return true;
		} else {
			return false;
		}
	}
	
	protected boolean getFurther( int target ) {
		int step = Dungeon.flee( this, pos, target, 
			Level.passable, 
			Level.fieldOfView );
		if (step != -1) {
			move( step );
			return true;
		} else {
			return false;
		}
	}
	
	@Override
	public void move( int step ) {
		super.move( step );
		
		if (!flying) {
			Dungeon.level.mobPress( this );
		}
	}
	
	protected float attackDelay() {
		return 1f;
	}
	
	protected boolean doAttack( Char enemy ) {
		
		boolean visible = Dungeon.visible[pos];
		
		if (visible) {
			sprite.attack( enemy.pos );
		} else {
			attack( enemy );
		}
				
		spend( attackDelay() );
		
		return !visible;
	}
	
	@Override
	public void onAttackComplete() {
		attack( enemy );
		super.onAttackComplete();
	}
	
	@Override
	public int defenseSkill( Char enemy ) {
        if (enemySeen && !paralysed) {
            int defenseSkill = this.defenseSkill;
            int penalty = 0;
            for (Buff buff : enemy.buffs(RingOfAccuracy.Accuracy.class)) {
                penalty += ((RingOfAccuracy.Accuracy) buff).level;
            }
            if (penalty != 0)
                defenseSkill *= Math.pow(0.75, penalty);
            return defenseSkill;
        } else {
            return 0;
        }
	}
	
	@Override
	public int defenseProc( Char enemy, int damage ) {
		if (!enemySeen && enemy == Dungeon.hero && ((Hero)enemy).subClass == HeroSubClass.ASSASSIN) {
			damage *= 1.34f;
			Wound.hit( this );
		}
		return damage;
	}
	
	@Override
	public void damage( int dmg, Object src ) {

		Terror.recover( this );

        if (state == SLEEPING) {
            state = WANDERING;
        }
		alerted = true;
		
		super.damage( dmg, src );
	}
	
	
	@Override
	public void destroy() {
		
		super.destroy();
		
		Dungeon.level.mobs.remove( this );
		
		if (Dungeon.hero.isAlive()) {
			
			if (hostile) {
				Statistics.enemiesSlain++;
				Badges.validateMonstersSlain();
				Statistics.qualifiedForNoKilling = false;
				
				if (Dungeon.nightMode) {
					Statistics.nightHunt++;
				} else {
					Statistics.nightHunt = 0;
				}
				Badges.validateNightHunter();
			}
			
			if (Dungeon.hero.lvl <= maxLvl && EXP > 0) {
				Dungeon.hero.sprite.showStatus( CharSprite.POSITIVE, TXT_EXP, EXP );
				Dungeon.hero.earnExp( EXP );
			}
		}
	}
	
	@Override
	public void die( Object cause ) {
		
		super.die( cause );
		
		if (Dungeon.hero.lvl <= maxLvl + 2) {
			dropLoot();
		}
		
		if (Dungeon.hero.isAlive() && !Dungeon.visible[pos]) {	
			GLog.i( TXT_DIED );
		}
	}
	
	protected Object loot = null;
	protected float lootChance = 0;
	
	@SuppressWarnings("unchecked")
	protected void dropLoot() {
        float lootChance = this.lootChance;
        int bonus = 0;
        for (Buff buff : Dungeon.hero.buffs(RingOfWealth.Wealth.class)) {
            bonus += ((RingOfWealth.Wealth) buff).level;
        }

        lootChance *= Math.pow(1.1, bonus);

		if (loot != null && Random.Float() < lootChance) {
			Item item = null;
			if (loot instanceof Generator.Category) {
				
				item = Generator.random( (Generator.Category)loot );
				
			} else if (loot instanceof Class<?>) {
				
				item = Generator.random( (Class<? extends Item>)loot );
				
			} else {
				
				item = (Item)loot;
				
			}
			Dungeon.level.drop( item, pos ).sprite.drop();
		}
	}
	
	public boolean reset() {
		return false;
	}
	
	public void beckon( int cell ) {
		
		notice();
		
		if (state != HUNTING) {
			state = WANDERING;
		}
		target = cell;
	}
	
	public String description() {
		return "Real description is coming soon!";
	}
	
	public void notice() {
		sprite.showAlert();
	}
	
	public void yell( String str ) {
		GLog.n( "%s: \"%s\" ", name, str );
	}

    public interface AiState {
        public boolean act( boolean enemyInFOV, boolean justAlerted );
        public String status();
    }

    private class Sleeping implements AiState {

        public static final String TAG	= "SLEEPING";

        @Override
        public boolean act( boolean enemyInFOV, boolean justAlerted ) {
            if (enemyInFOV && Random.Int( distance( enemy ) + enemy.stealth() + (enemy.flying ? 2 : 0) ) == 0) {

                enemySeen = true;

                notice();
                state = HUNTING;
                target = enemy.pos;

                if (Dungeon.isChallenged( Challenges.SWARM_INTELLIGENCE )) {
                    for (Mob mob : Dungeon.level.mobs) {
                        if (mob != Mob.this) {
                            mob.beckon( target );
                        }
                    }
                }

                spend( TIME_TO_WAKE_UP );

            } else {

                enemySeen = false;

                spend( TICK );

            }
            return true;
        }

        @Override
        public String status() {
            return String.format( "This %s is sleeping", name );
        }
    }

    private class Wandering implements AiState {

        public static final String TAG	= "WANDERING";

        @Override
        public boolean act( boolean enemyInFOV, boolean justAlerted ) {
            if (enemyInFOV && (justAlerted || Random.Int( distance( enemy ) / 2 + enemy.stealth() ) == 0)) {

                enemySeen = true;

                notice();
                state = HUNTING;
                target = enemy.pos;

            } else {

                enemySeen = false;

                int oldPos = pos;
                if (target != -1 && getCloser( target )) {
                    spend( 1 / speed() );
                    return moveSprite( oldPos, pos );
                } else {
                    target = Dungeon.level.randomDestination();
                    spend( TICK );
                }

            }
            return true;
        }

        @Override
        public String status() {
            return String.format( "This %s is wandering", name );
        }
    }

    private class Hunting implements AiState {

        public static final String TAG	= "HUNTING";

        @Override
        public boolean act( boolean enemyInFOV, boolean justAlerted ) {
            enemySeen = enemyInFOV;
            if (enemyInFOV && canAttack( enemy )) {

                return doAttack( enemy );

            } else {

                if (enemyInFOV) {
                    target = enemy.pos;
                }

                int oldPos = pos;
                if (target != -1 && getCloser( target )) {

                    spend( 1 / speed() );
                    return moveSprite( oldPos,  pos );

                } else {

                    spend( TICK );
                    state = WANDERING;
                    target = Dungeon.level.randomDestination();
                    return true;
                }
            }
        }

        @Override
        public String status() {
            return String.format( "This %s is hunting", name );
        }
    }

    protected class Fleeing implements AiState {

        public static final String TAG	= "FLEEING";

        @Override
        public boolean act( boolean enemyInFOV, boolean justAlerted ) {
            enemySeen = enemyInFOV;
            if (enemyInFOV) {
                target = enemy.pos;
            }

            int oldPos = pos;
            if (target != -1 && getFurther( target )) {

                spend( 1 / speed() );
                return moveSprite( oldPos, pos );

            } else {

                spend( TICK );
                nowhereToRun();

                return true;
            }
        }

        protected void nowhereToRun() {
        }

        @Override
        public String status() {
            return String.format( "This %s is fleeing", name );
        }
    }

    private class Passive implements AiState {

        public static final String TAG	= "PASSIVE";

        @Override
        public boolean act( boolean enemyInFOV, boolean justAlerted ) {
            enemySeen = false;
            spend( TICK );
            return true;
        }

        @Override
        public String status() {
            return String.format( "This %s is passive", name );
        }
    }
}

