package SimkitOpenDis7Examples;

import edu.nps.moves.dis7.enumerations.DisPduType;
import edu.nps.moves.dis7.pdus.EntityStatePdu;
import edu.nps.moves.dis7.utilities.DisChannel;
import edu.nps.moves.dis7.utilities.PduFactory;
import java.util.SortedSet;
import java.util.TreeSet;
import simkit.Priority;
import simkit.SimEntityBase;

/**
 * Add DIS outputs to TwoCraneBerths simkit simulation
 * @see TwoCraneBerths
 * @see SimkitOpenDis7Examples.run.RunTwoCranesBerth
 * @see SimkitOpenDis7Examples.run.RunTwoCranesBerthOpenDis7
 * @see <a href="run/RunTwoCranesBerthOpenDis7Log.txt">RunTwoCranesBerthOpenDis7Log.txt</a>
 * @see <a href="TwoCraneBerthsAssignment05.docx">TwoCraneBerthsAssignment05.docx</a>
 * @see <a href="TwoCraneBerthsAssignment05Solution.docx">TwoCraneBerthsAssignment05Solution.docx</a>
 * @author abuss@nps.edu@nps.edu
 * @author brutzman@nps.edu
 */
public class TwoCraneBerthsOpenDis7 extends SimEntityBase
{
    private final DisChannel disChannel = new DisChannel();

    /**
     * Queue of Ships waiting to go into the berth
     */
    protected SortedSet<Ship> queue;

    /**
     * Contains 0, 1, or two Ships being unloaded
     */
    protected SortedSet<Ship> berth;

    /**
     * Time in the system for each Ship
     */
    protected double timeInSystem;

    /**
     * Delay in the queue for each Ship
     */
    protected double delayInQueue;

    /**
     * Instantiate queue and berth containers
     */
    public TwoCraneBerthsOpenDis7() {
        this.queue = new TreeSet<>();
        this.berth = new TreeSet<>();
    }

    /**
     * Clear queue and berth containers
     */
    @Override
    public void reset() {
        super.reset();
        queue.clear();
        berth.clear();
        timeInSystem = Double.NaN;
        delayInQueue = Double.NaN;
    }

    /**
     * Only PropertyChangeEvents
     */
    public void doRun() {
        firePropertyChange("queue", getQueue());
        firePropertyChange("berth", getBerth());
        firePropertyChange("timeInSystem", getTimeInSystem());
        firePropertyChange("delayInQueue", getDelayInQueue());
    }

    /**
     * Add the given Ship to queue<br>
     * If berths is empty, schedule StartUnloadingTwoCranes<br>
     * If berths has 1 Ship, schedule SwitchToOneCrane
     *
     * @param ship Given Ship arriving to harbor
     */
    public void doArrival(Ship ship) {

        ship.stampTime();

        SortedSet<Ship> oldQueue = getQueue();
        queue.add(ship);
        firePropertyChange("queue", oldQueue, getQueue());

        if (berth.isEmpty()) {
            waitDelay("StartUnloadingTwoCranes", 0.0, Priority.HIGH);
        }

        if (berth.size() == 1) {
            waitDelay("SwitchToOneCrane", 0.0);
        }
    }

    /**
     * Remove the first Ship from queue<br>
     * DelayInQueue is elapsedTime of that Ship<br>
     * Add the ship to berth container<br>
     * Schedule EndUnloadingTwoCranes at half the remaining time
     */
    public void doStartUnloadingTwoCranes() {
        SortedSet<Ship> oldQueue = getQueue();
        Ship ship = queue.first();
        queue.remove(ship);
        firePropertyChange("queue", oldQueue, getQueue());

        delayInQueue = ship.getElapsedTime();
        firePropertyChange("delayInQueue", getDelayInQueue());

        ship.stampTime();

        SortedSet<Ship> oldBerth = getBerth();
        berth.add(ship);
        firePropertyChange("berth", oldBerth, getBerth());

        waitDelay("EndUnloadingTwoCranes", 0.5 * ship.getRemainingUnloadingTime());
    }

    /**
     * Remove the (one) Ship from berth<br>
     * TimeInSystem is the age of the Ship
     */
    public void doEndUnloadingTwoCranes() {
        SortedSet<Ship> oldBerth = getBerth();
        Ship ship = berth.first();
        berth.remove(ship);
        firePropertyChange("berth", oldBerth, getBerth());

        timeInSystem = ship.getAge();
        firePropertyChange("timeInSystem", getTimeInSystem());
    }

    /**
     * This event is when a Ship arrives to find only one other Ship being
     * unloaded.<br>
     * Credit the ship in the berth with work at a rate of 2 (since 2 cranes
     * have been unloading it<br>
     * Interrupt EndUnloadingTwoCranes<br>
     * Schedule EndUnloadingOneCrane with the Ship already in the berth<br>
     * Schedule StartUnloadingOneCrane
     */
    public void doSwitchToOneCrane() {
        Ship ship = berth.first();
        ship.work(2.0);
        ship.stampTime();

        interrupt("EndUnloadingTwoCranes");

        waitDelay("EndUnloadingOneCrane", ship.getRemainingUnloadingTime(), ship);

        waitDelay("StartUnloadingOneCrane", 0.0, Priority.HIGH);
    }

