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

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.agent.AbstractEmbodiedAgent;
import cz.cuni.amis.pogamut.base.agent.AgentStateType;
import cz.cuni.amis.pogamut.base.agent.exceptions.AgentException;
import cz.cuni.amis.pogamut.base.agent.exceptions.AgentRuntimeException;
import cz.cuni.amis.pogamut.base.agent.worldview.IStartableWorldView;
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.Pogamut;
import cz.cuni.amis.pogamut.base.utils.PogamutPlatform;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.Initialize;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.PasswordReply;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.Ready;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.ConfigChange;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.GameInfo;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.InitedMessage;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Password;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.InitCommandRequest;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.ReadyCommandRequest;
import cz.cuni.amis.utils.flag.FlagListener;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Abstract class - ancestor of all UT2004 bots.
 * <p><p>
 * It counts with GameBots2004 protocol, working like this:
 * <ol>
 * <li>-> HELLO_BOT received</li>
 * <li>this.prePrepareBot() called</li>\
 * <li>&lt;- READY sent</li>
 * <li>... now is the time of the handshake with GB2004, possibly requiring the password, use setPassword() if you expects it ...</li>
 * <li>-> InitCommandRequested event is caught</li>
 * <li>this.postPrepareBot() called</li>
 * <li>this.createInitializeCommand()</li>
 * <li>&lt;- created INIT command sent</li>
 * <li>-> ConfigChange message caught</li>
 * <li>-> InitedMessage caught</li>
 * <li>this.botInitialized() called</li>
 * <li>... bot is running in the environment ... </li>
 * </ol> 
 * <p><p>
 * Key methods to be implemented (they are called in this order):
 * <ol>
 * <li>prePrepareBot() - you should use it to setup world view listeners, nothing is known about the UT2004 game in this point</li>
 * <li>postPrepareBot() - you should use it to preprocess the map where the bot is running (note that when this method is called the nav points are known, game info is also available)</li>
 * <li>createInitializeCommand() - here you should provide your custom INIT command, you may specify a specific skin here for your bot (for instance)</li>
 * <li>botInitialized() - this is called with the first configuration parameters of the bot's body inside the environment</li>
 * </ol> 
 * 
 * @author Jimmy
 */
@AgentScoped
public abstract class AbstractUT2004Bot<WORLD_VIEW extends IStartableWorldView> extends AbstractEmbodiedAgent<WORLD_VIEW> {

    /**
     * If specified - used for the construction of the PasswordReply in createPasswordReply() method.
     */
    private String desiredPassword = null;
    private FlagListener<Boolean> runningListener = new FlagListener<Boolean>() {

        public void flagChanged(Boolean changedValue) {
            if (!changedValue) {
                if (!stopCalled) {
                    stop();
                }
            }
        }
    };
    private boolean stopCalled = false;

    @Inject
    public AbstractUT2004Bot(AgentLogger logger, WORLD_VIEW worldView, ICommandSerializer commandSerializer) {
        super(logger, worldView, commandSerializer);
        getWorldView().getRunning().addListener(runningListener);
        getWorldView().addListener(ReadyCommandRequest.class, readyCommandRequestListener);
        getWorldView().addListener(InitCommandRequest.class, initCommandRequestListener);
        getWorldView().addListener(Password.class, passwordRequestedListener);
        getWorldView().addListener(InitedMessage.class, initedMessageListener);
    }

    /**
     * Specify the password that should be used if required by the world.
     * @param password
     */
    public void setPassword(String password) {
        this.desiredPassword = password;
    }

    /**
     * To be used by the bot developer.
     * <p><p>
     * Called after the GameBots2004 greets the bot before the handshake
     * (reaction to ReadyCommandRequested event), to be used to hook your own event listeners.
     * <p><p>
     * Note that all your listeners are added after ones hooked up by Pogamut-Core that is:
     * <ul>
     * <li>a good thing - we are always sure that platform listeners is executed first and the user (you) deals with correct states of objects</li>
     * <li>not so good - your listeners are always called after platform listeners (but you should need REALLY GOOD reason to be called first!)</li>
     * </ul>
     */
    protected abstract void prePrepareBot();

    /**
     * To be used by the bot developer.
     * <p><p>
     * Called before the INIT command is sent, before createInitializeCommand() is called
     * thus giving you time to probe the game info.
     *
     * @param init
     * @param config
     */
    protected abstract void postPrepareBot(GameInfo info);

