package cz.cuni.amis.pogamut.ut2004.agent.module.sensor;

import cz.cuni.amis.pogamut.base.agent.worldview.IWorldView;
import cz.cuni.amis.pogamut.base.agent.worldview.WorldEventListener;
import cz.cuni.amis.pogamut.ut2004.agent.module.AgentModule;
import cz.cuni.amis.pogamut.ut2004.communication.messages.UnrealId;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.AddInventoryMsg;

import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.ItemPickedUp;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Self;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Thrown;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.WeaponUpdate;
import java.util.logging.Logger;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Memory module specialized on info about the agent's inventory.
 *
 * @author Juraj 'Loque' Simlovic
 */
public class Weaponry extends AgentModule {

    /**
     * Tells, whether specific weapon is in the agent's inventory.
     *
     * @param unrealId UnrealId of the weapon to be examined.
     * @return True, if the requested weapon is present; false otherwise.
     */
    public boolean hasWeapon(UnrealId unrealId) {
        // retreive from storage of weapons
        return storage.byUnrealId.containsKey(unrealId);
    }

    /**
     * Tells, whether specific weapon is in the agent's inventory.
     *
     * @param type Type of the weapon to be examined.
     * @return True, if the requested weapon is present; false otherwise.
     */
    public boolean hasWeapon(String type) {
        // retreive from storage of weapons
        return storage.byType.containsKey(type);
    }

    /*========================================================================*/
    /**
     * Retreives specific weapon from the agent's inventory.
     *
     * @param unrealId UnrealId of the weapon to be retreived.
     * @return Requested weapon from inventory; or null upon no such weapon.
     *
     * @see hasWeapon(UnrealId)
     */
    public AddInventoryMsg getWeapon(UnrealId unrealId) {
        // retreive from storage of weapons
        return storage.byUnrealId.get(unrealId);
    }

    /**
     * Retreives specific weapon from the agent's inventory.
     *
     * @param type Type of the weapon to be retreived.
     * @return Requested weapon from inventory; or null upon no such weapon.
     *
     * @see hasWeapon(String)
     */
    public AddInventoryMsg getWeapon(String type) {
        // retreive from storage of weapons
        return storage.byType.get(type);
    }

    /*========================================================================*/
    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * @param weapon Weapon to be examined.
     * @return Amount of ammo in the inventory.
     *
     * @see getWeapon(UnrealId)
     * @see getWeapon(String)
     * @see getPrimaryAmmo(UnrealId)
     * @see getPrimaryAmmo(String)
     * @see getAlternateAmmo(AddInventoryMsg)
     */
    public int getPrimaryAmmo(AddInventoryMsg weapon) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon
     * for alternate firing mode.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * <p>FIXME[js]: What about weapons where alternate fire mode uses primary
     * ammo?
     *
     * @param weapon Weapon to be examined.
     * @return Amount of ammo in the inventory for alternate fire mode.
     *
     * @see getWeapon(UnrealId)
     * @see getWeapon(String)
     * @see getAlternateAmmo(UnrealId)
     * @see getAlternateAmmo(String)
     * @see getPrimaryAmmo(AddInventoryMsg)
     */
    public int getAlternateAmmo(AddInventoryMsg weapon) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * @param unrealId UnrealId of the weapon to be examined.
     * @return Amount of ammo in the inventory.
     *
     * @see getPrimaryAmmo(AddInventoryMsg)
     * @see getPrimaryAmmo(String)
     */
    public int getPrimaryAmmo(UnrealId unrealId) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon
     * for alternate firing mode.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * <p>FIXME[js]: What about weapons where alternate fire mode uses primary
     * ammo?
     *
     * @param unrealId UnrealId of the weapon to be examined.
     * @return Amount of ammo in the inventory for alternate fire mode.
     *
     * @see getAlternateAmmo(AddInventoryMsg)
     * @see getAlternateAmmo(String)
     */
    public int getAlternateAmmo(UnrealId unrealId) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * @param type Type of the weapon to be examined.
     * @return Amount of ammo in the inventory.
     *
     * @see getPrimaryAmmo(AddInventoryMsg)
     * @see getPrimaryAmmo(UnrealId)
     */
    public int getPrimaryAmmo(String type) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /**
     * Tells, how much ammo is in the agent's inventory for a specific weapon
     * for alternate firing mode.
     *
     * <p>Note: Ammo for weapons the agent do not posses yet can be available.
     *
     * <p>FIXME[js]: What about weapons where alternate fire mode uses primary
     * ammo?
     *
     * @param type Type of the weapon to be examined.
     * @return Amount of ammo in the inventory for alternate fire mode.
     *
     * @see getAlternateAmmo(AddInventoryMsg)
     * @see getAlternateAmmo(UnrealId)
     */
    public int getAlternateAmmo(String type) {
        // FIXME[js]: implement when available
        throw new UnsupportedOperationException("Not supported yet");
    }