    /**
     * Pop the first Ship from the queue<br>
     * delayInQueue is elapsedTime (from Arrival event)<br>
     * Add that Ship to berth container<br>
     * Schedule EndUnloadingOneCrane with that Ship
     */
    public void doStartUnloadingOneCrane() {
        SortedSet<Ship> oldQueue = getQueue();
        Ship ship = queue.first(); // TODO rename ship queue
        queue.remove(ship);
        firePropertyChange("queue", oldQueue, getQueue());

        delayInQueue = ship.getElapsedTime();
        firePropertyChange("delayInQueue", getDelayInQueue());

        ship.stampTime();

        SortedSet<Ship> oldBerth = getBerth();
        berth.add(ship);
        firePropertyChange("berth", oldBerth, getBerth());
        
        performCraneUnloadOperations( 10, // numberOfContainers
                                       0.0  // initial position
                                     );    // TODO indicate berth

        waitDelay("EndUnloadingOneCrane", ship.getRemainingUnloadingTime(), ship);
    }
    
    PduFactory pduFactory = new PduFactory();
    public void performCraneUnloadOperations(
                    // which crane
                    // which berth
                    int numberOfContainers, 
                    double positionOfCrane
                    // lenthOfCrane
                )
    {
        double   craneLinearSpeed  =  1.0; // m/sec
        double   craneRotationRate = 10.0; // degrees/second
        double containerHookupTime = 60.0; // seconds average
        double containerDetachTime = 30.0; // seconds average
        
        EntityStatePdu espduCrane = (EntityStatePdu) pduFactory.createPdu(DisPduType.ENTITY_STATE);
        // 1. Crane at head of pier, operatior present, boom elevated vertically in stow postion
        // espduCrane1.setPosition 0 0 0
                
        // 2, Ship arrives, tug departs
        // shop PDU here or assum it happened earlier
        
        // 3. Crane moves to position of ship
        espduCrane.setEntityLocation(positionOfCrane, 0.0, 0.0); 
        // TODO compute travel time
        // update: travel to positionOfCrane by craneLinearSpeed
        
        // 4. repeat until done: Crane rotates, lift container, rotates, lower container
        for (int containerIndex = 1; containerIndex <= numberOfContainers; containerIndex++)
        {
            // perform one operation
            espduCrane.setEntityOrientation(0, 0, 0);
            disChannel.sendSinglePdu(espduCrane);
            espduCrane.setEntityOrientation(0, 0, 0);
            double rotationDelay = 90.0 / craneRotationRate; 
            disChannel.sendCommentPdu(disChannel.COMMENTPDU_NARRATIVE, "Hooking up Container " + containerIndex + " to crane");
            disChannel.sendSinglePduDelayed(containerHookupTime, espduCrane);
            disChannel.sendCommentPdu(disChannel.COMMENTPDU_NARRATIVE, "Container hooked up");
            disChannel.sendSinglePduDelayed(containerHookupTime, espduCrane);
            
            espduCrane.setEntityOrientation(90, 0, 0);
            disChannel.sendSinglePduDelayed(rotationDelay, espduCrane);

            // unhook
            // rotate back

            // if this container is special, meaning on watchlist, report it
        }
        
        // 4. Crane stows boom in vertical position
        // espcudCrnet orientation
        
        // 5. Crane returns to head of pier
        // update: travel from positionOfCrane to head by craneLinearSpeed
        espduCrane.setEntityLocation(positionOfCrane, 0.0, 0.0); 
        disChannel.sendSinglePdu(espduCrane);
    }

    /**
     * Remove given Ship from berth<br>
     * If Ships in queue, schedule StartUnloadingOneCrane<br>
     * If queue is empty, schedule SwitchToTwoCranes<br>
     * timeInSystem is age of Ship
     * 
     * @param ship Given Ship
     */
    public void doEndUnloadingOneCrane(Ship ship) {
        SortedSet<Ship> oldBerth = getBerth();
        berth.remove(ship);
        firePropertyChange("berth", oldBerth, getBerth());

        if (queue.size() > 0) {
            waitDelay("StartUnloadingOneCrane", 0.0, Priority.HIGH);
        }

        if (queue.isEmpty() && berth.size() == 1) {
            waitDelay("SwitchToTwoCranes", 0.0);
        }

        timeInSystem = ship.getAge();
        firePropertyChange("timeInSystem", getTimeInSystem());
    }

    /**
     * Credit the work of the remaining Ship in berth at unit rate<br>
     * Interrupt EndUnloadingOneCrane<br>
     * Schedule EndUnloadingTwoCranes at double the rate (i.e., half the remaining time)
     */
    public void doSwitchToTwoCranes() {
        Ship ship = berth.first();
        ship.work(1.0);
        ship.stampTime();

        interrupt("EndUnloadingOneCrane", ship);

        waitDelay("EndUnloadingTwoCranes",
                0.5 * ship.getRemainingUnloadingTime());
    }

    /**
     * Get tree of sorted Ship set queue
     * @return Shallow copy of queue
     */
    public SortedSet<Ship> getQueue() {
        return new TreeSet<>(queue);
    }

    /**
     * Get tree of sorted Ship set berths
     * @return Shallow copy of berth
     */
    public SortedSet<Ship> getBerth() {
        return new TreeSet<>(berth);
    }

    /**
     * accessor method to get a state variable
     * @return The timeInSystem
     */
    public double getTimeInSystem() {
        return timeInSystem;
    }

    /**
     * accessor method to get a state variable
     * @return The delayInQueue
     */
    public double getDelayInQueue() {
        return delayInQueue;
    }
}