package simulator;


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.io.*;
import java.util.Formatter;


/*
 * Customer.java
 * Created on 2005/1/4
 *
 */

/**
 *  Customer - This class defines the basic customer.  It is abstract because
 *  it does not represent any particular decision strategy.  It simply provides
 *  the framework for handling clock ticks, filling wish lists and actions to take
 *  when getting on/off rides.
 *
 *  @author Ping
 *
 */
public abstract class Customer implements ClockedThing, Cloneable
{
    public static String CUSTOMER_TYPE = "Default";

    // This enumeration defines the possible strategies for deciding when to
    // exit the park.
    // EMPTY_WISH_LIST - Exit the park when the wish list is empty
    // SET_TIME - There is a set time when the user should go home
    public static enum ExitStrategy
    {
        // We can not only create enums, but
        // we are able to associate data and methods with
        // each enumerated value
        // The values within the parens are passed to the constructor
        // of the enum type (see below)
        EMPTY_WISH_LIST ("Empty Wish List"),
        SET_TIME ("Set Time"),
        PARK_CLOSING_TIME ("Park Closing Time");

        // The string representation of the enum name.
        // This is for comparing against string input from files
        public String type;

        // Constructor
        ExitStrategy(String s)
        {
            type = s;
        }
    }

    // This is just a simple structure that contains
    // the record of a visit to a particular ride
    protected class RideEntry
    {
        public String rideName;
        public Time enteredLine;
        public Time enteredRide;
        public Time exitedRide;

        public RideEntry(String name)
        {
            this.rideName = name;
            enteredLine = null;
            enteredRide = null;
            exitedRide = null;
        }
        public RideEntry(String name, Time timeEnteredLine,
                Time timeEnteredRide, Time timeExitedRide)
        {
            rideName = name;
            try
            {
                enteredLine = (Time) timeEnteredLine.clone();
                enteredRide = (Time) timeEnteredRide.clone();
                exitedRide = (Time) timeExitedRide.clone();
            }
            catch(CloneNotSupportedException e)
            {
                e.printStackTrace();
            }
        }

        public RideEntry(RideEntry copy)
        {
            rideName = copy.rideName;
            try
            {
                enteredLine = (Time) copy.enteredLine.clone();
                enteredRide = (Time) copy.enteredRide.clone();
                exitedRide = (Time) copy.exitedRide.clone();
            }
            catch(CloneNotSupportedException e)
            {
                e.printStackTrace();
            }
        }
    }

    // This constant defines how many rides we will refill
    // the wish list by.  When refilling, it will grab the closest
    // number of rides specified by this constant
    protected static final int CLOSEST_RIDES = 15;


    // This defines the possible state that the customer may be in
    // IN_LINE - The customer is in line for a ride
    // IN_TRANSIT - The customer is currently walking towards a destination
    // ON_RIDE - The customer is currently on a ride (or being loaded on one)
    // WAITING - The customer has not decided what to do yet
    // ARRIVED - The customer has arrived at the destination and waiting
    //  for further instructions
    protected enum CustomerStatus {IN_LINE, IN_TRANSIT, ON_RIDE, WAITING,
        ARRIVED}

    // The name of the customer
    protected String name;

    // This hash map stores the name of each ride with a corresponding
    // boolean represeting whether or not the ride has been visited.
    protected HashMap<String, Boolean> ridesVisited;
    
    // This queue stores the ride entries of the rides the customer has been
    // on. We don't need to be able to access any particular ride. Used for 
    // printing the statistics of the ride visits in the order that they were 
    // visited.
    protected MyQueue<RideEntry> rideList;
    
    // This hashtable stores the original wish list. The key is the ride name 
    // and the value is the vector of the priorities of desired visits to 
    // the ride.
    protected Hashtable<String, ArrayList<Integer>> originalWishList;
    
    // This stores the number of rides on the original wish list that
    // was visited.
    protected int numOfOriginalRidesVisited;

    // This is the actual agenda with the rides ordered by priority
    protected MyPriorityQueue<Attraction, Integer> agenda;

    // This stores the current ride that the customer is on
    protected RideEntry currRide;

    // This specifies what the customer is currently doing
    protected CustomerStatus customerStatus;

