/***********************************************************************

GECCO, a platform for command and control gaming

Copyright (C) 2001 Henrik Bäärnhielm, Andreas Enblom, Niklas
Hallenfur, Karl Hasselström, Henrik Hägerström, Oskar Linde, Jing Fu
Zi and Jon Åslund

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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
USA.

***********************************************************************/

package gecco.server.actionprocessor;
import gecco.server.core.*;
import gecco.server.unitmanager.UnitManager;
import gecco.server.visibility.VisibilityManager;

import java.util.*;

/**
 *  The action processor handles the scheduling and execution of actions for the
 *  units in the ongoing game.
 *
 * @author     Henrik Bäärnhielm, Karl Hasselström, Henrik Hägerström
 * @created    April 23, 2001
 */
public class ActionProcessor implements Runnable {

    /**
     *  Description of the Field
     */
    private TreeSet actionQueue;

    /**
     *  Description of the Field
     */
    private UnitManager unitManager;
    /**
     *  Description of the Field
     */
    private VisibilityManager visibilityManager;
    /**
     *  Description of the Field
     */
    private Client comClient;


    /**
     *  Constructor for the ActionProcessor object
     *
     * @param  _unitManager        An instance of the unit manager
     * @param  _visibilityManager  An instance of the visibility manager
     * @param  _comClient          An instance of the communication client
     *      interface
     */
    public ActionProcessor(UnitManager _unitManager, VisibilityManager _visibilityManager, Client _comClient) {
        actionQueue = new TreeSet();
        unitManager = _unitManager;
        visibilityManager = _visibilityManager;
        comClient = _comClient;
    }


    /**
     *  Returns the <code>TreeSet</code> object which is the queue that holds
     *  the current scheduled actions for the units in the game. This method is
     *  primarily used when another thread wants to synchronize on the queue
     *  object, preventing any other thread to add a new action to the queue.
     *
     * @return    The priority queue that is used for scheduling actions for
     *      units.
     */
    public TreeSet getActionQueue() {
        return actionQueue;
    }


    /**
     *  Simply returns the current system time.
     *
     * @return    The CurrentTime value
     */
    public long getCurrentTime() {
        return System.currentTimeMillis();
    }


    /**
     *  Initiates an action for a unit.
     *
     * @param  fromRole  The commanding role which initiated the action.
     * @param  action    The Action class itself.
     */
    public void initiateAction(String fromRole, Action action) {
        QueuedAction qA = new QueuedAction(fromRole, action, 1);
        synchronized (actionQueue) {
            actionQueue.add(qA);
            actionQueue.notifyAll();
        }
    }


    /**
     *  Executes an instantaneous action. An instantaneous action is an action
     *  that can be executed during the execution of another (scheduled) action.
     *  A unit can for example be executing a movement action, and fire a
     *  missile at another unit at the same time. The instantaneous action will
     *  never be queued. There is only one single call made to the <code>
     *  checkPoint()</code> method in the action class.
     *
     * @param  action  The class representing the instantaneous action.
     */
    public void executeInstantaneousAction(Action action) {
        // Synchronize on the queue so that nothing is happening with any units!
        synchronized (actionQueue) {
            // Get the Unit and the UnitType
            Unit u = action.getUnit();
            UnitType uT = u.getUnitType();

            // Save the coordinates for later use
            double oldx = u.getX();
            double oldy = u.getY();
            int oldIntX = (int) oldx;
            int oldIntY = (int) oldy;

            // Reset the update-maps
            u.resetUpdatedActions();
            u.resetUpdatedProperties();

            // Do one iteration of the action!
            ActionReturn actRet = action.checkPoint();

            // Check for changes
            if (u.isDestroyed()) {
                // Unit is destroyed, act!!!
                visibilityManager.deleteUnit(u, oldx, oldy);
                unitManager.deleteUnit(u, oldx, oldy);
            } else {
                // Check if the unit has moved, set new values for properties or
                // blocked one or more actions.
                checkChangedPosition(oldx, oldy, u);
                checkChangedProperties(u);
                checkActionBlocklist(u);
            }
            // Send the unit message, if the user returned one
            String unitMessage = actRet.getUnitMessage();
            if (unitMessage != null) {
                // Send to commander, observers and gods eye roles!
                sendUnitMessageToAll(u, unitMessage);
            }
        }
    }


