package cz.cuni.amis.pogamut.ut2004.examples;

import cz.cuni.amis.pogamut.base.agent.navigation.PathNotConstructable;
import cz.cuni.amis.pogamut.base3d.worldview.objects.ILocated;

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.agent.navigation.PathPlannerListener;
import cz.cuni.amis.pogamut.base.agent.worldview.WorldEventListener;
import cz.cuni.amis.pogamut.base.communication.commands.ICommandSerializer;
import cz.cuni.amis.pogamut.base.exceptions.PogamutException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.base3d.worldview.objects.Location;

import cz.cuni.amis.pogamut.ut2004.agent.module.sensor.AgentInfo;
import cz.cuni.amis.pogamut.ut2004.agent.module.sensor.Game;
import cz.cuni.amis.pogamut.ut2004.agent.module.sensor.Players;
import cz.cuni.amis.pogamut.ut2004.agent.navigation.UTAstar;
import cz.cuni.amis.pogamut.ut2004.agent.worldview.UT2004SyncLockableWorldView;
import cz.cuni.amis.pogamut.ut2004.bot.SyncUT2004Bot;
import cz.cuni.amis.pogamut.ut2004.communication.messages.UnrealId;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.*;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.*;

import cz.cuni.amis.pogamut.ut2004.communication.translator.events.MapPointListObtained;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Example of Simple Pogamut bot, that randomly walks around the map. Bot is uncapable 
 * of handling movers so far.
 *
 * @author Michal Bida aka Knight
 */
@AgentScoped
public class SimpleBot extends SyncUT2004Bot<UT2004SyncLockableWorldView> {

    //agent pathing helper variables
    public ArrayList<ILocated> myPath = new ArrayList<ILocated>();
    public boolean pathRequested = false;
    public boolean pathReceived = false;
    //All NavPoints on the map are stored here
    public HashMap<UnrealId, NavPoint> knownNavPoints = new HashMap<UnrealId, NavPoint>();
    //nav point i am heading to
    public NavPoint myNavTarget;
    //current UnrealTournament time
    public double currentTime = 0;
    //last time we've actually moved
    public double lastMovementTime = 0;
    //bot memory modules
    public Game game;
    public AgentInfo agentInfo;
    public Players players;
    //path finding module
    public UTAstar myPathPlanner;
    //Here we will listen to map event and store all nav points in our internals
    public WorldEventListener myMapListObtainedListener = new WorldEventListener() {

        @Override
        public void notify(Object event) {

            MapPointListObtained map;

            map = (MapPointListObtained) event;
            knownNavPoints.putAll(map.getNavPoints());

        }
    };
    //Listens to BeginMessage and stores current UnrealTournament time
    public WorldEventListener myBegListener = new WorldEventListener() {

        @Override
        public void notify(Object event) {
            if (event instanceof BeginMessage) {
                BeginMessage bm = (BeginMessage) event;
                currentTime = bm.getTime();
            }
        }
    };
    //Path listener - initialized in goToLocation method
    public PathPlannerListener myPathListener = new PathPlannerListener() {

        @Override
        public void pathEvent(List path) {
            myPath = (ArrayList<ILocated>) path;
            pathReceived = true;
        }
    };

    @Inject
    public SimpleBot(AgentLogger logger, UT2004SyncLockableWorldView worldView, ICommandSerializer commandSerializer) {
        super(logger, worldView, commandSerializer);
    }

    /**
     * Initialize all necessary variables here, before the bot actually receives anything
     * from the environment.
     */
    @Override
    protected void prePrepareBot() {

        //sets the logger level, if we would let the logger level to be Level.All the bot would
        //be slowed significantly due to high number of messages
        getLogger().setLevel(Level.WARNING);

        //Register two listeners - one for navigation point, second for begine message (so we can update currentTime)
        this.getWorldView().addListener(MapPointListObtained.class, myMapListObtainedListener);
        this.getWorldView().addListener(BeginMessage.class, myBegListener);

        //initialize memory modules
        this.game = new Game(this.getWorldView(), this.getLogger().user());
        this.agentInfo = new AgentInfo(this.getWorldView(), game, this.getLogger().user());
        this.players = new Players(this.getWorldView(), agentInfo, this.getLogger().user());

        //initialize path planner
        this.myPathPlanner = new UTAstar(this.getAct(), this.getWorldView());
    }

    /**
     * Here we have already received information about game in GameInfo
     *
     * @param info
     */
    @Override
    protected void postPrepareBot(GameInfo info) {
    }

