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

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.agent.AgentState;
import cz.cuni.amis.pogamut.base.agent.AgentStateType;
import cz.cuni.amis.pogamut.base.agent.IAgent;
import cz.cuni.amis.pogamut.base.agent.exceptions.AgentException;
import cz.cuni.amis.pogamut.base.agent.jmx.AgentJMXProxy;
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.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.server.AbstractWorldServer;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.ut2004.bot.AbstractUT2004Bot;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.ChangeMap;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbcommands.GetMaps;
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.GameInfo;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.MapList;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Mutator;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Password;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Player;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.MapPointListObtained;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.PlayerListObtained;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.ReadyCommandRequest;
import cz.cuni.amis.pogamut.ut2004.server.exceptions.MapChangeException;
import cz.cuni.amis.utils.collections.ElementListener;
import cz.cuni.amis.utils.collections.ObservableCollection;
import cz.cuni.amis.utils.collections.ObservableList;
import cz.cuni.amis.utils.collections.TranslatedObservableCollection;
import cz.cuni.amis.utils.collections.adapters.WVVisibleObjectsSetAdapter;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.FlagListener;
import cz.cuni.amis.utils.flag.ImmutableFlag;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import javax.management.ObjectName;

/**
 * Abstract class - ancestor of all UT2004 server controls.
 * <p><p>
 * It counts with GameBots2004 protocol therefore taking care of:
 * <ol>
 * <li>ReadyCommandRequest - sending automatically ready(), override readyCommandRequested() if you're not comfortable with this</li>
 * <li>Password - when password is requested it calls method createPasswordReply()</li>
 * </ol> 
 * <p><p>
 * Also introducing user-method for setting up custom worldview listeners that 
 * is called before Ready message is sent -  prePrepareServer().
 * <p><p>
 * You may use setPassword() method to specify the password before starting the agent.
 * 
 * @author Jimmy
 */
@AgentScoped
public abstract class AbstractUT2004Server<WORLD_VIEW extends IStartableWorldView> extends AbstractWorldServer<WORLD_VIEW, IAgent> implements IUT2004Server {

    ObservableCollection<Player> players = null;
    List<Mutator> mutators = null;
    Flag<Double> gameSpeed = new Flag<Double>();
    List<MapList> maps = null;
    /**
     * Collection of all connected Pogamut bots.
     */
    ObservableCollection<AbstractUT2004Bot> agents = null;
    /**
     * If specified - used for the construction of the PasswordReply in createPasswordReply() method.
     */
    private String desiredPassword = null;
    private WorldEventListener<PlayerListObtained> playerListObtainedListener =
            new WorldEventListener<PlayerListObtained>() {

                public void notify(PlayerListObtained event) {
                    players.addAll(event.getPlayers());
                    // players list is received as the last in initial communication
                }
            };
    private WorldEventListener<MapPointListObtained> mapPointListObtainedListener =
            new WorldEventListener<MapPointListObtained>() {

                public void notify(MapPointListObtained event) {
                    // TODO process the navpoints
                    // ask for maps on the server
                    getAct().act(new GetMaps());
                }
            };

    @Inject
    public AbstractUT2004Server(AgentLogger logger, WORLD_VIEW worldView, ICommandSerializer commandSerializer) {
        super(logger, worldView, commandSerializer);
        getWorldView().addListener(ReadyCommandRequest.class, readyCommandRequestListener);
        getWorldView().addListener(Password.class, passwordRequestedListener);
        getWorldView().addListener(GameInfo.class, gameInfoListener);

        // listen for initial players list
        getWorldView().addListener(PlayerListObtained.class, playerListObtainedListener);

        getWorldView().addListener(MapPointListObtained.class, mapPointListObtainedListener);
        //TODO setAgentState(AgentStateType.RUNNING, "Control server connection is up and running.");
        players = new WVVisibleObjectsSetAdapter<Player>(Player.class, getWorldView());
    }

    /**
     * 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 control-server developer.
     * <p><p>
     * Called after the GameBots2004 greets the control server 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 prePrepareServer();

    // --------------
    // -=-=-=-=-=-=-=
    // 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());
    }

    @Override
    public void start() throws AgentException {
        try {
            final CountDownLatch latch = new CountDownLatch(1);
            FlagListener<AgentState> okListener = new FlagListener<AgentState>() {

                @Override
                public void flagChanged(AgentState changedValue) {
                    if (changedValue.getType().isOKState()) {
                        latch.countDown();
                    } // TODO report failure
                }
            };
            getAgentState().addListener(okListener);
            super.start();
            latch.await();
        } catch (InterruptedException ex) {
            throw new AgentException("Interrupted waiting for initialization.", ex);
        }

    }
    /**
     * 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 prePrepareServer()");
                    prePrepareServer();
                    setAgentStateDescription("prePrepareServer() finished, sending READY");
                    readyCommandRequested();
                    setAgentStateDescription("READY sent.");
                }
            };
    // -----------------
    // -=-=-=-=-=-=-=-=-
    // 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);
                }
            };

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

    @Override
    public void pause() throws AgentException {
        // no need to do anything ... the server doesn't have a body
    }

    @Override
    public void resume() throws AgentException {
        // no need to do anything ... the server doesn't have a body
    }

    @Override
    public Collection<MapList> getAvailableMaps() {
        return maps;
    }

    @Override
    public Flag<Double> getGameSpeedFlag() {
        return gameSpeed;
    }

    @Override
    public ImmutableFlag<String> getMapFlag() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public ObservableCollection<Player> getPlayers() {
        return players;
    }

    @Override
    public void setGameMap(String map) throws MapChangeException {
        getAct().act(new ChangeMap(map));
    }

    @Override
    public List<Mutator> getMutators() {
        return mutators;
    }

    @Override
    public ObservableCollection<IAgent> getAgents() {
        if (getPlayers() == null) {
            // the info has not been initialized yet
            return null;
        } else {
            /*TODO
            agents = new TranslatedObservableCollection<IAgent, Player>(getPlayers()) {

            @Override
            protected IAgent translate(Player obj) {
            if (obj.getJmx() != null) {
            // the player represents Pogamut agent
            // TODO
            return new AgentJMXProxy(null, ObjectName.WILDCARD);
            } else {
            return null;
            }
            }
            };
             */
        }
        // TODO Auto-generated method stub
        return null;
    }

    public void connectNativeBot(String botName) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    protected class AgentsList extends ObservableList<AbstractUT2004Bot> {

        public AgentsList(ObservableCollection<Player> players) {
            super(new ArrayList()); // TODO
            players.addCollectionListener(new ElementListener() {

                @Override
                public void elementChanged(Object elem, boolean added) {
                    throw new UnsupportedOperationException("Not supported yet.");
                }
            });
        }
    }
}