    /**
     *  This method handles an incoming <code>UnitEvent</code> coming from the
     *  automaton-layer, most probably due to a change of state in the automaton
     *  at position (x, y).
     *
     * @param  x          The x-coordinate of the automaton.
     * @param  y          The y-coordinate of the automaton.
     * @param  unitEvent  The UnitEvent class.
     */
    public void handleAutomatonUnitEvent(int x, int y, UnitEvent unitEvent) {
        // Synchronize on the queue so that nothing is happening with any units!
        synchronized (actionQueue) {
            // Gets an iterator over all units at discrete coordinate position.
            Iterator it = unitManager.getUnitsAtAutomatonPosition(x, y).iterator();
            while (it.hasNext()) {
                Unit u = (Unit) it.next();
                UnitType uT = u.getUnitType();
                double oldx = u.getX();
                double oldy = u.getY();
                int oldIntX = (int) oldx;
                int oldIntY = (int) oldy;
                u.resetUpdatedActions();
                u.resetUpdatedProperties();
                uT.getEventHandler().handleEvent(unitEvent, u);
                // Check for changes...
                if (u.isDestroyed()) {
                    // Unit is destroyed, act!!!
                    visibilityManager.deleteUnit(u, oldx, oldy);
                    unitManager.deleteUnit(u, oldx, oldy);
                } else {
                    checkChangedPosition(oldx, oldy, u);
                    checkChangedProperties(u);
                    checkActionBlocklist(u);
                }
            }
        }
    }


    /**
     *  This method handles an incoming UnitEvent which originates from another
     *  unit in the game.
     *
     * @param  unit       The affected unit.
     * @param  unitEvent  The UnitEvent class.
     */
    public void handleUnitToUnitEvent(Unit unit, UnitEvent unitEvent) {
        // Synch on the queue so that nothing is happening with any units!
        synchronized (actionQueue) {
            UnitType uT = unit.getUnitType();
            double oldx = unit.getX();
            double oldy = unit.getY();
            int oldIntX = (int) oldx;
            int oldIntY = (int) oldy;
            unit.resetUpdatedActions();
            unit.resetUpdatedProperties();
            uT.getEventHandler().handleEvent(unitEvent, unit);
            // Check for changes...
            if (unit.isDestroyed()) {
                // Unit is destroyed, act!!!
                visibilityManager.deleteUnit(unit, oldx, oldy);
                unitManager.deleteUnit(unit, oldx, oldy);
            } else {
                checkChangedPosition(oldx, oldy, unit);
                checkChangedProperties(unit);
                checkActionBlocklist(unit);
            }
        }
    }


    /**
     *  Aborts an ongoing action for a unit.
     *
     * @param  fromRole      The commanding role
     * @param  actionHandle  The actionHandle
     * @return               <code>true</code> if the action was found and
     *      removed, <code>false</code> otherwise.
     */
    public boolean abortAction(String fromRole, int actionHandle) {
        // This method could be optimized, it iterates over all ongoing actions
        // and checks if it is the one we're looking for. Use a HashMap or something
        // instead, mapping (role, actionId) to null or the action class.
        synchronized (actionQueue) {
            Iterator it = actionQueue.iterator();
            while (it.hasNext()) {
                QueuedAction qA = (QueuedAction) it.next();
                if (qA.getAction().getActionHandle() == actionHandle &&
                        qA.getFromRole().equals(fromRole)) {
                    // Gotcha!
                    qA.getAction().getUnit().setActiveAction(null);
                    it.remove();
                    return true;
                }
            }
        }
        return false;
    }