    /**
     * This method is called after handshake with GameBots2004 is over and the GameBots2004
     * is awaiting the INIT command (Initialize class). Here you have to construct the
     * Initialize message where you may specify many starting parameters of the bot including:
     * <ul>
     * <li>bot's name</li>
     * <li>bot skill level (aim precision)</li>
     * <li>bot skin</li>
     * <li>raycasting</li>
     * <li>etc. - for complete list see Initialize command</li>
     * </ul>   
     * This message is then saved to private field initalizeCommand and is accessible
     * via getInitializeCommand() method if required to probe the starting parameters (even though
     * they can be changed during the bot's lifetime!).
     */
    protected abstract Initialize createInitializeCommand();

    /**
     * Last method that is called before the logic of the bot can become effective.
     * <p><p>
     * You may probe the bot's configuration parameters inside this method and
     * react on them.
     * 
     * @param config
     * @param init
     */
    protected abstract void botInitialized(ConfigChange config, InitedMessage init);

    // --------------
    // -=-=-=-=-=-=-=
    // READY LISTENER
    // -=-=-=-=-=-=-=
    // --------------
    /**
     * This method is called whenever HelloBot message is parsed - the GameBots2004 is awaiting
     * the bot to reply with Ready command to begin the handshake.
     */
    protected void readyCommandRequested() {
        getAct().act(new Ready());
    }
    /**
     * Listener that is hooked to WorldView awaiting event ReadyCommandRequest calling
     * setupWorldViewListeners() and then readyCommandRequested() method upon receiving the event.
     */
    private WorldEventListener<ReadyCommandRequest> readyCommandRequestListener =
            new WorldEventListener<ReadyCommandRequest>() {

                @Override
                public void notify(ReadyCommandRequest event) {
                    setAgentState(AgentStateType.INIT, "GameBots2004 greeted us, adding custom listeners onto the worldview");
                    setAgentStateDescription("Calling prePrepareBot()");
                    prePrepareBot();
                    setAgentStateDescription("prePrepareBot() finished, sending READY");
                    readyCommandRequested();
                    setAgentStateDescription("READY sent, handshaking.");
                }
            };
    // --------------------
    // -=-=-=-=-=-=-=-=-=-=
    // INITIALIZER LISTENER
    // -=-=-=-=-=-=-=-=-=-=
    // --------------------
    /**
     * Instance of this command that has been sent to the GameBots2004 when INIT command is requested (after handshake).
     */
    private Initialize initializeCommand = null;

    /**
     * Instance of this command that has been sent to the GameBots2004 when INIT command is requested (after handshake).
     * 
     * @return
     */
    public Initialize getInitializeCommand() {
        return initializeCommand;
    }

    /**
     * This method is called whenever handshake with GameBots2004 is over - the GameBots2004 is awaiting
     * the bot to reply with Ready command to begin the handshake. It calls setUpInit() method
     * to obtains Initialize message that is then sent to GameBots2004.
     * <p><p>
     * Left as protected if you need to override it - but you probably wouldn't.
     */
    protected void initCommandRequested() {
        initializeCommand = createInitializeCommand();
        if (initializeCommand == null) {
            AgentRuntimeException e = new AgentRuntimeException("createInitializeCommand() method returned null message, can't initialize the agent!", getLogger().platform());
            terminate(AgentStateType.FAILED, e.getMessage(), getAgentStopTimeoutMillis());
            throw e;
        }
        try {
            // set the JMX name
            initializeCommand.setJmx(startJMX());
        } catch (PogamutException ex) {
            throw new RuntimeException("Error seting up JMX name of the agent.", ex);
        }
        getAct().act(initializeCommand);
    }

    /**
     * Starts JMX and returns the address of the agent.
     * @return
     */
    protected String startJMX() throws PogamutException {

        // set the JMX name
        /* TODO String name = Pogamut.getPlatform().getMBeanServerURL().toString() + "|" + getJMX().getAgentJMXName().toString();
        return name;
         */
        return "not-initialized";
    }

    /**
     * Listener that is hooked to WorldView awaiting event InitCommandRequest calling
     * initCommandRequested() method upon receiving the event.
     */
    private WorldEventListener<InitCommandRequest> initCommandRequestListener =
            new WorldEventListener<InitCommandRequest>() {

                @Override
                public void notify(InitCommandRequest event) {
                    setAgentStateDescription("Handshake over, calling postPrepareBot()");
                    postPrepareBot(getGameInfo());
                    setAgentStateDescription("postPrepareBot() finished, sending INIT command");
                    initCommandRequested();
                }
            };
    // -----------------
    // -=-=-=-=-=-=-=-=-
    // PASSWORD LISTENER
    // -=-=-=-=-=-=-=-=-
    // -----------------
    /**
     * Instance of the password reply command that was sent upon receivieng request for the password (the world is locked).
     * <p><p>
     * If null the password was not required by the time the bot connected to the world.
     */
    private PasswordReply passwordReply = null;