    /*========================================================================*/
    /**
     * Retreives current weapon from the agent's inventory.
     *
     * @return Current weapon from inventory; or null upon no current weapon.
     *
     * @see getCurrentPrimaryAmmo()
     * @see getCurrentAlternateAmmo()
     * @see AgentInfo#getCurrentWeapon()
     */
    public AddInventoryMsg getCurrentWeapon() {
        // retreive current weapon id from AgentInfo
        UnrealId current = agentInfo.getCurrentWeapon();
        // retreive inventory weapon by id
        return (current == null) ? null : getWeapon(current);
    }

    /**
     * Tells, how much ammo is in the agent's inventory for current weapon.
     *
     * @return Amount of ammo in the inventory.
     *
     * @see getCurrentAlternateAmmo()
     * @see AgentInfo#getCurrentAmmo()
     */
    public int getCurrentPrimaryAmmo() {
        // retreive from AgentInfo
        return agentInfo.getCurrentAmmo();
    }

    /**
     * Tells, how much ammo is in the agent's inventory for current weapon for
     * alternate firing mode.
     *
     * <p>FIXME[js]: What about weapons where alternate fire mode uses primary
     * ammo?
     *
     * @return Amount of ammo in the inventory for alternate fire mode.
     *
     * @see getCurrentPrimaryAmmo()
     * @see AgentInfo#getCurrentAlternateAmmo()
     */
    public int getCurrentAlternateAmmo() {
        // retreive from AgentInfo
        return agentInfo.getCurrentAlternateAmmo();
    }

    /*========================================================================*/
    /**
     * Retreives all weapons from the agent's inventory.
     *
     * @return List of all available weapons from inventory.
     *
     * @see getMeleeWeapons()
     * @see getRangedWeapons()
     * @see getLoadedWeapons()
     * @see getLoadedMeleeWeapons()
     * @see getLoadedRangedWeapons()
     * @see getAmmos()
     */
    public Map<UnrealId, AddInventoryMsg> getWeapons() {
        // publish map of all weapons
        return Collections.unmodifiableMap(storage.byUnrealId);
    }

    /**
     * Retreives all loaded weapons from the agent's inventory.
     *
     * <p>Note: <b>Shield guns</b> are never treated as loaded weapons, though
     * they are usually <i>loaded</i>, i.e. ready to be fired.</p>
     *
     * @return List of all available weapons from inventory.
     *
     * @see hasLoadedWeapon()
     * @see getLoadedMeleeWeapons()
     * @see getLoadedRangedWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getLoadedWeapons() {
        // publish map of all loaded weapons
        return Collections.unmodifiableMap(storage.allLoaded);
    }

    /**
     * Retreives melee weapons from the agent's inventory.
     *
     * @return List of all available weapons from inventory.
     *
     * @see getLoadedMeleeWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getMeleeWeapons() {
        // publish map of all melee weapons
        return Collections.unmodifiableMap(storage.allMelee);
    }

    /**
     * Retreives ranged weapons from the agent's inventory.
     *
     * @return List of all available weapons from inventory.
     *
     * @see getLoadedRangedWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getRangedWeapons() {
        // publish map of all ranged weapons
        return Collections.unmodifiableMap(storage.allRanged);
    }

    /**
     * Retreives melee weapons from the agent's inventory.
     *
     * @return List of all available weapons from inventory.
     *
     * @see getLoadedRangedWeapons()
     * @see getLoadedWeapons()
     * @see getMeleeWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getLoadedMeleeWeapons() {
        // publish map of all loaded melee weapons
        return Collections.unmodifiableMap(storage.allLoadedMelee);
    }

    /**
     * Retreives ranged weapons from the agent's inventory.
     *
     * @return List of all available weapons from inventory.
     *
     * @see getLoadedMeleeWeapons()
     * @see getLoadedWeapons()
     * @see getRangedWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getLoadedRangedWeapons() {
        // publish map of all loaded ranged weapons
        return Collections.unmodifiableMap(storage.allLoadedRanged);
    }

    /*========================================================================*/
    /**
     * Retreives all ammos wihout weapons from the agent's inventory.
     *
     * @return List of all ammos wihout weapons from inventory.
     *
     * @see getWeapons()
     */
    public Map<UnrealId, AddInventoryMsg> getAmmos() {
        // publish map of all ammos without weapons
        return Collections.unmodifiableMap(storage.ammoByUnrealId);
    }

