// ---------------------------------------------------------------------------
// - Appointer.cpp                                                           -
// - afnix:pim module - appointer class implementation                       -
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - 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.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2011 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Vector.hpp"
#include "Integer.hpp"
#include "Boolean.hpp"
#include "Runnable.hpp"
#include "Appointer.hpp"
#include "QuarkZone.hpp"
#include "Exception.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // max number of week days
  static const long ATC_MAX_WDAY = 7;
  static const long ATC_MAX_MDAY = 31;
  static const long ATC_MAX_YMON = 12;

  // the appointer rule
  enum t_artype {
    AR_BDAY, // blocked day
    AR_SDAY, // special day
    AR_MAXS, // max slot rule
    AR_VBTM  // valid block time
  };

  struct s_rule {
    // the rule type
    t_artype d_type;
    // the rule info
    long d_info;
    union {
      // special month day
      long d_mday;
      // the block begin time
      t_long d_bbtm;
    };
    union {
      // special year month
      long d_ymon;
      // the block end time
      t_long d_betm;
    };
    // the next rule in chain
    s_rule* p_next;
    // create a rule by type
    s_rule (t_artype type) {
      d_type = type;
      d_info = -1;
      d_bbtm = 0LL;
      d_betm = Time::DSEC;
      p_next = nilp;
    }
    // copy construct this rule
    s_rule (const s_rule& that) {
      d_type = that.d_type;
      d_info = that.d_info;
      d_bbtm = that.d_bbtm;
      d_betm = that.d_betm;
      p_next = (that.p_next == nilp) ? nilp : new s_rule (*that.p_next);
    }
    // delete this rule
    ~s_rule (void) {
      delete p_next;
    }
    // link a rule with the tail
    void link (s_rule* rule) {
      if (rule == nilp) return;
      if (p_next != nilp) {
	p_next->link (rule);
      } else {
	p_next = rule;
      }
    }
    // check if the time  is vald
    bool chktime (const t_long time) const {
      // the check result
      bool result = true;
      // process the 'and' plane first
      const s_rule* rule = this;
      while (rule != nilp) {
	// process a blocked day rule
	if (rule->d_type == AR_BDAY) {
	  Date date (time);
	  if (date.getwday (true) == rule->d_info) return false;
	}
	// process a special day rule
	if (rule->d_type == AR_SDAY) {
	  Date date (time);
	  if ((date.getmday (true) == rule->d_mday) && 
	      (date.getymon (true) == rule->d_ymon)) return false;	  
	}
	// next rule
	rule = rule->p_next;
      }
      // process the 'or' plane
      rule = this;
      while (rule != nilp) {
	// process a valid block time
	if (rule->d_type == AR_VBTM) {
	  t_long bbtm = time % Date::DSEC;
	  if (rule->d_bbtm <= bbtm) return true;
	  result = false;
	}
	rule = rule->p_next;
      }
      // default validation
      return result;
    }
    // check if the slot is valid
    bool chkslot (const t_long time, const t_long dlen) const {
      // the check result
      bool result = true;
      // process the 'and' plane first
      const s_rule* rule = this;
      while (rule != nilp) {
	// process a blocked day rule
	if (rule->d_type == AR_BDAY) {
	  Date dbtm (time);
	  if (dbtm.getwday (true) == rule->d_info) return false;
	  Date detm (time + dlen);
	  if (detm.getwday (true) == rule->d_info) return false;
	  result = true;
	}
	// process a special day rule
	if (rule->d_type == AR_SDAY) {
	  Date dbtm (time);
	  if ((dbtm.getmday (true) == rule->d_mday) &&
	      (dbtm.getymon (true) == rule->d_ymon)) return false;
	  Date detm (time + dlen);
	  if ((detm.getmday (true) == rule->d_mday) &&
	      (detm.getymon (true) == rule->d_ymon)) return false;
	  result = true;
	}
	// next rule
	rule = rule->p_next;
      }
      // process the 'or' plane
      rule = this;
      while (rule != nilp) {
	// process a valid block time
	if (rule->d_type == AR_VBTM) {
	  t_long bbtm = time % Date::DSEC;
	  t_long betm = (time + dlen) % Date::DSEC;
	  if ((rule->d_bbtm <= bbtm) && (betm <= rule->d_betm)) return true;
	  result = false;
	}
	rule = rule->p_next;
      }
      // default validation
      return result;
    }
    // get the next available time
    t_long gettime (const t_long time, const bool nday) const {
      t_long tnxt = time;
      // process the 'and' plane first - the iteration limit is one
      // wee controlled by the day counter
      long dcnt = 0;
      // loop in the rules
      const s_rule* rule = this;
      while (rule != nilp) {
	// process a blocked day rule
	if (rule->d_type == AR_BDAY) {
	  Date date (tnxt);
	  if (date.getwday (true) == rule->d_info) {
	    // move to the next day
	    tnxt = date.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	}
	// process a special day rule
	if (rule->d_type == AR_SDAY) {
	  Date date (tnxt);
	  if ((date.getmday (true) == rule->d_mday) && 
	      (date.getymon (true) == rule->d_ymon)) {
	    // move to the next day
	    tnxt = date.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	}
	// next rule
	rule = rule->p_next;
      }
      // process the 'or'plane by selecting the base time
      rule = this;
      while (rule != nilp) {
	if (rule->d_type == AR_VBTM) {
	  t_long bbtm = tnxt % Date::DSEC;
	  if (bbtm < rule->d_bbtm) {
	    Date date (tnxt);
	    tnxt = date.getbday () + rule->d_bbtm;
	    return tnxt;
	  }
	}
	// next rule
	rule = rule->p_next;
      }
      // check if we try the next day
      if (nday == false) return time;
      // move to the next day and restart
      Date date (tnxt);
      tnxt = date.getbday () + Date::DSEC;
      return gettime (tnxt, false);
    }
    // get the time that adhere to the max slot rule
    t_long getmaxs (const t_long time, const long dsnm) {
      t_long tnxt = time;
      // process the rules and check the max slot rule
      const s_rule* rule = this;
      while (rule != nilp) {
	if (rule->d_type == AR_MAXS) {
	  if (dsnm >= rule->d_info) {
	    // move to the next day
	    Date date (tnxt);
	    tnxt = date.getbday () + Date::DSEC;
	    break;
	  }
	}
	rule = rule->p_next;
      }
      return tnxt;
    }
    // get the valid slot time
    t_long getslot (const t_long time, const t_long dlen) const {
      t_long tnxt = time;
      // process the 'and' plane first - the iteration limit is one
      // wee controlled by the day counter
      long dcnt = 0;
      // loop in the rules
      const s_rule* rule = this;
      while (rule != nilp) {
	// process a blocked day rule
	if (rule->d_type == AR_BDAY) {
	  // check begin time
	  Date dbtm (tnxt);
	  if (dbtm.getwday (true) == d_info) {
	    // move to the next day
	    tnxt = dbtm.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	  // check end time
	  Date detm (tnxt+dlen);
	  if (detm.getwday (true) == rule->d_info) {
	    // move to the next day
	    tnxt = detm.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	}
	// process a special day rule
	if (rule->d_type == AR_SDAY) {
	  // check begin time
	  Date dbtm (tnxt);
	  if ((dbtm.getmday (true) == d_mday) &&
	      (dbtm.getymon (true) == d_ymon)) {
	    // move to the next day
	    tnxt = dbtm.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	  // check end time
	  Date detm (tnxt+dlen);
	  if ((detm.getmday (true) == rule->d_mday) &&
	      (detm.getymon (true) == rule->d_ymon)) {
	    // move to the next day
	    tnxt = detm.getbday () + Date::DSEC;
	    // check max counter
	    if (dcnt++ > ATC_MAX_WDAY) return time;
	    // reset processing rule
	    rule = this;
	    continue;
	  }
	}
	// next rule
	rule = rule->p_next;
      }
      // process the 'or'plane by selecting the base time
      rule = this;
      while (rule != nilp) {
	if (rule->d_type == AR_VBTM) {
	  t_long bbtm = tnxt % Date::DSEC;
	  t_long betm = (tnxt + dlen) % Date::DSEC;
	  if ((rule->d_bbtm <= bbtm) && (betm <= rule->d_betm)) return tnxt;
	}
	// next rule
	rule = rule->p_next;
      }
      // eventually try to get the next available time
      return gettime (tnxt, true);
    }
  };

  // check if a slot is in the slot set
  static bool is_pushb_slot (const Set& pset, const Slot& slot) {
    long slen = pset.length ();
    for (long i = 0; i < slen; i++) {
      Slot* sobj = dynamic_cast <Slot*> (pset.get (i));
      if (sobj == nilp) continue;
      if (sobj->match (slot) == true) return true;
    }
    return false;
  }

  // return the index of a push-backed slot that matches in duration
  static long get_pushb_slot (const Set& pset, const t_long dlen) {
    long slen = pset.length ();
    for (long i = 0; i < slen; i++) {
      Slot* sobj = dynamic_cast <Slot*> (pset.get (i));
      if (sobj == nilp) continue;
      if (sobj->getdlen () == dlen) return i;
    }
    return -1;
  }

  // return the index of a push-backed slot that matches in time and duration
  static long get_pushb_slot (const Set& pset, 
			      const t_long time, const t_long dlen) {
    long slen = pset.length ();
    for (long i = 0; i < slen; i++) {
      Slot* sobj = dynamic_cast <Slot*> (pset.get (i));
      if (sobj == nilp) continue;
      if ((sobj->gettime () >= time) &&
	  (sobj->getdlen () == dlen)) return i;
    }
    return -1;
  }

  // return the pushback slot minimum time
  static t_long get_pushb_amtm (const Set& pset, const t_long time) {
    t_long result = time;
    long slen = pset.length ();
    for (long i = 0; i < slen; i++) {
      Slot* sobj = dynamic_cast <Slot*> (pset.get (i));
      if (sobj == nilp) continue;
      t_long sltm = sobj->gettime ();
      if (sltm <= result) result = sltm;
    }
    return result;
  }

  // return the pushback slot minimum time
  static t_long get_pushb_amtm (const Set& pset, 
				const t_long time, const t_long mrtm) {
    t_long result = mrtm < time ? time : mrtm;
    long slen = pset.length ();
    for (long i = 0; i < slen; i++) {
      Slot* sobj = dynamic_cast <Slot*> (pset.get (i));
      if (sobj == nilp) continue;
      t_long sltm = sobj->gettime ();
      if ((sltm <= result) && (mrtm <= sltm)) result = sltm;
    }
    return result;
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------

  // create a default appointer

  Appointer::Appointer (void) {
    d_time = 0LL;
    d_dsnm = 0;
    d_snum = 0;
    p_rule = nilp;
  }

  // create a new appointer by time

  Appointer::Appointer (const t_long time) {
    d_time = time;
    d_dsnm = 0;
    d_snum = 0;
    p_rule = nilp;
  }

  // copy construct this appointer

  Appointer::Appointer (const Appointer& that) {
    that.rdlock ();
    try {
      d_time = that.d_time;
      d_dsnm = that.d_dsnm;
      d_snum = that.d_snum;
      p_rule = (that.p_rule == nilp) ? nilp : new s_rule (*that.p_rule);
      that.unlock ();
    } catch (...) {
      that.unlock ();
      throw;
    }
  }

  // destroy this appointer

  Appointer::~Appointer (void) {
    delete p_rule;
  }

  // return the appointer class name

  String Appointer::repr (void) const {
    return "Appointer";
  }

  // return a clone of this object

  Object* Appointer::clone (void) const {
    return new Appointer (*this);
  }
  
  // reset this appointer

  void Appointer::reset (void) {
    wrlock ();
    try {
      d_dsnm = 0;
      d_snum = 0;
      d_pset.reset ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the appointer time

  void Appointer::settime (const t_long time) {
    wrlock ();
    try {
      d_time = time;
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the appointer time

  t_long Appointer::gettime (void) const {
    rdlock ();
    try {
      t_long result = d_time;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the appointer minimum time

  t_long Appointer::getamtm (void) const {
    rdlock ();
    try {
      t_long result = get_pushb_amtm (d_pset, d_time);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the appointer minimum time with respect to a time

  t_long Appointer::getamtm (const t_long mrtm) const {
    rdlock ();
    try {
      t_long result = get_pushb_amtm (d_pset, d_time, mrtm);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set the appointer date

  void Appointer::setdate (const Date& date) {
    wrlock ();
    try {
      d_time = date.gettime (true);
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }
    
  // return the appointer date

  Date Appointer::getdate (void) const {
    rdlock ();
    try {
      Date date (d_time);
      unlock ();
      return date;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // return the number of slots

  long Appointer::getsnum (void) const{
    rdlock ();
    try {
      long result = d_snum;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a blocked day rule

  void Appointer::setbday (const long wday) {
    if ((wday < 0) || (wday >= ATC_MAX_WDAY)) {
      throw Exception ("index-error", "invalid week day index to block");
    }
    wrlock ();
    try {
      // create a new rule
      s_rule* rule = new s_rule (AR_BDAY);
      rule->d_info = wday;
      // attach the rule
      if (p_rule != nilp) {
	p_rule->link (rule);
      } else {
	p_rule = rule;
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a special day rule

  void Appointer::setsday (const long ymon, const long mday) {
    if ((ymon < 1) || (ymon > ATC_MAX_YMON)) {
      throw Exception ("index-error", "invalid special month index");
    }
    if ((mday < 1) || (mday >= ATC_MAX_MDAY)) {
      throw Exception ("index-error", "invalid special month day index");
    }
    wrlock ();
    try {
      // create a new rule
      s_rule* rule = new s_rule (AR_SDAY);
      rule->d_ymon = ymon;
      rule->d_mday = mday;
      // attach the rule
      if (p_rule != nilp) {
	p_rule->link (rule);
      } else {
	p_rule = rule;
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a max slot rule rule

  void Appointer::setmaxs (const long maxs) {
    // ignore 0 or less argument
    if (maxs <= 0) return;
    wrlock ();
    try {
      // create a new rule
      s_rule* rule = new s_rule (AR_MAXS);
      rule->d_info = maxs;
      // attach the rule
      if (p_rule != nilp) {
	p_rule->link (rule);
      } else {
	p_rule = rule;
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // set a valid block time rule

  void  Appointer::setvbtm (const t_long bbtm, const t_long betm) {
    wrlock ();
    try {
      // create a new block
      s_rule* rule = new s_rule (AR_VBTM);
      rule->d_bbtm = bbtm % Date::DSEC;
      rule->d_betm = betm % Date::DSEC;
      // attach the rule
      if (p_rule != nilp) {
	p_rule->link (rule);
      } else {
	p_rule = rule;
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the next available slot by duration

  Slot Appointer::getslot (const t_long dlen) {
    wrlock ();
    try {
      // check in the pushback set
      long sidx = get_pushb_slot (d_pset, dlen);
      if (sidx != -1) {
	// get the slot
	Slot* slot = dynamic_cast <Slot*> (d_pset.get (sidx));
	Slot result = *slot;
	d_pset.remove (slot);
	// unlock and return
	unlock ();
	return result;
      }
      // check without rule
      if (p_rule == nilp) {
	Slot result (d_time, dlen);
	d_time += dlen;
	d_snum++;
	unlock ();
	return result;
      }
      // check the max number of slots
      t_long tnxt = p_rule->getmaxs (d_time, d_dsnm);
      if (tnxt != d_time) d_dsnm = 0;
      // compute the base time
      if (p_rule->chkslot (tnxt, dlen) == false) {
	tnxt = p_rule->gettime (tnxt, true);
	if (p_rule->chktime (tnxt) == false) {
	  throw Exception ("appointer-error", "cannot set slot base time");
	}
      }
      // compute the slot time
      t_long time = p_rule->getslot (tnxt, dlen);
      if (p_rule->chkslot (time, dlen) == false) {
	throw Exception ("appointer-error", "cannot find appointer slot");
      }				  
      // allocate the result slot
      Slot result (time, dlen);
      // update the time
      d_time = time + dlen;
      d_dsnm++;
      d_snum++;
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the next available slot by time and duration

  Slot Appointer::getslot (const t_long time, const t_long dlen) {
    wrlock ();
    try {
      // check in the pushback set
      long sidx = get_pushb_slot (d_pset, time, dlen);
      if (sidx != -1) {
	// get the slot
	Slot* slot = dynamic_cast <Slot*> (d_pset.get (sidx));
	Slot result = *slot;
	d_pset.remove (slot);
	// unlock and return
	unlock ();
	return result;
      }
      // check the time
      if (d_time < time) settime (time);
      // get the slot
      Slot result = getslot (dlen);
      unlock ();
      return result;
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // pushback a slot in the slot pool
  
  void Appointer::pushback (const Slot& slot) {
    wrlock ();
    try {
      if (is_pushb_slot (d_pset, slot) == false) {
	d_pset.add (new Slot (slot));
      }
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 13;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);

  // the object supported quarks
  static const long QUARK_RESET   = zone.intern ("reset");
  static const long QUARK_PUSHB   = zone.intern ("pushback");
  static const long QUARK_SETTIME = zone.intern ("set-time");
  static const long QUARK_GETTIME = zone.intern ("get-time");
  static const long QUARK_SETDATE = zone.intern ("set-date");
  static const long QUARK_GETDATE = zone.intern ("get-date");
  static const long QUARK_GETSLOT = zone.intern ("get-slot");
  static const long QUARK_GETAMTM = zone.intern ("get-minimum-time");
  static const long QUARK_GETSNUM = zone.intern ("get-slot-number");
  static const long QUARK_SETBDAY = zone.intern ("set-blocked-day");
  static const long QUARK_SETSDAY = zone.intern ("set-special-day");
  static const long QUARK_SETMAXS = zone.intern ("set-maximum-slots");
  static const long QUARK_SETVBTM = zone.intern ("set-valid-block-time");

  // create a new object in a generic way

  Object* Appointer::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    // create a default appointer object
    if (argc == 0) return new Appointer;
    // check for 1 argument
    if (argc == 1) {
      t_long time = argv->getlong (0);
      return new Appointer (time);
    }
    throw Exception ("argument-error",
                     "too many argument with appointer constructor");
  }

  // return true if the given quark is defined

  bool Appointer::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? Object::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }

  // apply this object with a set of arguments and a quark

  Object* Appointer::apply (Runnable* robj, Nameset* nset, const long quark,
			   Vector* argv) {
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();

    // check for  argument
    if (argc == 0) {
      if (quark == QUARK_GETTIME) return new Integer (gettime ());
      if (quark == QUARK_GETAMTM) return new Integer (getamtm ());
      if (quark == QUARK_GETDATE) return new Date    (gettime ());
      if (quark == QUARK_GETSNUM) return new Integer (getsnum ());
      if (quark == QUARK_RESET) {
	reset ();
	return nilp;
      }
    }
    // check for 1 argument
    if (argc == 1) {
      if (quark == QUARK_GETSLOT) {
	t_long dlen = argv->getlong (0);
	return new Slot (getslot (dlen));
      }
      if (quark == QUARK_SETBDAY) {
	long wday = argv->getlong (0);
	setbday (wday);
	return nilp;
      }
      if (quark == QUARK_SETMAXS) {
	long maxs = argv->getlong (0);
	setmaxs (maxs);
	return nilp;
      }
      if (quark == QUARK_SETTIME) {
	t_long time = argv->getlong (0);
	settime (time);
	return nilp;
      }
      if (quark == QUARK_GETAMTM) {
	t_long mrtm = argv->getlong (0);
	return new Integer (getamtm (mrtm));
      }
      if (quark == QUARK_SETDATE) {
	Object* obj = argv->get (0);
	Date*  date = dynamic_cast <Date*> (obj);
	if (date == nilp) {
	  throw Exception ("type-error", "invalid object with set-date",
			   Object::repr (obj));
	}
	setdate (*date);
	return nilp;
      }
      if (quark == QUARK_PUSHB) {
	Object* obj = argv->get (0);
	Slot*  slot = dynamic_cast <Slot*> (obj);
	if (slot == nilp) {
	  throw Exception ("type-error", "invalid object with pushback",
			   Object::repr (obj));
	}
	pushback (*slot);
	return nilp;
      }
    }
    // check for 2 arguments
    if (argc == 2) {
      if (quark == QUARK_GETSLOT) {
	t_long time = argv->getlong (0);
	t_long dlen = argv->getlong (1);
	return new Slot (getslot (time, dlen));
      }
      if (quark == QUARK_SETVBTM) {
	t_long bbtm = argv->getlong (0);
	t_long betm = argv->getlong (1);
	setvbtm (bbtm, betm);
	return nilp;
      }
      if (quark == QUARK_SETSDAY) {
	long ymon = argv->getlong (0);
	long mday = argv->getlong (1);
	setsday (ymon, mday);
	return nilp;
      }
    }
    // call the object method
    return Object::apply (robj, nset, quark, argv);
  }
}