    // These variables store where the customer is and where he is going
    protected Location currLocation;
    // this stores the attraction that the customer is heading towards.
    // If the customer is headed towards the exit, this is set to null
    protected Attraction destinationAttraction;
    protected Location destination;

    // This keeps track of the number of minutes remaining until
    // the customer reaches the destination
    protected int travelTimeRemaining;

    // This stores the total number of minutes spent in the park
    protected int totalTimeInPark;
    // This stores the total number of minutes spent in line
    protected int totalTimeInLine;
    // This stores the total number of minutes spent on rides
    protected int totalRideTime;

    // this stores the current time
    // Note: This should just be a reference and not touched.
    protected Time time;
    // This is the time that the customer entered the park
    protected Time timeEntered;

    // This is the desired time for the customer to exit
    protected Time desiredExitTime;

    // Keep a reference of the park and the logger
    protected AmusementPark park;
    protected Logger logger;
    protected boolean isInPark;
    protected ExitStrategy exitStrategy;

    /**
     * Note that the desiredExitTime may be null if the exit
     * strategy is either when the wish list is empty or when
     * the park closes
     *
     * @param name
     * @param wishList
     * @param exitStrategy
     * @param timeEntered
     * @param desiredExitTime
     * @param logger
     */
    public Customer(String name,
            Collection<PrioritizedElement<String, Integer>> wishList,
            ExitStrategy exitStrategy, 
            Time timeEntered,
            Time desiredExitTime)
    {
    	this.name = name;
        this.exitStrategy = exitStrategy;
        this.timeEntered = timeEntered;
        this.desiredExitTime = desiredExitTime;        
        
        rideList = new MyQueue<RideEntry>();
        this.agenda = new MyPriorityQueue<Attraction, Integer>();

        park = AmusementParkSimulation.park;
        
        
        // put all of the ride names in visitedRides, each mapping to
        // false because they have not been visited   
        
        Iterator<Attraction> rides = park.getAllRides();
        Attraction nextRide;
        
        ridesVisited = new HashMap<String, Boolean>(AttractionReader.numAttractions);
        
        while(rides.hasNext())
        {
        	nextRide = rides.next();
        	ridesVisited.put(nextRide.getName(), false);
        }
        
        
        // Populate the wish list
        
        originalWishList = new Hashtable<String, ArrayList<Integer>>();
       
        for(PrioritizedElement<String, Integer> e : wishList)
        {
            ArrayList<Integer> visits = originalWishList.get(e.getElement());
         
            if(visits == null)
            {
                visits = new ArrayList<Integer>();
            }
                 
            // Taking prioritized element e from the wishlist given:
            // Integeter priority - add its priority to the vector of visits 
            // String name - then add that vector to the original wishlist, under the ride name
        	visits.add(e.getPriority());
            originalWishList.put(e.getElement(), visits);  	
        }
    }

    /*
     * This function fills the agenda with rides from the park.
     * It first fills the list with rides that the customer has not
     * been to, then fills it up with the closest X rides
     */
    protected void fillAgenda()
    {
        Iterator<Attraction> rides = park.getAllRides();
        String rideName;
        Attraction ride;

        while(rides.hasNext())
        {
            ride = rides.next();
            rideName = ride.getName();

            if(!ridesVisited.get(rideName))
            {
                // have not visited this ride
                agenda.enqueue(ride,
                        currLocation.distance(ride.getEntrance()));
            }
        }

        if(agenda.isEmpty())
        {
            // need more rides to fill the agenda, so have to grab
            // the closest rides
            MyPriorityQueue<PrioritizedElement<Attraction, Integer>, Integer>
                closeRides = new MyPriorityQueue<
                PrioritizedElement<Attraction, Integer>, Integer>();

            rides = park.getAllRides();
            while(rides.hasNext())
            {
                // sort all the rides by their distances, closest rides first
                ride = rides.next();

                int distance = currLocation.distance(ride.getEntrance());
                closeRides.enqueue(new PrioritizedElement<Attraction, Integer>(
                        ride, distance), distance);
            }
            while(!closeRides.isEmpty() && agenda.size() < CLOSEST_RIDES)
            {
                // now grab the closest few rides
                PrioritizedElement<Attraction, Integer> e = closeRides.dequeue();
                agenda.enqueue(e.getElement(), e.getPriority());
            }
        }
    }