    /*========================================================================*/
    /**
     * Tells, whether the agent has any loaded weapon in the inventory.
     *
     * <p>Note: <b>Shield guns</b> are never treated as loaded weapons, though
     * they are usually <i>loaded</i>, i.e. ready to be fired.</p>
     *
     * @return True, if there is a loaded weapon in the inventory.
     *
     * @see getLoadedWeapons()
     */
    public boolean hasLoadedWeapon() {
        // are there any in the list of loaded weapons?
        // FIXME[js]: optimize!
        return !getLoadedWeapons().isEmpty();
    }

    /*========================================================================*/
    /**
     * Local weaponry class.
     */
    private class Storage {

        /** All foraged weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> byUnrealId = new HashMap<UnrealId, AddInventoryMsg>();
        /** All foraged weapon mapped by their type. */
        private HashMap<String, AddInventoryMsg> byType = new HashMap<String, AddInventoryMsg>();
        /** All foraged ammo (ammo without akin weapon) mapped by UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> ammoByUnrealId = new HashMap<UnrealId, AddInventoryMsg>();
        /** All foraged ammo (ammo without akin weapon) mapped by type. */
        private HashMap<String, AddInventoryMsg> ammoByType = new HashMap<String, AddInventoryMsg>();
        /** All loaded weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> allLoaded = new HashMap<UnrealId, AddInventoryMsg>();
        /** All loaded weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> allMelee = new HashMap<UnrealId, AddInventoryMsg>();
        /** All loaded weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> allRanged = new HashMap<UnrealId, AddInventoryMsg>();
        /** All loaded weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> allLoadedMelee = new HashMap<UnrealId, AddInventoryMsg>();
        /** All loaded weapons mapped by their UnrealId. */
        private HashMap<UnrealId, AddInventoryMsg> allLoadedRanged = new HashMap<UnrealId, AddInventoryMsg>();
        /**
         * FIXME[js]: TODO the processing.. update published maps, update the
         * current status of ammo...
         * 
         * See module of Players for inspiration.. It's a good place to start
         * copy/pasting.. ;)
         */
    }
    /** Local storage. */
    private Storage storage = new Storage();

    /*========================================================================*/
    /**
     * AddInventoryMsg listener.
     * By this we will get all basic information about new weapons in our inventory.
     * The problem is that the AddInventoryMsg arrives just once for every weapon
     * or ammo category. That's why we will need another listener - ItemPickedUp listener.
     * Here we shouldn't count any ammo at all.
     */
    private class AddInventoryMsgListener implements WorldEventListener<AddInventoryMsg> {

