package simulator;

/*
 * Attraction.java
 * Created on 2005/1/3
 *
 */

import java.util.LinkedList;
import java.util.ListIterator;
import java.io.*;
import java.util.Formatter;

/**
 *  Attraction - This is the base class for all types of attractions.  It
 *  contains the basic variables needed for statistics and ride implementation.
 *  However, ride specific information such as when to dispatch cars and when to
 *  add/remove cars are left for the implementing sub-classes.
 *  @author Ping
 *
 */
public abstract class Attraction implements ClockedThing, Cloneable
{
    public static String ATTRACTION_TYPE = "Default";

    /*
     * This internal class represents the cars in the attraction.  Each car
     * holds the customers that are on the car.
     *
     */
    protected class Car implements ClockedThing
    {

        // this stores the status of the car
        private CarStatus carStatus;
        // This stores how many minutes this car has spent in the current
        // activity (either in the ride, waiting for passengers, or
        // loading).
        private int minutesSpent;

        // this stores the actual customers on the car
        private Customer[] customers;
        private int numRiders;

        public Car()
        {
            carStatus = CarStatus.CAR_WAITING;
            customers = new Customer[carCapacity];
            numRiders = 0;
        }

        /**
         * This is a copy constructor, it will make a deep copy of everything
         * @param copy
         */
        public Car(Car copy)
        {
            this.carStatus = copy.carStatus;
            this.minutesSpent = copy.minutesSpent;
            this.numRiders = copy.numRiders;
            customers = new Customer[copy.customers.length];
            try
            {
                for(int i = 0; i < numRiders; i++)
                {
                    // only copy over if there are actually riders
                    customers[i] = (Customer)copy.customers[i].clone();
                }
            }
            catch(CloneNotSupportedException e)
            {
                e.printStackTrace();
            }
        }

        /*
         * This function returns true if the car is empty
         */
        public boolean isCarEmpty()
        {
            return numRiders == 0;
        }

        /*
         * This function returns true if the car is full
         */
        public boolean isCarFull()
        {
            return numRiders == customers.length;
        }

        /*
         * This function returns true if the car is ready to be dispatched
         */
        public boolean isCarReady()
        {
            return carStatus == CarStatus.CAR_READY;
        }

        /*
         * This function returns true if the car is waiting to be loaded
         */
        public boolean isCarWaiting()
        {
            return carStatus == CarStatus.CAR_WAITING;
        }

        /*
         * This function returns true if the car is on the ride
         */
        public boolean isCarOnRide()
        {
            return carStatus == CarStatus.CAR_ON_RIDE;
        }

        public boolean isCarDone()
        {
            return carStatus == CarStatus.CAR_DONE;
        }

        /*
         * This function returns true if the car is loading
         */
        public boolean isCarLoading()
        {
            return carStatus == CarStatus.CAR_LOADING;
        }

        /*
         * This function returns the number of riders on the car
         */
        public int getNumRiders()
        {
            return numRiders;
        }

        /*
         * This function gets the number of minutes spent on the current activity
         */
        public int getMinutesSpent()
        {
            return minutesSpent;
        }

        /*
         * This function loads the array of customers onto the car
         * and sets the car to the loading status.
         */
        public void loadCustomers(Customer[] boardingCustomers)
        {
            if(boardingCustomers.length > customers.length)
            {
                throw new IllegalArgumentException("Unable to load more customers than" +
                		" the maximum number of riders allowed per car.");
            }

            for(int i = 0; i < boardingCustomers.length; i++)
            {
                customers[i] = boardingCustomers[i];

                if(!isSimulation)
                {
                    customers[i].boardRide(Attraction.this);
                }
            }
            // update the number of riders
            currNumOfRiders += boardingCustomers.length;
            totalRiders += boardingCustomers.length;
            if(currNumOfRiders > maxNumOfRiders)
            {
                maxNumOfRiders = currNumOfRiders;
            }

            numRiders = boardingCustomers.length;
            // if there are no load time for this ride, then go straight to
            // ready state
            if(loadingTime == 0)
            {
                carStatus = CarStatus.CAR_READY;
            }
            else
            {
                carStatus = CarStatus.CAR_LOADING;
            }
            minutesSpent = 0;
        }
        /*
         * This function unloads all the customers on the ride and
         * sends them to the exit
         */
        public void unloadCustomers()
        {
            for(int i = 0; i < numRiders; i++)
            {
                if(!isSimulation)
                {
                    customers[i].exitRide(Attraction.this);
                }
                customers[i] = null;
            }
            // update the number of riders
            currNumOfRiders -= numRiders;
            numRiders = 0;
            carStatus = CarStatus.CAR_WAITING;
            // reset the time
            minutesSpent = 0;
        }