    /**
     * This function should be called when the customer first arrives
     * at a particular ride
     * @param rideName
     */
    protected void arrivedAtRide(Attraction ride)
    {
        numOfOriginalRidesVisited++;
    	
    	currRide = new RideEntry(ride.getName());
        try
        {
            currRide.enteredLine = (Time) time.clone();
        }
        catch(CloneNotSupportedException e)
        {
            e.printStackTrace();
        }

        // This may not be necessary, but just for clarity
        currLocation = ride.getEntrance();
        travelTimeRemaining = 0;

        boolean isSuccessful = ride.enqueue(this);

        if(!isSuccessful)
        {
            // the ride is closed, so pick another ride
            agenda.dequeue();
            currRide = null;
            customerStatus = CustomerStatus.WAITING;
        }
        else
        {
            // tell the park that the customer has arrived
            // this removes the customer from the transit list
            park.customerArrivedAtDestination(this);
            customerStatus = CustomerStatus.IN_LINE;

            // log the event
            logger.arriveAtAttraction(this, ride);
        }
    }

    /**
     * This function determines if it is time for the customer to
     * head towards the exit.
     * @return True if it is time to head for the exit, false otherwise.
     */
    @SuppressWarnings("incomplete-switch")
    protected boolean isTimeToGoHome()
    {
        boolean timeToGo = false;
        
        // the current time is after when the park closes
        if(time.after(park.getClosingTime()))
        {
            timeToGo = true;
        }
        else
        {
            switch(exitStrategy)
            {
                case EMPTY_WISH_LIST:
                    if(agenda.isEmpty())
                    {
                        timeToGo = true;
                    }
                    break;
                case SET_TIME:
                    // the current time is after the desired go home time
                    if(time.after(desiredExitTime))
                    {
                        timeToGo = true;
                    }
                    break;
            }
        }
        return timeToGo;
    }

    /**
     * This function sends the customer towards the exit
     * @param exit
     */
    protected void goToExit()
    {
        Location exit = park.getExit();
        
        // time to go home, set the destination as the exit
        destination = exit;
        destinationAttraction = null;
        
        travelTimeRemaining = currLocation.distance(exit);
        customerStatus = CustomerStatus.IN_TRANSIT;
        logger.nextAttraction(this, null);
    }

    /**
     * This function should adjust the priority queue appropriately.
     * This adjustment is based on the specific customer implementation.
     * This function will also fill up the wish list if it is currently empty.
     *
     * Note all subclasses should call this function before doing anything else.
     *
     */
    protected void selectRide()
    {
        if(agenda.isEmpty())
        {
            fillAgenda();
        }
    }

    /**
     * @return Returns the name of the customer
     */
    public String getName()
    {
        return name;
    }

    /**
     * @return Returns the minutes remaining before reaching
     * the current destination.
     */
    public int getTravelTimeRemaining()
    {
        return travelTimeRemaining;
    }

    /**
     * @return Returns the customer's status.
     */
    public CustomerStatus getCustomerStatus()
    {
        return customerStatus;
    }

    /**
     * @return Returns the timeEntered.
     */
    public Time getTimeEntered()
    {
        return timeEntered;
    }

    /**
     * @return Returns the total number of minutes spent on rides
     */
    public int getTotalRideTime()
    {
        return totalRideTime;
    }

    /**
     * @return Returns the total number of minutes spent in the park
     */
    public int getTotalTimeInPark()
    {
        return totalTimeInPark;
    }

    /**
     * @return Returns the total number of minutes spent in waiting line.
     */
    public int getTotalTimeInLine()
    {
        return totalTimeInLine;
    }