        @Override
        public void notify(AddInventoryMsg event) {
            boolean isWeapon, isAmmo, hasAmmo;

            isWeapon = false;
            isAmmo = false;

            if (event.getType().contains("Ammo"))
                isAmmo = true;
            else if (event.getType().contains("Weapon"))
                isWeapon = true;
            
            //BASIC adding of the weapon to the inventory
            //We are using put because this message should come just once for
            //every weapon or ammo category
            if (isWeapon) //For weapons
            {/* //TODO: we are waiting for new item translator to be implemented
                hasAmmo = (event.getPrimaryInitialAmmo() != 0);

                if (hasAmmo) {
                    storage.allLoaded.put(event.getId(), event);
                }

                if (event.isMelee()) {
                    storage.allMelee.put(event.getId(), event);
                    if (hasAmmo) {
                        storage.allLoadedMelee.put(event.getId(), event);
                    }
                } else {
                    storage.allRanged.put(event.getId(), event);
                    if (hasAmmo) {
                        storage.allLoadedRanged.put(event.getId(), event);
                    }
                }*/
            } else if (isAmmo) //For ammunition
            {
                storage.ammoByUnrealId.put(event.getId(), event);
                storage.ammoByType.put(event.getType(), event);
            }
        //else - for other inventory - basically we will ignore it here


        }

        /**
         * Constructor. Registers itself on the given WorldView object.
         * @param worldView WorldView object to listent to.
         */
        public AddInventoryMsgListener(IWorldView worldView) {
            worldView.addListener(AddInventoryMsg.class, this);
        }
    }
    /** ItemPickedUp listener */
    AddInventoryMsgListener addInventoryMsgListener;

    /**
     * ItemPickedUp listener.
     * Here we will count the ammo properly.
     */
    private class ItemPickedUpListener implements WorldEventListener<ItemPickedUp> {

        @Override
        public void notify(ItemPickedUp event) {
        // FIXME[mb]: what to do? Well process and update! :-)
        }

        /**
         * Constructor. Registers itself on the given WorldView object.
         * @param worldView WorldView object to listent to.
         */
        public ItemPickedUpListener(IWorldView worldView) {
            worldView.addListener(ItemPickedUp.class, this);
        }
    }
    /** ItemPickedUp listener */
    ItemPickedUpListener itemPickedUpListener;

    /**
     * WeaponUpdate listener.
     * When we change weapon, we need to update ammo of the old weapon - because of
     * the delay in synchronous batches.
     */
    private class WeaponUpdateListener implements WorldEventListener<WeaponUpdate> {

        @Override
        public void notify(WeaponUpdate event) {
        // FIXME[mb]: what to do? Well process and update! :-)
        }

        /**
         * Constructor. Registers itself on the given WorldView object.
         * @param worldView WorldView object to listent to.
         */
        public WeaponUpdateListener(IWorldView worldView) {
            worldView.addListener(WeaponUpdate.class, this);
        }
    }
    /** WeaponUpdate listener */
    WeaponUpdateListener weaponUpdateListener;

    /**
     * Thrown listener.
     * When we loose some weapon from the inventory we want to know about it.
     */
    private class ThrownListener implements WorldEventListener<Thrown> {

        @Override
        public void notify(Thrown event) {
        // FIXME[mb]: what to do? Well process and update! :-)
        }

        /**
         * Constructor. Registers itself on the given WorldView object.
         * @param worldView WorldView object to listent to.
         */
        public ThrownListener(IWorldView worldView) {
            worldView.addListener(Thrown.class, this);
        }
    }
    /** Thrown listener */
    ThrownListener thrownListener;

    /*========================================================================*/
    /** AgentInfo memory module. */
    protected AgentInfo agentInfo;

    /**
     * Constructor. Setups the memory module based on given WorldView.
     * @param worldView WorldView object to read the info from.
     * @param agentInfo AgentInfo memory module. Note: If <i>null</i> is
     * provided, this memory module creates its own AgentInfo memory module.
     * Provide shared AgentInfo memory module to economize CPU time and other
     * resources.
     * @param log Logger to be used for logging runtime/debug info.
     */
    public Weaponry(IWorldView worldView, AgentInfo agentInfo, Logger log) {
        super(log);

        // set or create AgentInfo memory module
        if (agentInfo != null) {
            this.agentInfo = agentInfo;
        } else {
            this.agentInfo = new AgentInfo(worldView, null, log);
        }

        // create listeners
        addInventoryMsgListener = new AddInventoryMsgListener(worldView);
        itemPickedUpListener = new ItemPickedUpListener(worldView);
        weaponUpdateListener = new WeaponUpdateListener(worldView);
        thrownListener = new ThrownListener(worldView);
        //TODO selfListener = new SelfListener(worldView);

    }
}