    /**
     * Instance of the password reply command that was sent upon receivieng request for the password (the world is locked).
     * <p><p>
     * If null the password was not required by the time the bot connected to the world.
     *
     * @return
     */
    public PasswordReply getPasswordReply() {
        return passwordReply;
    }

    /**
     * This method is called whenever the Password event is caught telling us the world
     * is locked and is requiring a password.
     * <p><p>
     * May return null - in that case an empty password is sent to the server (which will probably
     * result in closing the connection and termination of the agent).
     * <p><p>   
     * This message is then saved to private field passwordReply and is accessible
     * via getPasswordReply() method if required to be probed during the bot's runtime.
     * <p><p>
     * Note that if setPassword() method is called before this one it will use
     * provided password via that method.
     */
    protected PasswordReply createPasswordReply() {
        return desiredPassword != null ? new PasswordReply(desiredPassword) : null;
    }
    /**
     * Listener that is hooked to WorldView awaiting event InitCommandRequest calling
     * initCommandRequested() method upon receiving the event.
     */
    private WorldEventListener<Password> passwordRequestedListener =
            new WorldEventListener<Password>() {

                @Override
                public void notify(Password event) {
                    setAgentStateDescription("Password requested by the world.");
                    passwordReply = createPasswordReply();
                    if (passwordReply == null) {
                        passwordReply = new PasswordReply("");
                    }
                    getLogger().platform().info("Password required for the world, replying with '" + passwordReply.getPassword() + "'.");
                    getAct().act(passwordReply);
                }
            };
    // -------------------------
    // -=-=-=-=-=-=-=-=-=-=-=-=-
    // GAMEINFO MESSAGE LISTENER
    // -=-=-=-=-=-=-=-=-=-=-=-=-
    // -------------------------
    /**
     * Contains information about the game.
     */
    private GameInfo gameInfo = null;

    public GameInfo getGameInfo() {
        return gameInfo;
    }
    private WorldEventListener<GameInfo> gameInfoListener =
            new WorldEventListener<GameInfo>() {

                @Override
                public void notify(GameInfo event) {
                    gameInfo = event;
                    getWorldView().removeListener(GameInfo.class, this);
                }
            };
    // ------------------------------------
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // FIRST CONFIG CHANGE MESSAGE LISTENER
    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    // -----------------------------------
    /**
     * Contains information about the game.
     */
    private ConfigChange firstConfig = null;
    private WorldEventListener<ConfigChange> configListener =
            new WorldEventListener<ConfigChange>() {

                @Override
                public void notify(ConfigChange event) {
                    firstConfig = event;
                    getWorldView().removeListener(ConfigChange.class, this);
                }
            };
    // -----------------------
    // -=-=-=-=-=-=-=-=-=-=-=-
    // INITED MESSAGE LISTENER
    // -=-=-=-=-=-=-=-=-=-=-=-
    // -----------------------
    /**
     * Listener that is hooked to WorldView awaiting event InitedMessage calling
     * botInitialized method upon receiving the event.
     */
    private WorldEventListener<InitedMessage> initedMessageListener =
            new WorldEventListener<InitedMessage>() {

                @Override
                public void notify(InitedMessage event) {
                    setAgentStateDescription("Bot initialized, calling botInited()");
                    botInitialized(firstConfig, event);
                    firstConfig = null;
                    setAgentStateDescription("botInited() finished, bot initialized OK");
                    setAgentState(AgentStateType.RUNNING, "Bot is running.");
                }
            };

    @Override
    public WORLD_VIEW getWorldView() {
        return super.getWorldView();
    }

    @Override
    public void start() throws AgentException {
        stopCalled = false;
        super.start();
        /* TODO
         try {
            // init JMX
            getJMX().enableJMX(Pogamut.getPlatform().getMBeanServer());
        } catch (PogamutException ex) {
            throw new AgentException("Error initializing JMX.", ex, this);
        }
         */
    }

    @Override
    public void stop() {
        stopCalled = true;
        super.stop();
    }
}