    public void enterPark(AmusementPark newPark, Time newTime,
    	Location newEntrance, Logger newLogger)
    {
        park = newPark;
        time = newTime;
        logger = newLogger;
        currLocation = newEntrance;

        currRide = null;
        customerStatus = CustomerStatus.WAITING;
        travelTimeRemaining = 0;

        totalTimeInPark = 0;
        totalTimeInLine = 0;
        totalRideTime = 0;
        numOfOriginalRidesVisited = 0;
        isInPark = true;

        // now load up the agenda
        Set<Map.Entry<String, ArrayList<Integer>>> entries =
            originalWishList.entrySet();

        for(Map.Entry<String, ArrayList<Integer>> e : entries)
        {
            // iterate through each entry in the original wish list
            // Note: in the agenda, we store a reference to
            // the actual ride as opposed to just the string name of the
            // ride.  This is for efficiency reasons since we need to
            // access the rides' time estimation, entrance location and other
            // functions.
            Attraction a = park.getAttraction(e.getKey());
            if(a == null)
            {
                System.err.println("* Encountered unknown ride " +
                        e.getKey());
            }
            else
            {
                ArrayList<Integer> visits = e.getValue();
                for(Integer priority : visits)
                {
                    agenda.enqueue(a, priority);
                }
            }
        }

        destination = null;
        destinationAttraction = null;
    }

    /**
     * This function is called when the park is closing.  Any customer
     * thats not in line or already on a ride should start heading towards the
     * exit
     *
     */
    public void parkClosing()
    {
        if(customerStatus == CustomerStatus.IN_TRANSIT ||
           customerStatus == CustomerStatus.WAITING)
        {
            goToExit();
        }
    }

    /**
     * This function is called when the customer exits a ride
     * @param ride The ride the customer just exited from.
     */
    public void exitRide(Attraction ride)
    {    	
    	try
        {
            currRide.exitedRide = (Time) time.clone();
        }
        catch(CloneNotSupportedException e)
        {
            e.printStackTrace();
        }
        
        ridesVisited.put(currRide.rideName, true);
        rideList.enqueue(currRide);
    
        
        // remove the ride that we just got out of from the agenda
        agenda.dequeue();
        currRide = null;

        currLocation = ride.getExit();
        customerStatus = CustomerStatus.WAITING;
        destinationAttraction = null;
        
        // The customer has exited the ride and should be put on the
        // in transit list.
        park.customerInTransit(this);
        
        // log the event that the customer exited the ride
        logger.exitAttraction(this, ride);
    }

    /**
     * This function is called when the user boards a ride
     * @param Ride
     */
    public void boardRide(Attraction ride)
    {
        try
        {
            currRide.enteredRide = (Time) time.clone();
        }
        catch(CloneNotSupportedException e)
        {
            e.printStackTrace();
        }
        
        customerStatus = CustomerStatus.ON_RIDE;
        
        // log that the customer got on the ride
        logger.boardAttraction(this, ride);
    }

    /**
     * This function is called every clock tick
     * @return boolean - This function will return true if the the customer is still in the park
     * and should be notified of the clock ticks.  When it return false, that signifies that the
     * customer has left the park
     */
    public boolean onClockTick()
    {
        totalTimeInPark++;
        switch(customerStatus)
        {
            case IN_LINE:
                // the customer is in line.  Currently all he can do is wait.
                totalTimeInLine++;
                break;
            case ON_RIDE:
                totalRideTime++;
                break;
            // We handle both in transit and waiting together.
            // If the customer is waiting, he/she makes a decision
            // and immediately starts the travel.
            case IN_TRANSIT:
            case WAITING:
            {
                // sends the customer to the exit if he/she is not
                // already heading towards it
                if(isTimeToGoHome() &&
                   (destination == null || !destination.equals(park.getExit())))
                {
                    goToExit();
                }
                else
                {
                    if(customerStatus == CustomerStatus.WAITING)
                    {
                        // re-shuffle the agenda such that the
                        // ride we want to go to is in front
                        selectRide();

                        Attraction ride = agenda.peek();
                        destination = ride.getEntrance();
                        destinationAttraction = ride;
                        travelTimeRemaining = currLocation.distance(destination);
                        customerStatus = CustomerStatus.IN_TRANSIT;

                        logger.nextAttraction(this, destinationAttraction);
                    }
                    // the customer has moved
                    travelTimeRemaining--;
                    currLocation = currLocation.moveTowards(destination);
                    if(travelTimeRemaining <= 0)
                    {
                        // the customer has arrived at the destination
                        // Don't process now, wait till the next clock cycle
                        customerStatus = CustomerStatus.ARRIVED;
                    }
                }
                break;
            }
            case ARRIVED:
            {
                currLocation = destination;
                // arrived at some destination
                if(destinationAttraction == null)
                {
                    // got to the exit
                    isInPark = false;
                }
                else
                {
                    // got to some ride
                    arrivedAtRide(destinationAttraction);
                }
                break;
            }
        }
        return isInPark;
    }

 
    public Object clone() throws CloneNotSupportedException
    {
        Customer clone = (Customer) super.clone();
        
        // full deep clone of the hashmap
        
        clone.ridesVisited = new HashMap<String, Boolean>();
        
        Set<String> keys = this.ridesVisited.keySet();
        for(String key : keys)
        {
        	Boolean isVisited = this.ridesVisited.get(key);
        	
        	clone.ridesVisited.put(key, isVisited);
        }
        
        
        // This clone is not a full deep clone, but it is ok.
        // Strings are not modifiable, and we are only reading from
        // this list, never modifying it.
        clone.originalWishList =
        	new Hashtable<String, ArrayList<Integer>>(this.originalWishList);

        clone.agenda = this.agenda.clone();

        return clone;
    }
  