        public void dispatchCar()
        {
            // only cars that are loaded and ready can be dispatched
            if(carStatus == CarStatus.CAR_READY)
            {
	            carStatus = CarStatus.CAR_ON_RIDE;
	            minutesSpent = 0;
            }
        }

        /*
         * This function updates the car's status
         */
        public boolean onClockTick()
        {
            minutesSpent++;
            // only cars on the ride already or those being loaded need to
            // be updated.
            if(carStatus == CarStatus.CAR_ON_RIDE &&
               minutesSpent >= rideLength)
            {
                // the ride has finished, unload everyone
                carStatus = CarStatus.CAR_DONE;
                minutesSpent = 0;
            }
            else if(carStatus == CarStatus.CAR_LOADING &&
                    minutesSpent >= loadingTime)
            {
                // The car has been loaded and can be dispatched
                carStatus = CarStatus.CAR_READY;
                minutesSpent = 0;
            }
            return true;
        }
    }

    // Enumeration to represent the state of the ride
    protected enum AttractionStatus {OPEN, CLOSED}

    // This enum specifies the status of the car.  Note that it cannot be
    // declared inside the Car class since it is an inner class.
    // CAR_WAITING is when the car is done and waiting to be loaded
    // CAR_ON_RIDE is when the car has been dispatched and is on the ride
    // CAR_LOADING is when we are loading the car with passengers
    // CAR_READ    is when the car has been loaded and is ready to be dispatched
    // CAR_DONE    is when the car just finished the ride
    protected enum CarStatus {CAR_WAITING, CAR_ON_RIDE, CAR_LOADING, CAR_READY, CAR_DONE}

    protected AttractionStatus attractionStatus;

    // local variables for storing general information about an attraction
    protected String name;
    protected Location entrance;
    protected Location exit;
    //protected int numOfCars = 0;
    protected int maxNumOfCarsPossible;
    protected int carCapacity;

    protected int loadingTime;
    protected int rideLength;

    // we use a linked list to implement a queue of the riders in line
    protected LinkedList<Customer> waitingLine;
    // This list stores the entering time of each customer so that we can
    // determine the waiting time of each customer
    protected LinkedList<Integer> waitingTime;
    // This link list represents the cars that are active in the attraction
    protected LinkedList<Car> cars;

    // Variables for storing the statistics
    // The number of ticks since the ride openned
    protected int numOfTicks;
    // the max number of riders in this attraction (actually riding it)
    protected int maxNumOfRiders;
    // current number of riders
    protected int currNumOfRiders;
    // the max length of the waiting line
    protected int maxLineLength;
    // the max/min number of cars in operation
    protected int maxNumOfCars;
    protected int minNumOfCars;
    // the total number of people who have been on this attraction
    protected int totalRiders;

    // These values are used to compute the averages
    // these values should be updated properly on each clock tick in
    // order for the averages to be correct
    protected int totalRidersForAllTicks;
    protected int totalCarsForAllTicks;
    protected int totalLineLengthForAllTicks;
    protected int totalWaitingTime;

    // this flag is to denote whether or not the attraction is in "simulation"
    // mode.  If it is in simulation mode, it will not make calls to
    // inform customers of anything
    protected boolean isSimulation = false;

    protected int estimatedWaitTime;
    protected int timeSinceEstimate;
    protected final static int TIME_UNTIL_NEW_ESTIMATES = 10;

    // This variable stores whether or not the ride should be processed
    // on any given clock tick.  The ride should not be processed
    // if the ride is closed and there are no body in line/on the ride.
    protected boolean needProcessing;

