//----------------------------------------------------------------------
//- Functions common to Event Registration and Event Description Editing
//----------------------------------------------------------------------

//----------------------------------------------------------------------
//- tbaTrim: Remove leading & trailing whitespace from a string
//----------------------------------------------------------------------
  function tbaTrim(str)
  {
   return !str ? '' : String(str).replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  }

//----------------------------------------------------------------------
//- Base class descriptions
//----------------------------------------------------------------------

  //--------------------------------------------------------------------
  //- Class: tbaPrice: A single Price for an Event
  //--------------------------------------------------------------------
  function tbaPrice (desc, price, parent)
  {
    this.description = tbaTrim(desc);
    this.price       = price;
    this.parent      = parent;
  }

  tbaPrice.prototype.description = '';
  tbaPrice.prototype.price       = 0;
  tbaPrice.prototype.parent      = undefined;

  //--------------------------------------------------------------------
  //- freeWords: Acceptable terms for the concept of "No Cost"
  //--------------------------------------------------------------------

  tbaPrice.freeWords = ['free', 'none', '0', 0, 'no charge'];

  //--------------------------------------------------------------------
  //- getPrice: Gets a guaranteed numeric or NaN result for the price.
  //--------------------------------------------------------------------
  tbaPrice.prototype.getPrice = function()
  {
   return this.forceNumber(this.price);
  };

  //--------------------------------------------------------------------
  //- getDescription: Gets a price's description with no extraneous characters.
  //--------------------------------------------------------------------
  tbaPrice.prototype.getDescription = function()
  {
   return tbaTrim(this.description);
  };

  //--------------------------------------------------------------------
  //- isValid: Make sure the object has a valid price number and a
  //- non-blank description.
  //--------------------------------------------------------------------
  tbaPrice.prototype.isValid = function()
  {
   return this.validPrice() &&
          this.validDescription();
  };

  //--------------------------------------------------------------------
  //- isEmpty: Are the price and the description missing?
  //--------------------------------------------------------------------
  tbaPrice.prototype.isEmpty = function()
  {
   return ((tbaTrim(this.description).length === 0) &&
           (tbaTrim(this.price).length       === 0));
  }

  //--------------------------------------------------------------------
  //- validDescription: Does the price object have a valid (i.e. ANY)
  //- description?
  //--------------------------------------------------------------------
  tbaPrice.prototype.validDescription = function()
  {
   return this.getDescription().length > 0;
  };

  //--------------------------------------------------------------------
  //- validPrice: Is the object's price a valid number or the terms
  //- 'free' or 'none'?
  //--------------------------------------------------------------------
  tbaPrice.prototype.validPrice = function()
  {
   if (isNaN(this.getPrice()))
   {
    var p = tbaTrim(String(this.price).toLowerCase());

    for (i=0; i<tbaPrice.freeWords.length; i++)
    {
     if (p == tbaPrice.freeWords[i])
      {return true;}
    }

    return false;
   }

   return true;
  };

  //--------------------------------------------------------------------
  //- forceNumber: Do turn a thing into a useful Number or NaN.
  //--------------------------------------------------------------------
  tbaPrice.prototype.forceNumber = function (value)
  {
    switch (typeof value)
    {
     case 'number':
      return value;

     case 'boolean':
      return value ? 1 : 0;

     case 'function':
     case 'undefined':
      return NaN;

     case 'string':
     case 'object':
       var o  = tbaTrim(value);
           o  = o.replace(/\$/g,   '');
           o  = o.replace(/\x2C/g, '');  // x2C == a comma (,)
       var v  = (o.length > 0) ? Number(o) : NaN;
       return v;

     default:
      return NaN;
    }
  };

  //--------------------------------------------------------------------
  //- Class: tbaEvent: The base description for a single Event
  //--------------------------------------------------------------------
  function tbaEvent (id)
  {
    this.id     = id;
    this.prices = [];
    this.prepare();
  }

  tbaEvent.prototype.id      = undefined;
  tbaEvent.prototype.tag     = undefined;
  tbaEvent.prototype.group   = '' ;
  tbaEvent.prototype.name    = '' ;
  tbaEvent.prototype.date    = '' ;
  tbaEvent.prototype.endDate = '' ;
  tbaEvent.prototype.time    = '' ;
  tbaEvent.prototype.desc    = '' ;
  tbaEvent.prototype.link    = '' ;
  tbaEvent.prototype.msg     = '' ;
  tbaEvent.prototype.cutoff  = '' ;
  tbaEvent.prototype.prices  = [] ;
  tbaEvent.prototype.delevt  = false;
  tbaEvent.prototype.usermsg = undefined;
  tbaEvent.prototype.newevt  = undefined;  //- Used by tbaEventCollection
  tbaEvent.prototype.parent  = undefined;  //- Used by tbaEventCollection

  //--------------------------------------------------------------------
  //- isNew, isDeleted: Boolean getters/setters.
  //--------------------------------------------------------------------
  tbaEvent.prototype.isNew = function (b)
  {
   if (typeof(b) == 'boolean')
    {this.newevt = b;}

   return this.newevt;
  };

  //--------------------------------------------------------------------
  tbaEvent.prototype.isDeleted = function (b)
  {
   if (typeof(b) == 'boolean')
    {this.delevt = b;}
   return this.delevt;
  };

  //--------------------------------------------------------------------
  //- addPrice: Adds a price/description pair to an Event.
  //- non-null object. The princ is a number (with or without $ , etc.)
  //- or 'FREE' or 'NONE'.  If successful (i.e. valid data) the price/
  //- description pair is returned as a tbaPrice, otherwise 'false';
  //--------------------------------------------------------------------
  tbaEvent.prototype.addPrice = function (desc, amt)
  {
    var p = new tbaPrice(desc, amt, this);

    if (!p.isValid())
     {return false;}

    this.prices.push(p);

    return p;
  };

  //--------------------------------------------------------------------
  //- prepare: Called during the object's constructor. By default it
  //- does nothing. This can be overridden to do any setup needed.
  //--------------------------------------------------------------------
  tbaEvent.prototype.prepare = function() {};

  //--------------------------------------------------------------------
  //- isValid: Test Event for validity -- May be overridden.
  //--------------------------------------------------------------------
  tbaEvent.prototype.isValid = function()
  {
    var i;

    for (i=0; i<this.prices.length; i++)
     {if (!this.prices[i].isValid())
      {return false;}}

    return (this.prices.length > 0);
  };

  //--------------------------------------------------------------------
  //- getInvalid: Collect invalid parts of the event. Returns an array
  //- of invalid things.
  //- -- May be overridden.
  //--------------------------------------------------------------------
  tbaEvent.prototype.getInvalid = function()
  {
   var a = [];
   var i;

   for (i=0; i<this.prices.length; i++)
    {if (!this.prices[i].isValid())
     {a.push(this.prices[i]);}}

   return a;
  };

  //--------------------------------------------------------------------
  //- getSelected: Collect valid/changed parts of the event. Returns an
  //- array of valid/changed things.
  //- -- May be overridden.
  //--------------------------------------------------------------------
  tbaEvent.prototype.getSelected = function()
  {
   var a = [];
   var i;

   for (i=0; i<this.prices.length; i++)
    {if (this.prices[i].isValid())
     {a.push(this.prices[i]);}}

   return a;
  };

  //--------------------------------------------------------------------
  //- isEvent: Is the object given a tbaEvent?
  //--------------------------------------------------------------------
  tbaEvent.isEvent = function (target)
  {
   if (typeof(target) == 'object')
    {return (target.constructor == tbaEvent.prototype.constructor);}
   else
    {return false;}
  };

  //--------------------------------------------------------------------
  //- isOver: Has the date for this event passed?
  //--------------------------------------------------------------------
  tbaEvent.prototype.isOver = function ()
  {
   var today   = new Date();
   var edate   = new Date(this.date);
   return (today > edate);
  };

  //--------------------------------------------------------------------
  //- tbaEventCollection: Base description for a collection of Events.
  //--------------------------------------------------------------------
  function tbaEventCollection ()
  {
    this.events = [];
  }

  tbaEventCollection.prototype.events = [];
  tbaEventCollection.prototype.grps   = [];
  tbaEventCollection.prototype.length = 0;

  //--------------------------------------------------------------------
  //- newEvent: Create a new Event for the Collection. If an Event with
  //- the id given does not exist in the collection, create it, add it
  //- and return it. If an Event with the id does exist, return it.
  //- If no id is given, create, add, and return a new Event with a
  //- generated id;
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.newEvent = function (id)
  {
    var t = this.getEvent(id);

    if (t)
     {return t;}

    if (tbaEvent.isEvent(id))
     {return this.addEvent(id);}

    switch (typeof id)
    {
     case 'number'    :
     case 'string'    :
     case 'undefined' :
      var e  = new tbaEvent(id);
      e.isNew(true);
      e.prices.push(new tbaPrice('', '', e));
      return this.addEvent(e);

     default:
      return undefined;
    }
  };

  //--------------------------------------------------------------------
  //- addEvent: Insert an Event into the collection. If a tbaEvent
  //- object is passed, just add it. If an event id (number) is passed,
  //- treat this as a 'newEvent'.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.addEvent = function (event)
  {
   switch (typeof event)
   {
     case 'number'    :
     case 'string'    :
     case 'undefined' :
      return this.newEvent(event);

     default:
      break;
   }

   if (!tbaEvent.isEvent(event))
    {return undefined;}

   if (this.getEvent(event))
    {return event;}

   if (!event.id)
    {event.id = this.maxId()+1;}

   event.parent = this;

   this.events.push(event);
   this.length = this.events.length;
   this.addGroup(event);

   return event;
  };

  //--------------------------------------------------------------------
  //- MaxId: What is the highest event.id value in the collection?
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.maxId = function ()
  {
    var max = 0;
    var i;

    for (i=0; i<this.events.length; i++)
    {
     if (this.events[i].id > max)
      {max = this.events[i].id;}
    }

    return max;
  };

  //--------------------------------------------------------------------
  //- removeEvent: Remove an Event from the collection. If an id
  //- number/string passed, find the event with that id number and
  //- remove it. If a tbaEvent is passed, find the event and remove it.
  //- If the remove is successful, return the removed event, otherwise
  //- return undefined.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.removeEvent = function (id)
  {
   var target;
   var i;

   switch (typeof id)
   {
     case 'number'    :
     case 'string'    :
      target = id;
      break;

     case 'object'    :
      if (tbaEvent.isEvent(id))
       {target = id.id;}
      break;

     default:
      break;
   }

   for (i=0; target && i<this.events.length; i++)
   {
    if (this.events[i].id == target)
    {
     this.events.splice(i, 1);
     id.parent = undefined;

     return id;
    }
   }

   return undefined;
  };

  //--------------------------------------------------------------------
  //- sort: Sort the event collection. Default is by Date within Group.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.sort = function (fname)
  {
   return this.events.sort(fname ? fname : this.byGroupAndDate);
  };

  //--------------------------------------------------------------------
  //- byGroupAndDate: Sort aid function to sort tbaEvent objects by
  //- Date within Group.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.byGroupAndDate = function (a,b)
  {
    //- Compare Groups.
    if (a.group < b.group)
     {return -1;}

    if (a.group > b.group)
     {return  1;}

    //- Groups are equal so compare Event Dates.
    var ad = new Date(a.date);
    var bd = new Date(b.date);

    if (ad < bd)
     {return -1;}

    if (ad > bd)
     {return  1;}

    //- Groups AND Event Dates are Equal, so compare Event Names
    if (a.name < b.name)
     {return -1;}

    if (a.name > b.name)
     {return  1;}

    //- They are as equal as they can get!
    return 0;
  };

  //--------------------------------------------------------------------
  //- getEventById: Given an id, search the event collection and return
  //- the event, if found.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.getEventById = function (id)
  {
    var i;

    for (i=0; i<this.events.length; i++)
     {if (this.events[i].id == id)
      {return this.events[i];}}

    return false;
  };

  //--------------------------------------------------------------------
  //- getEventByIndex: Get the N'th event in the event collection.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.getEventByIndex = function (index)
  {
    if (index < 0 || index >= this.events.length)
     {return undefined;}

    return this.events[index];
  };

  //--------------------------------------------------------------------
  //- getEvent: Checks to see if an event is in the collection. If it
  //- is, return it. If not, return undefined.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.getEvent = function (id)
  {
    if (!id)
     {return undefined;}

    if (typeof(id) == 'number' || typeof(id) == 'string')
     {return this.getEventById(id);}

    if (!tbaEvent.isEvent(id))
     {return undefined;}

    if (!id.id)
     {return undefined;}

    if (id.parent == this)
     {return id;}

    return undefined;
  };

  //--------------------------------------------------------------------
  //- groups: Collect and return all the groups from the collection
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.groups = function ()
  {
    for (i=0; i<this.events.length; i++)
     {this.addGroup(this.events[i]);}
    return this.grps;
  };

  //--------------------------------------------------------------------
  //- getEventsForGroup: Collect and return all events for a given group.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.getEventsForGroup = function (groupName)
  {
    var group = new Array();

    for (var i=0; i<this.events.length; i++)
    {
      if (this.events[i].group.toLowerCase() == groupName.toLowerCase())
       group.push(this.events[i]);
    }

    return group;
  }

  //--------------------------------------------------------------------
  //- addGroup: Add the 'group' value from the given event to the
  //- 'groups' collection if it is not alreay in there.
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.addGroup = function (event)
  {
    var group = tbaTrim(event.group);
    var i;

    if (group.length === 0)
     {return;}

    var target = new RegExp('^'+group+'$', 'i');

    for (i=0; i<this.grps.length; i++)
    {
     if (this.grps[i].search(target) > -1)
      {return;}
    }

    this.grps.push(group);
    this.grps = this.grps.sort();
  };

  //--------------------------------------------------------------------
  //- removeGroup: Removes the 'group' value of the given event from the
  //- 'groups' collection if it is the last one of that group in there,
  //--------------------------------------------------------------------
  tbaEventCollection.prototype.removeGroup = function (event)
  {
    var target = tbaTrim(event.group);
    var i;

    if (target.length === 0)
     {return;}

    target = new RegExp('^'+target+'$', 'i');

    //- If there is another event with the same group still in the
    //- collection, then do not remove the group from 'groups'.
    for (i=0; i<this.events.length; i++)
    {
     if ((this.events[i].id == event.id) ||
         (this.events[i].group.search(target) > -1))
      {return;}
    }

    //- Since there are no other events with the same group, remove
    //- the group from the groups collection.
    for (i=0; i<this.grps.length; i++)
    {
     if (grps[i].search(target) > -1)
     {
      grps = groups.splice(i, 1);

      return;
     }
    }
  };

//----------------------------------------------------------------------