    /**
     *  Main processing method for the execution of actions.
     */
    public void run() {
        while (true) {
            QueuedAction qA = null;
            long nextExecutionTime = 0;

            // Synchronize on the queue.
            synchronized (actionQueue) {
                // Fetch current time!
                long curTime = getCurrentTime();
                // See if there's anything in the queue!
                if (actionQueue.size() > 0) {
                    // Something is in the queue, see if it is due for execution!
                    qA = (QueuedAction) actionQueue.first();
                    if (qA.getTimeToExecute() <= curTime) {
                        // Yes indeed, remove the object from the queue!
                        actionQueue.remove(qA);
                    } else {
                        // Nope, it is not due yet, store the time for execution
                        // and leave it in the queue!
                        nextExecutionTime = qA.getTimeToExecute();
                        qA = null;
                    }
                }
            }
            // If qA isn't null, then we've found an action which is due for execution!
            if (qA != null) {
                // Execute a checkpoint for the action!
                Action act = qA.getAction();
                Unit u = unitManager.getUnit(act.getUnitHandle());
                double oldx = u.getX();
                double oldy = u.getY();
                int oldIntX = (int) oldx;
                int oldIntY = (int) oldy;
                u.resetUpdatedActions();
                u.resetUpdatedProperties();

                ActionReturn actRet = act.checkPoint();
                // Check if things has changed...
                if (u.isDestroyed()) {
                    visibilityManager.deleteUnit(u, oldx, oldy);
                    unitManager.deleteUnit(u, oldx, oldy);
                } else {
                    checkChangedPosition(oldx, oldy, u);
                    checkChangedProperties(u);
                    checkActionBlocklist(u);
                    checkActionReturn(qA, u, actRet);
                }
                // Yield to other threads before continuing.
                Thread.yield();
            } else {
                // Wait for notifications coming from the automaton queue,
                // or 'til it's time to execute the next scheduled action...
                long timeToSleep = nextExecutionTime - getCurrentTime();
                if (timeToSleep <= 0) {
                    timeToSleep = 1;
                }
                synchronized (actionQueue) {
                    try {
                        if (actionQueue.size() == 0) {
                            timeToSleep = 0;
                        }
                        actionQueue.wait(timeToSleep);
                    } catch (InterruptedException e) {
                        // There shouldn't be an InterruptedException, just continue...
                    }
                }
            }
        }
    }


    /**
     *  Adds an action to the action queue, with a given execution time.
     *
     * @param  qA             The action to be added.
     * @param  msToExecution  The time until execution.
     */
    private void addUpdateToQueue(QueuedAction qA, long msToExecution) {
        long timeToExecute = getCurrentTime() + msToExecution;
        synchronized (actionQueue) {
            qA.setTimeToExecute(timeToExecute);
            actionQueue.add(qA);
        }
    }


    /**
     *  Check if a given unit has changed from a known last position. If the
     *  unit has moved, inform the unit manager and visibility manager of the
     *  update.
     *
     * @param  oldx  The last known x coordinate
     * @param  oldy  The last known x coordinate
     * @param  u     The unit.
     */
    private void checkChangedPosition(double oldx, double oldy, Unit u) {
        // Check if the unit has moved!
        if (oldx != u.getX() || oldy != u.getY()) {
            // Yes, it has moved!

            // Has it also moved when using a discrete-valued coordinate system?
            if ((int) oldx != u.getIntX() || (int) oldy != u.getIntY()) {
                // Inform the unitManager of the new position
                unitManager.reportUpdatedUnitPosition(u, (int) oldx, (int) oldy);
            }

            // Inform the visibilityManager of the new position
            visibilityManager.updateUnitPosition(u,
                    oldx,
                    oldy,
                    u.getX(),
                    u.getY());

        }
    }


    /**
     *  Checks the ongoing actions of a given unit and notify the roles
     *  (clients) that commands the role about any updates.
     *
     * @param  u  The unit to check.
     */
    private void checkActionBlocklist(Unit u) {
        // Action blocklist changes...
        Set actions = u.getUpdatedActions();
        if (!actions.isEmpty()) {
            String commandRole = u.getCommander();

            // Send message to the commander for the unit
            UnitType uType = u.getUnitType();
            Iterator it = actions.iterator();
            while (it.hasNext()) {
                String thisAction = (String) it.next();
                boolean isBlocked = u.getBlockedActions().contains(thisAction);
                ActionDefinition actDef = uType.getActionDefinition(thisAction);
                comClient.unitActionBlocked(commandRole,
                        u.getHandle(),
                        actDef,
                        isBlocked);
            }
        }
    }