    /**
     * This is the constructor for the attraction class. Note that this is protected so
     * that outside classes cannot make an instance of this class.  It simply initializes
     * some of the variables.
     *
     * @param attractionName
     * @param entrance
     * @param exit
     * @param initialNumOfCars
     * @param maxNumOfCarsPossible
     * @param carCapacity
     * @param rideTime
     * @param loadingTime
     */
    protected Attraction(String attractionName, Location entrance, Location exit,
            int initialNumOfCars, int maxNumOfCarsPossible, int carCapacity, int rideTime,
            int loadingTime)
    {
        // Note that some initialization we use the "this" some we don't.  We only need
        // to use "this" if there could be confusion in the name (if two variables have the same
        // name, the variable with "this" is the variable belonging to the class while
        // the one without is passed in from the parameter).
        name = attractionName;
        this.entrance = entrance;
        this.exit = exit;

        this.maxNumOfCarsPossible = maxNumOfCarsPossible;
        this.carCapacity = carCapacity;
        rideLength = rideTime;
        this.loadingTime = loadingTime;

        attractionStatus = AttractionStatus.OPEN;
        waitingLine = new LinkedList<Customer>();
        waitingTime = new LinkedList<Integer>();
        cars = new LinkedList<Car>();

        // load up the initial cars
        for(int i = 0; i < initialNumOfCars; i++)
        {
            cars.add(new Car());
        }
        //maxNumOfCars = initialNumOfCars;

        isSimulation = false;
    }

    /*
     * This function pulls the appropriate number of customers out of the
     * waiting line and calculates their waiting time.  It then returns
     * an array of the customers that was removed and ready to be put on the
     * ride.
     */
    protected Customer[] extractCustomers()
    {
        // lets find out how many customers we can actually pull out of the
        // waiting line
        int size = Math.min(waitingLine.size(), carCapacity);
        Customer[] riders = new Customer[size];

        // pull the customers out of the waiting line
        for(int i = 0; i < size; i++)
        {
            riders[i] = waitingLine.remove();
            int timeEntered = waitingTime.remove();
            // calculate the rider's waiting time
            totalWaitingTime += numOfTicks - timeEntered;
        }

        return riders;
    }

    /**
     * This function returns true if a car will be available during this
     * clock tick.  This includes cars that are waiting or cars that are done.
     * @return
     */
    protected boolean hasCarAvailable()
    {
        for(Car c : cars)
        {
            if(c.isCarDone() || c.isCarWaiting())
            {
                // found a car that will be available
                return true;
            }
        }
        return false;
    }

    /**
     * This function removes the first empty car from the list of cars
     */
    protected void removeCar()
    {
        // find a car thats waiting so it can be removed
        // don't want to remove a car that is in the middle of the ride.
        ListIterator<Car> iterator = cars.listIterator();
        while(iterator.hasNext())
        {
            Car c = iterator.next();
            if(c.isCarEmpty())
            {
                // remove the first empty car we find
                iterator.remove();
                break;
            }
        }
    }

    /**
     *
     */
    protected void updateNumOfCars()
    {
        if(shouldAddCar())
        {
            //System.out.println("Adding 1 car");

            cars.addFirst(new Car());

            if(cars.size() > maxNumOfCars)
            {
                maxNumOfCars = cars.size();
            }
        }
        else if(shouldRemoveCar())
        {
            //System.out.println("Removing 1 car");
            removeCar();
            if(cars.size() < minNumOfCars)
            {
                minNumOfCars = cars.size();
            }
        }
    }

    /**
     * This function will return true if the ride should add a car to
     * the ride.  This is ride type specific.
     * @return True if a car should be added, false otherwise
     */
    abstract protected boolean shouldAddCar();
    /**
     * This function will return true if the ride should remove a car from
     * the ride.  This is ride type specific.
     * @return True if a car should be removed, false otherwise.
     */
    abstract protected boolean shouldRemoveCar();

    /**
     * This function returns the name of attraction
     * @return String containing the name of the attraction
     */
    public String getName()
    {
        return name;
    }

    /**
     * This function will determine if there is anyone in the ride
     * (actually riding or in line).
     *
     * @return boolean - True if there is no one in line or no one
     * in the ride, false otherwise.
     */
    public boolean isRideEmpty()
    {
        return currNumOfRiders == 0 && waitingLine.size() == 0;

    }
    /**
     * This function returns the length of the ride
     * @return int representing how long the ride takes (in minutes)
     */
    public int getRideLength()
    {
        return rideLength;
    }

    /**
     * This function returns the loading time of the ride
     * @return int representing how long it takes to load the cars
     * on the ride (in minutes)
     */
    public int getLoadingTime()
    {
        return loadingTime;
    }

