#include <cmath>
#include <cstdlib>
#include <iostream>
#include <queue>
#include <string>

using namespace std;

class Simulation;

class Event
{
public:
   Event(double event_time);
   virtual void process(Simulation& sim) const;
   double get_time() const;
   virtual ~Event() {}
private:
   double time;   
};

// Przedefiniuj porównanie < dla wskaźników Event*.
namespace std
{
   template<>
   class less<Event*>
   {
   public:      
      bool operator()(Event* a, Event* b)
      {
         return a->get_time() > b->get_time();
      }
   };
}

class Simulation
{
public:   
   /**
      Konstruuje symulację oddzielnych zdarzeń.
   */
   Simulation();

   double get_current_time() const;

   /**
      Generuje liczby losowe z rozkładem wykładniczym.
      @param mean wartość średnia sekwencji liczb
      @return losowa liczba
   */
   double expdist(double mean) const;

   /**
      Dodaje zdarzenie do kolejki zdarzeń.
      @param evt dodawane zdarzenie
   */
   void add_event(Event* evt);

   /**
      Wyświetla częściowe wyniki po każdym zdarzeniu.
   */
   virtual void print() const;

   /**
      Wyświetla zbiorcze wyniki po zakończeniu symulacji.
   */
   virtual void print_summary() const;

   /**
      Uruchamia symulację dla danego okresu czasu.
      @param start_time czas rozpoczęcia
      @param end_time czas zakończenia
   */
   void run(double start_time, double end_time);
   virtual ~Simulation() {}
private:
   priority_queue<Event*> event_queue;
   double current_time;
};

class Arrival : public Event
{
public:
   /**
      @param time czas przybycia
   */
   Arrival(double time);
   virtual void process(Simulation& sim) const;
};

class Departure : public Event
{
public:
   /**
      @param time czas wyjścia
      @param teller kasjer obsługujący klienta
   */
   Departure(double time, int teller);   
   virtual void process(Simulation& sim) const;
private:
   int teller;
};

class Customer
{
public:
   /** 
       Konstruuje obiekt klienta.
       @param czas wejścia klienta do banku
   */
   Customer(double time);

   /**
      Pobiera czas, w którym klient wszedł do banku.
      @return czas przybycia
   */
   double get_arrival_time() const;
private:
   double arrival_time;
};

class BankSimulation : public Simulation
{
public:   
   /**
      Konstruuje symulację sali bankowej.
      @param number_of_tellers liczba kasjerów
   */
   BankSimulation(int number_of_tellers);
   /**
      Dodaje klienta do symulacji.
      @param c klient
   */
   void add(Customer* c);
   /** 
       Usuwa klienta od kasjera.
       @param i miejsce kasjera
   */
   void remove(int i);
   virtual void print() const;
   virtual void print_summary() const;
private:
   /**
      Przydziela klienta do kasjera i planuje zdarzenie odejścia.
      @param i numer kasjera
      @param c klient
   */
   void add_to_teller(int i, Customer* c);
   vector<Customer*> tellers;
   queue<Customer*> customer_queue;
   int total_customers;
   double total_time;

   const double INTERARRIVAL = 1; 
      // średni czas pomiędzy pojawianiem się kolejnych klientów, wynoszący 1 minutę
   const double PROCESSING = 5; 
      // średni czas załatwiania sprawy klienta, wynoszący 5 minut
};

Simulation::Simulation() {}

double Simulation::get_current_time() const { return current_time; }
   
double Simulation::expdist(double mean) const
{  
   return -mean * log(1 - rand() * 1.0 / RAND_MAX);
}

void Simulation::add_event(Event* evt)
{
   event_queue.push(evt);
}

void Simulation::print() const {}

void Simulation::print_summary() const {}

void Simulation::run(double start_time, double end_time)
{
   current_time = start_time;

   while (event_queue.size() > 0 && current_time <= end_time)
   { 
      Event* event = event_queue.top();
      event_queue.pop();
      current_time = event->get_time();
      event->process(*this);
      delete event;
      print();
   }
   print_summary();   
}
   
Event::Event(double event_time)
{
   time = event_time;
}

void Event::process(Simulation& sim) const {}

double Event::get_time() const { return time; }

Arrival::Arrival(double time) : Event(time) {}

void Arrival::process(Simulation& sim) const
{
   double now = sim.get_current_time();
   BankSimulation& bank = dynamic_cast<BankSimulation&>(sim);
   Customer* c = new Customer(now);
   bank.add(c);
}

Departure::Departure(double time, int teller) : Event(time)
{
   this->teller = teller;
}
   
void Departure::process(Simulation& sim) const
{  
   BankSimulation& bank = dynamic_cast<BankSimulation&>(sim);
   bank.remove(teller);
}

Customer::Customer(double time) { arrival_time = time; }

double Customer::get_arrival_time() const { return arrival_time; }

BankSimulation::BankSimulation(int number_of_tellers) 
{
   for (int i = 0; i < number_of_tellers; i++)
   {
      tellers.push_back(nullptr);
   }
   total_customers = 0;
   total_time = 0;
}

void BankSimulation::add(Customer* c) 
{
   bool added_to_teller = false;
   for (int i = 0; !added_to_teller && i < tellers.size(); i++)
   {
      if (tellers[i] == nullptr)
      {  
         add_to_teller(i, c);
         added_to_teller = true;
      }
   }
   if (!added_to_teller) { customer_queue.push(c); }

   add_event(new Arrival(get_current_time() + expdist(INTERARRIVAL)));
}

void BankSimulation::add_to_teller(int i, Customer* c)
{
   tellers[i] = c;
   add_event(new Departure(get_current_time() + expdist(PROCESSING), i));
}
   
void BankSimulation::remove(int i)
{
   Customer* c = tellers[i];
   tellers[i] = nullptr;

   // Zaktualizuj dane statystyczne.
   total_customers++; 
   total_time = total_time + get_current_time() - c->get_arrival_time();
   delete c;
   
   if (customer_queue.size() > 0)
   {
      add_to_teller(i, customer_queue.front());
      customer_queue.pop();
   }
}

void BankSimulation::print() const
{
   for (int i = 0; i < tellers.size(); i++)
   {
      if (tellers[i] == nullptr)
      {
         cout << ".";
      } 
      else
      {
         cout << "K";
      } 
   }
   cout << "<";
   int q = customer_queue.size();
   for (int j = 1; j <= q; j++) { cout << "K"; }
   cout << endl;
}

void BankSimulation::print_summary() const
{
   double average_time = 0;
   if (total_customers > 0) 
   { 
      average_time = total_time / total_customers; 
   }
   cout << total_customers << " klientów. Średni czas "
      << average_time << " minut." << endl;
}

int main()
{
   const double START_TIME = 9 * 60; // 9 rano
   const double END_TIME = 17 * 60; // 5 po południu

   const int NTELLERS = 5;

   BankSimulation sim(NTELLERS);
   sim.add_event(new Arrival(START_TIME));
   sim.run(START_TIME, END_TIME);
}