    /**
     * This function prints out the various stats of the customer,
     * such as the rides visited and the total amount of time spent
     * on various activities.
     *
     */
    public void printStats()
    {
    	System.out.println("-------------------------------------------------------------------------------");
        System.out.println("Customer: " + name + "\n");
    	
    	// Establish format to be used when printing
        String format = "%-30s | %-13s | %-13s | %-13s%n";
        System.out.printf(format, "Ride Name", "Time Entered", "Time Boarded",
        "Time Exited\n");        
        
        // Print every ride entry in the queue rideList using the format from above.
        // Because we are using a queue, we have kept the rides ordered by the time
        // the customer went on them, and printing them in this order.
        MyQueue<RideEntry> rideListCopy = rideList.clone();
        
        while(!rideListCopy.isEmpty())
        {
        	RideEntry r = rideListCopy.dequeue();
        	
        	System.out.printf(format, r.rideName, r.enteredLine.toString(),
                    r.enteredRide.toString(), r.exitedRide.toString());
        }

        // Print additional statistics about overall time spent doing different activities.
    	System.out.printf(	"\n%5d minutes total time in park\n" +
        					"%5d minutes spent in line\n" +
        					"%5d minutes spent on rides\n",
        					totalTimeInPark, totalTimeInLine, totalRideTime);

    	// Calculate percentage of rides on wishlist visited, then print the percentage.
        float visitedWishListRatio = 0;
        if(originalWishList.size() != 0)
        {
            visitedWishListRatio = (float)numOfOriginalRidesVisited /
                originalWishList.size();
        }

    	System.out.printf("%nPercentage of rides on wishlist visited: %.2f%%%n%n",
                visitedWishListRatio * 100);
        System.out.println("-------------------------------------------------------------------------------");

    }



    public void writeStatsToFile(FileWriter outfile)
    {
		Formatter formatter = new Formatter(outfile);

		String format = "%-30s | %-15s | %-15s | %-15s%n";

		formatter.format("--------------------------------------------------------------------------------%n");
		formatter.format("%nCustomer: %s%n%n", name);

		formatter.format(format, "Ride Name", "Time Entered", "Time Boarded",
				"Time Exited");

		formatter.format("%n");

		
		MyQueue<RideEntry> rideListCopy = rideList.clone();
        
        while(!rideListCopy.isEmpty())
        {
        	RideEntry r = rideListCopy.dequeue();
        	
        	formatter.format(format, r.rideName, r.enteredLine.toString(),
                    r.enteredRide.toString(), r.exitedRide.toString());
        }
		
		formatter.format("%n%5d minutes total time in park", totalTimeInPark);
		formatter.format("%n%5d minutes spent in line", totalTimeInLine);
		formatter.format("%n%5d minutes spent on rides%n", totalRideTime);

		float visitedWishListRatio = 0;

		if(originalWishList.size() != 0)
		{
			visitedWishListRatio = (float)numOfOriginalRidesVisited /
				originalWishList.size();
		}

		formatter.format("%nPercentage of rides on wishlist visited: %.2f%%%n%n",
				visitedWishListRatio * 100);
	}
}