    /**
     * This function returns the location of the ride's entrance
     * @return Returns the entrance.
     */
    public Location getEntrance()
    {
        return entrance;
    }
    /**
     * This function returns the location of the ride's exit
     * @return Returns the exit.
     */
    public Location getExit()
    {
        return exit;
    }

    /**
     * This function returns the number of people waiting in line (currently)
     * for this attraction
     * @return int representing the number of people in line
     */
    public int getLineLength()
    {
        return waitingLine.size();
    }

    /**
     * This returns the maximum line length since the attraction openned
     * @return Returns the maxLineLength.
     */
    public int getMaxLineLength()
    {
        return maxLineLength;
    }
    /**
     * This returns the maximum number of cars in operation since the
     * attraction openned
     * @return Returns the maxNumOfCars.
     */
    public int getMaxNumOfCars()
    {
        return maxNumOfCars;
    }

    /**
     * This function gets the minimum number of cars used since the ride
     * openned
     * @return Returns the minNumOfCars.
     */
    public int getMinNumOfCars()
    {
        return minNumOfCars;
    }

    /**
     * This returns the maximum number of riders on the attraction since
     * the attraction openned
     * @return Returns the maxNumOfRiders.
     */
    public int getMaxNumOfRiders()
    {
        return maxNumOfRiders;
    }
    /**
     * This returns the total number of riders who have been on this attraction
     * since the attraction openned
     * @return Returns the totalRiders.
     */
    public int getTotalRiders()
    {
        return totalRiders;
    }

    /**
     * This function returns the average number of riders/minute (clock tick)
     * @return double representing the avg number of riders
     */
    public double getAvgNumOfRiders()
    {
        return ((double)totalRidersForAllTicks) / numOfTicks;
    }

    /**
     * This function returns the average line length/minute (clock tick)
     * @return double representing the avg line length
     */
    public double getAvgLineLength()
    {
        return ((double)totalLineLengthForAllTicks) / numOfTicks;
    }

    /**
     * This function returns the average number of riders/minute (clock tick)
     * @return double representing the avg number of riders
     */
    public double getAvgNumOfCars()
    {
        return ((double)totalCarsForAllTicks) / numOfTicks;
    }

    /**
     * This function returns the average waiting time for this attraction
     * @return double representing the average waiting time
     */
    public double getAvgWaitingTime()
    {
        if(totalRiders == 0)
        	return 0;
        else
           return ((double)totalWaitingTime) / totalRiders;
    }

    /**
     * This function sets the simulation mode of the attraction.
     * If it should be in simulation mode (no logging, no information to the
     * customer, etc), then the mode should be true.
     * @param mode - true if it should be in simulation mode (no logging, as if nothing
     * is actually happening), false otherwise.
     */
    public void setSimulationMode(boolean mode)
    {
        isSimulation = mode;
    }
    /**
     * This function puts the customer in line for this attraction
     * @param customer - The customer to put in line
     * @return boolean true if the customer was able to get in line, false otherwise
     */
    public boolean enqueue(Customer customer)
    {
        boolean success = false;
        if(attractionStatus == AttractionStatus.OPEN)
        {
            waitingLine.addLast(customer);
            // store the current tick that the customer entered
            waitingTime.addLast(numOfTicks);

            if(waitingLine.size() > maxLineLength)
                maxLineLength = waitingLine.size();
            success = true;
        }
        return success;
    }

    /**
     * This function should be called when the attraction first opens
     *
     */
    public void openRide()
    {
        attractionStatus = AttractionStatus.OPEN;
        // initialize the variables
        numOfTicks = 0;
        maxNumOfRiders = 0;
        currNumOfRiders = 0;
        maxLineLength = 0;
        maxNumOfCars = 0;
        totalRiders = 0;
        totalRidersForAllTicks = 0;
        totalCarsForAllTicks = 0;
        totalLineLengthForAllTicks = 0;
        totalWaitingTime = 0;
        minNumOfCars = maxNumOfCars = cars.size();

        timeSinceEstimate = TIME_UNTIL_NEW_ESTIMATES;
        needProcessing = true;
    }

    /**
     * This function should be called when the park or the ride closes
     *
     */
    public void closeRide()
    {
        attractionStatus = AttractionStatus.CLOSED;
    }