    /**
     *  Checks all properties of the given unit, and notify all roles (clients)
     *  that observes the unit about any updates. Also notify roles with status
     *  as god's eye.
     *
     * @param  u  The unit to check.
     */
    private void checkChangedProperties(Unit u) {
        // Property changes...
        HashMap hM = u.getUpdatedProperties();
        if (!hM.keySet().isEmpty()) {
            // Send message to all observers for the unit
            String[] obsRoles = unitManager.getRolesObservingUnit(u);
            Set allRoles = new HashSet(unitManager.getGodsEyeRoles());
            for (int i = 0; i < obsRoles.length; i++) {
                allRoles.add(obsRoles[i]);
            }

            Iterator it = allRoles.iterator();
            while (it.hasNext()) {
                String curRole = (String) it.next();
                Iterator itProps = hM.keySet().iterator();
                while (itProps.hasNext()) {
                    String thisKey = (String) itProps.next();
                    String thisValue = (String) hM.get(thisKey);
                    comClient.unitPropertyChanged(curRole,
                            u.getHandle(),
                            thisKey,
                            thisValue);
                }
            }
        }
    }


    /**
     *  Check if a given action, executed by a given unit, is completed, aborted
     *  or still ongoing, and take the necessary steps in each case.
     *
     * @param  qA      The action to check.
     * @param  u       The unit executing the action.
     * @param  actRet  The return from the last checkpoint call.
     */
    private void checkActionReturn(QueuedAction qA, Unit u, ActionReturn actRet) {
        String unitMessage = actRet.getUnitMessage();

        // Is the action completed?
        if (actRet instanceof ActionReturnCompleted) {
            String commandRole = u.getCommander();
            int actionHandle = qA.getAction().getActionHandle();
            if (unitMessage != null) {
                sendUnitMessageToAllButCommander(u, unitMessage);
            }
            comClient.actionCompleted(commandRole, actionHandle, unitMessage);
            u.setActiveAction(null);
        }
        // Is the action aborted?
        else if (actRet instanceof ActionReturnError) {
            String commandRole = u.getCommander();
            int actionHandle = qA.getAction().getActionHandle();
            comClient.actionFailed(commandRole, actionHandle, unitMessage);
            if (unitMessage != null) {
                sendUnitMessageToAllButCommander(u, unitMessage);
            }
            u.setActiveAction(null);
        }
        // Is the action still ongoing?
        else if (actRet instanceof ActionReturnRequeue) {
            addUpdateToQueue(qA, ((ActionReturnRequeue) actRet).getTimeToWakeup());
            if (unitMessage != null) {
                sendUnitMessageToAll(u, unitMessage);
            }
        }
    }


    /**
     *  Sends a message to all observers, except the commander, of a given unit.
     *  Also send to all roles with status as god's eye.
     *
     * @param  unit         The unit.
     * @param  unitMessage  The message to send.
     */
    private void sendUnitMessageToAllButCommander(Unit unit, String unitMessage) {
        int handle = unit.getHandle();
        // Send to all observers minus the commander!
        String commander = unit.getCommander();
        Iterator it = unit.getObservers().iterator();
        while (it.hasNext()) {
            String role = (String) it.next();
            if (!role.equals(commander)) {
                comClient.unitMessage(role, handle, unitMessage);
            }
        }
        // Send to all gods eye roles!
        it = unitManager.getGodsEyeRoles().iterator();
        while (it.hasNext()) {
            String role = (String) it.next();
            comClient.unitMessage(role, handle, unitMessage);
        }
    }


    /**
     *  Sends a message to all observers of a given unit. Also send to all roles
     *  with status as god's eye.
     *
     * @param  unit         The unit.
     * @param  unitMessage  The message to send.
     */
    private void sendUnitMessageToAll(Unit unit, String unitMessage) {
        int handle = unit.getHandle();
        // Send to all observers!
        Iterator it = unit.getObservers().iterator();
        while (it.hasNext()) {
            String role = (String) it.next();
            comClient.unitMessage(role, handle, unitMessage);
        }
        // Send to all gods eye roles!
        it = unitManager.getGodsEyeRoles().iterator();
        while (it.hasNext()) {
            String role = (String) it.next();
            comClient.unitMessage(role, handle, unitMessage);
        }
    }


    /**
     *  Outputs a message to the server log.
     *
     * @param  s  The message to output.
     */
    private static void log(String s) {
        ReferenceHolder.getLogManager().log("ActionProcessor\t: " + s);
    }


    /**
     *  Outputs a debug message to the server log, as well as to the stdout of
     *  the server.
     *
     * @param  s  The message to output.
     */
    private static void debug(String s) {
        ReferenceHolder.getLogManager().debug("ActionProcessor\t: " + s);
        System.out.println(s);
    }

}