    /**
     * Here we can modify initializing command for our bot.
     *
     * @return
     */
    @Override
    protected Initialize createInitializeCommand() {
        Initialize myInit = new Initialize();

        //just set the name of the bot, nothing else
        myInit.setName("SimpleBot007");

        return myInit;
    }

    /**
     * The bot is initilized in the environment - a physical representation of the
     * bot is present in the game.
     *
     * @param config information about configuration
     * @param init information about configuration
     */
    @Override
    protected void botInitialized(ConfigChange config, InitedMessage init) {

        getAct().act(new SendMessage().setText("I am alive!"));

    }

    /**
     * Main method that controls the bot - makes decisions what to do next.
     * It is called iteratively by Pogamut engine every time a synchronous batch
     * from the environement is received. This is usually 4 times per second - it
     * is affected by visionTime variable, that can be adjusted in GameBots ini file in
     * UT2004/System folder.
     *
     * @throws cz.cuni.amis.pogamut.base.exceptions.PogamutException
     */
    @Override
    protected void doLogic() throws PogamutException {

        //update our lastMovementTime counter
        if (agentInfo.getVelocity().size() > 10) {
            lastMovementTime = currentTime;
        }

        //set new random navigation point to go to
        if (myNavTarget == null) {
            myPath.clear();
            pathRequested = false;
            pathReceived = false;
            myNavTarget = pickNewRandomNavTarget(); //random movement
        }

        if (myNavTarget != null) {
            //follow the path to NavPoint and set to null if we reach it
            if ((myNavTarget.getLocation().getDistance(agentInfo.getLocation()) < 80)) {
                myNavTarget = null;
            } else {
                goToLocation(myNavTarget.getLocation());
            }
        }

        //anti stuck policy here - if we are not moving for 3 seconds..
        if ((currentTime - lastMovementTime) > 3) {
            //jump bot and pick different Nav Point.
            getAct().act(new Jump());
            myNavTarget = null;
        }

        //this.getLogger().user().warning("VelocitySize: "+agentInfo.getVelocity().size() + " Cur.time: " + currentTime + " LastmoveTime: " + lastMovementTime);
    }

    /**
     * Called each time our bot die. Good for reseting all bot state dependent variables.
     *
     * @param event
     */
    @Override
    protected void botKilled(BotKilled event) {
        myPath.clear();
        pathReceived = false;
        pathRequested = false;
        myNavTarget = null;
    }

    /**
     * Goes to target location. Keeps returning true while we are on our way there.
     * If we are at location or some problem encountered, then returns false.
     *
     * @param targetLocation
     * @return
     */
    private boolean goToLocation(Location targetLocation) {

        if (agentInfo.atLocation(targetLocation, 100)) {
            myPath.clear();
            pathReceived = false;
            pathRequested = false;
            return false;
        }

        if (myPath.isEmpty() && !pathRequested) {
            try {
                myPathPlanner.addPathListener(myPathListener);
                myPathPlanner.computePath(agentInfo.getLocation(), targetLocation);
                pathRequested = true;
                return true;
            } catch (PathNotConstructable ex) {
                Logger.getLogger(SimpleBot.class.getName()).log(Level.SEVERE, null, ex);
                return false;
            }
        }

        //waiting for the path
        if (pathRequested && !pathReceived) {
            return true;
        }

        //if we are so close we get 0 path, we will add our target location to the path
        if (myPath.isEmpty() && pathReceived) {
            myPath.add(targetLocation);
        }

        //throw out points we've been to
        if (myPath.get(0).getLocation().getDistance(agentInfo.getLocation()) < 40) {
            myPath.remove(0);
        }

        Iterator<ILocated> it = myPath.iterator();
        if (it.hasNext()) {
            Location l1 = it.next().getLocation();
            if (it.hasNext()) {
                // there are at least two points in the path
                Location l2 = it.next().getLocation();
                getAct().act(new Move().setFirstLocation(l1).setSecondLocation(l2));
                return true;
            } else {
                // there is only one point to go to
                getAct().act(new Move().setFirstLocation(l1));
                return true;
            }
        }

        return false;

    }

    /**
     * Rendomly picks some navigation point to head to.
     *
     * @return
     */
    private NavPoint pickNewRandomNavTarget() {
        Random rand = new Random();
        int i, counter;

        counter = rand.nextInt(knownNavPoints.values().size());

        i = 0;
        for (NavPoint nav : knownNavPoints.values()) {
            if (i == counter) {
                return nav;
            }
            i++;
        }

        return null;
    }
}