    /**
     * This function simply updates the various statistics based on the clock tick
     *
     * The attraction sub-classes must remove customers from the waiting queue and put
     * them on rides (if there are space available) or remove customer from rides
     * (if they are done) during each clock tick.
     *
     * @return boolean - this function will return true when the ride is closed
     * and that there are no more people inline or on the ride.
     *
     */
    public boolean onClockTick()
    {
        if(needProcessing)
        {
            if(!isSimulation)
            {
                // tell all the customers in line that a minute has passed
                for(Customer c : waitingLine)
                {
                    c.onClockTick();
                }

                // now tell everyone on the ride that a tick has gone by
                for(Car c : cars)
                {
                    for(int i = 0; i < c.numRiders; i++)
                    {
                        c.customers[i].onClockTick();
                    }
                }
            }

            // updates local variables
            numOfTicks++;
            totalRidersForAllTicks += currNumOfRiders;
            totalCarsForAllTicks += cars.size();
            totalLineLengthForAllTicks += waitingLine.size();
            timeSinceEstimate++;

            needProcessing = attractionStatus == AttractionStatus.OPEN ||
                !isRideEmpty();
        }
        return needProcessing;
    }

    public Object clone() throws CloneNotSupportedException
    {
        Attraction clone = (Attraction)super.clone();

        clone.cars = new LinkedList<Car>();
        // makes a deep copy
        for(Car c : this.cars)
        {
            // Note the syntax.  This is to create an instance of car
            // that belongs to the instance of Attraction (specifically,
            // the clone instance).  This is so that the Attraction.this
            // refers to the right Attraction instance
            clone.cars.add(clone.new Car(c));
        }

        clone.waitingLine = new LinkedList<Customer>();
        for(Customer cust : this.waitingLine)
        {
            clone.waitingLine.add((Customer) cust.clone());
        }


        clone.waitingTime = new LinkedList<Integer>();
        for(Integer i : this.waitingTime)
        {
            clone.waitingTime.add(new Integer(i));
        }
        return clone;
    }

    /**
     * This function prints out various stats about the attraction.
     * These stats include the number of riders, their wait times,
     * and the number of cars used.
     *
     */
    public void printStats()
    {
        System.out.println("---------------------------------------------");
        System.out.println("Ride: " + name);
        System.out.println("\nTotal number of riders: " + totalRiders);
        System.out.printf("Average number of riders per minute: %.2f%n",
                getAvgNumOfRiders());
        System.out.printf("Average wait time per rider: %.2f%n",
                getAvgWaitingTime());
        System.out.println("\nNumber of cars:");
        System.out.printf("%7d    maximum %n%7d    minimum %n%10.2f average%n",
        		maxNumOfCars, minNumOfCars, getAvgNumOfCars());
        System.out.println("\nLine length:");
        System.out.printf("%7d    maximum %n%10.2f average%n%n", maxLineLength,
        		getAvgLineLength());

        System.out.println("---------------------------------------------");
    }

    /**
     * Writes the statistics of the attraction to the given file.
     * 
     * @param outfile
     */
	public void writeStatsToFile(FileWriter outfile)
	{
		Formatter formatter = new Formatter(outfile);

		formatter.format("---------------------------------------------%n");

		formatter.format("Ride: " + name);

		formatter.format("%n%nTotal number of riders: " + totalRiders);
		formatter.format("%nAverage number of riders per minute: %.2f",
				getAvgNumOfRiders());
		formatter.format("%nAverage wait time per rider: %.2f",
				getAvgWaitingTime());

		formatter.format("%n%nNumber of cars:%n");
		formatter.format("%7d    maximum %n%7d    minimum %n%10.2f average%n",
				maxNumOfCars, minNumOfCars, getAvgNumOfCars());
		formatter.format("%nLine length:%n");
		formatter.format("%7d    maximum %n%10.2f average%n%n", maxLineLength,
				getAvgLineLength());

		formatter.format("---------------------------------------------%n");
	}



    /**
     * This function returns the estimated waiting time for this ride.
     * It does not have to be 100% accurate but should present a rough
     * estimate as to when someone will get on the ride (assuming he/she
     * enters the waiting line right now).
     *
     * @return int representing the expected number of minutes someone will wait
     */
    abstract public int getEstimatedWaitTime();
}
