--- availability_calendars.module	Sun Jan 23 05:35:46 2011
+++ availability_calendars.module.new	Mon Feb 07 21:05:55 2011
@@ -102,8 +102,13 @@
  * @return array
  */
 function availability_calendars_admin_settings() {
-  $form = array();
   $settings = availability_calendar_getsettings();
+
+  $form = array();
+  $form['#validate'][] = 'availability_calendars_admin_settings_validate';
+  $form['#submit'][] = 'availability_calendars_admin_settings_submit';
+  $form['#tree'] = TRUE;
+
   $form['display'] = array(
     '#type' => 'fieldset',
     '#title' => t('View settings'),
@@ -146,11 +151,138 @@
     '#title' => t('Show availability calendars within teasers.'),
     '#default_value' => $settings->showteaser,
   );
-  //TODO: add status codes and css classes in here
+
+  // Add states
+  $form['states'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('States'),
+    '#description' => t('<p>You can modify the availability states here.</p>
+<ul>
+<li>The label is what users will see in the legend and what editors will see when editing the calendar.</li>
+<li>The class must be unique and will be used for the css.</li>
+<li>The weight defines the order in the legend and in the dropdown on the edit form.</li>
+<li>Make a label empty to remove the row.</li>
+<li>If there are no more empty lines to add new states, save the form and you will be able to add another state.</li>
+<li><strong>Changes made to existing classes will not be updated in existing calendars!</strong> So do not change on live sites.</li>
+<li>If you define your own classes, you will also have to provide your own css.</li>
+</ul>'),
+    '#attributes' => array('class' => 'state-list'),
+  );
+
+  $element = &$form['states'];
+  $states = availability_calendars_get_states();
+  $i = 0;
+  foreach ($states as $state) {
+    availability_calendars_admin_settings_add_state($element, $i, $state);
+    $i++;
+  }
+
+  // Show a minimum of 4 available states with at least one empty state
+  do {
+    availability_calendars_admin_settings_add_state($element, $i, array('class' => '', 'label' => '', 'weight' => 0));
+    $i++;
+  } while ($i < 4);
+
   return system_settings_form($form);
 }
 
 /**
+ * Validate callback for the admin_settings form
+ * - at least one label should be filled
+ */
+function availability_calendars_admin_settings_validate($form, &$form_state) {
+  $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
+  if ($op == t('Save configuration')) {
+    $element = $form_state['values']['states'];
+    $all_empty = true;
+    foreach ($element as $i => $state_fields){
+      // Only add non-empty labels
+      if (!empty($state_fields['label'])) {
+        $all_empty = false;
+        break;
+      }
+    }
+
+    if ($all_empty) {
+      form_set_error('states][0][label', t('At least 1 state should be defined.'));
+    }
+  }
+}
+
+/**
+ * Helper function to add a state item to a form.
+ * Only the first item gets labels, as the fields will be presented below each other.
+ *
+ * @param array $element the form element to add the state item to
+ * @param int $i the state item count
+ * @param array $state array containing a state record
+ */
+function availability_calendars_admin_settings_add_state(&$element, $i, $state) {
+  static $max_weight = 0;
+  $element[$i]['label'] = array(
+    '#type' => 'textfield',
+    '#title' => $i == 0 ? t('Label') : '',
+    '#default_value' => $state['label'],
+    '#size' => 40,
+    '#prefix' => '<div class="state-item">',
+  );
+  $element[$i]['class'] = array(
+    '#type' => 'textfield',
+    '#title' => $i == 0 ? t('Class') : '',
+    '#default_value' => $state['class'],
+    '#size' => 24,
+  );
+  $element[$i]['weight'] = array(
+    '#type' => 'select',
+    '#title' => $i == 0 ? t('Weight') : '',
+    '#default_value' => $state['weight'] > 0 ? $state['weight'] : ++$max_weight,
+    '#options' => array_combine(range(1, 20, 1), range(1, 20, 1)),
+    '#suffix' => '</div>',
+  );
+  if ($state['weight'] > $max_weight) {
+    $max_weight = $state['weight'];
+  }
+}
+
+/**
+ * Submit callback for the admin_settings form
+ *
+ * Processes the submitted form. the states are non system settings, and are handled here.
+ * Other values are handled by the default system settings form handling.
+ */
+function availability_calendars_admin_settings_submit($form, &$form_state) {
+  $op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
+  if ($op == t('Save configuration')) {
+    $element = $form_state['values']['states'];
+    // Do not process the states by the default submit handler for system settings forms
+    unset($form_state['values']['states']);
+    $states = array();
+    foreach ($element as $i => $state_fields){
+      // Only add non-empty labels
+      if (!empty($state_fields['label'])) {
+        if (empty($state_fields['class'])) {
+          $state_fields['class'] = availability_calendars_string_to_safe_id($state_fields['label']);
+        }
+        $states[$state_fields['class']] = array(
+          'class' => $state_fields['class'],
+          'label' => $state_fields['label'],
+          'weight' => $state_fields['weight'],
+        );
+      }
+    }
+
+    $existing_States = availability_calendars_get_states();
+    if ($states != $existing_States) {
+      // update states: dellete all existing, insert all states on the form
+      db_query("DELETE FROM {availability_calendars_states}");
+      foreach ($states as $state) {
+        db_query("INSERT INTO {availability_calendars_states} (class,label,weight) VALUES ('%s','%s',%d)", $state['class'], $state['label'], $state['weight']);
+      }
+    }
+  }
+}
+
+/**
  * Create tab to show node availability.
  *
  * @return string or FALSE
@@ -631,8 +763,7 @@
         $classes = array();
         if ($today == $daystamp) $classes[] = 'caltoday'; // today
         if ($daystamp < $today) $classes[] = 'calpastdate'; // past date
-        if ($settings->hideold === 1 && $daystamp < $today) $classes[] = 'calnotavailable'; // past dates should be "fully booked"
-        else {
+        if ($settings->hideold !== 1 || $daystamp >= $today) {
           if ($day_status[$day] === NULL) $classes[] = ($settings->defaultstatus) ? $settings->defaultstatus : $options[0];
           else $classes[] = $day_status[$day];
         }
@@ -688,6 +819,18 @@
 }
 
 /**
+ * @return array array with records for all states
+ */
+function availability_calendars_get_states() {
+  $states = array();
+  $result = db_query("SELECT * FROM {availability_calendars_states} ORDER BY weight");
+  while ($row = db_fetch_array($result)) {
+    $states[$row['class']] = $row;
+  }
+  return $states;
+}
+
+/**
  * Implementation of hook_form_alter().
  * All form alterations needed for the calendars.
  *
@@ -864,4 +1007,21 @@
       }
     return  $block;
   }
+}
+
+/**
+* Converts a string to a valid html id/class attribute.
+*
+* http://www.w3.org/TR/html4/struct/global.html#h-7.5.2 specifies what makes a
+* valid id/class attribute in HTML. This function:
+*
+* - Ensure an ID starts with an alpha character by prefixing with 'cal'.
+* - Replaces any character except a-z, A-Z, numbers, and underscores with dashes.
+* - Converts entire string to lowercase.
+*
+* @param $string The string
+* @return the converted string
+*/
+function availability_calendars_string_to_safe_id($string) {
+  return 'cal' . strtolower(preg_replace('/[^a-zA-Z0-9_-]+/', '-', $string));
 }
--- availability_calendars.css	Tue Jan 18 05:10:01 2011
+++ availability_calendars.css.new	Mon Feb 07 18:46:22 2011
@@ -31,3 +31,6 @@
 td.cal-notavailableprov_available > .cal-split-bg{border-color:#FFFFE0 #90EE90 #90EE90 #FFFFE0;}
 td.cal-notavailableprov_notavailable,tr.odd td.cal-notavailableprov_notavailable,tr.even td.cal-notavailableprov_notavailable{background:#FFB6C1;}
 td.cal-notavailableprov_notavailable > .cal-split-bg{border-color:#FFFFE0 #FFB6C1 #FFB6C1 #FFFFE0;}
+
+fieldset.state-list .state-item { position: relative; width: 100%; overflow: auto;}
+fieldset.state-list .form-item { float: left; margin-right: 1em;}
\ No newline at end of file
--- availability_calendars.install	Fri Jan 21 08:16:11 2011
+++ availability_calendars.install.new	Mon Feb 07 21:18:44 2011
@@ -45,8 +45,8 @@
       ),
       'status' => array(
         'description' => 'The status.',
-        'type' => 'text',
-        'size' => 'medium',
+        'type' => 'varchar',
+        'length' => 64,  // status = class or (split day) calsplit cal-<am-class>_<pm-class> = 14 + 2 * length of a class
       ),
       'date' => array(
         'description' => 'Datetime representation of availability',
@@ -89,6 +89,33 @@
       )
     )
   );
+
+  $schema['availability_calendars_states'] = array(
+    'description' => 'Store classes and labels for the possible states in availability calendars',
+    'fields' => array(
+      'class' => array(
+        'description' => 'The class used for this state',
+  			'type' => 'varchar',
+        'length' => 24,
+        'not null' => TRUE,
+      ),
+      'label' => array(
+        'description' => 'The label as displayed to users for this state',
+      	'type' => 'varchar',
+        'length' => 64,  // should not be too long: will give display problems
+        'not null' => TRUE,
+      ),
+      'weight' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'tiny',
+        'description' => 'The weight of this state',
+      ),
+    ),
+    'primary key' => array('class'),
+	);
+
   return $schema;
 }
 
@@ -96,8 +123,40 @@
  * Implementation of hook_install().
  */
 function availability_calendars_install() {
+  // Install schema
   drupal_install_schema('availability_calendars');
-  drupal_set_message(t('Availability Calendars module installed successfully.'), 'warning');
+
+  // Fill schema: add a default (starter, example) set of states to the database
+  // @TODO: shorten the classes if no css refers to these classes anymore
+  $states = array(
+    array(
+      'class' => 'calavailable',
+      'label' => 'Available',
+      'weight' => 1,
+    ),
+    array(
+      'class' => 'calnotavailable',
+      'label' => 'Fully booked',
+      'weight' => 2,
+    ),
+    array(
+      'class' => 'calnotavailableprov',
+      'label' => 'Provisionally booked',
+      'weight' => 3,
+    ),
+  );
+  $insert_query = "INSERT INTO {availability_calendars_states} (class,label,weight) VALUES ('%s','%s',%d)";
+  $success = true;
+  foreach ($states as $state) {
+    $success = db_query($insert_query, $state['class'], $state['label'], $state['weight']) !== false && $success;
+  }
+
+  if (!$success) {
+    drupal_set_message(t('Availability Calendars module not installed successfully.'), 'error');
+  }
+  else {
+    drupal_set_message(t('Availability Calendars module installed successfully.'), 'warning');
+  }
 }
 
 /**
@@ -197,7 +256,7 @@
   );
   return $ret;
 }
-
+
 /**
  * Implementation of hook_update_N().
  * Change statuses to now use class strings instead of integers and alter defaultstatus setting if set.
@@ -253,5 +312,52 @@
     'success' => TRUE,
     'query' => 'Fixed variables for ' . $sandbox['max'] . ' availability calendar variable settings.',
   );
+  return $ret;
+}
+
+/**
+ * Implementation of hook_update_N().
+ * Add custom states
+ *
+ * @return array
+ */
+function availability_calendars_update_6104(&$sandbox) {
+  $ret = array();
+
+  // Change type of field status of table availability_calendars_day,
+  // as it (kind of) refers to the class field of the new states table
+  db_change_field($ret, 'availability_calendars_day', 'status', 'status', array('type' => 'varchar', 'length' => 64));
+
+  // Add table to store configurable statuses
+  $tables = availability_calendars_schema();  //DRY: get table def from schema
+  $table_name = 'availability_calendars_states';
+  db_create_table($ret, $table_name, $tables[$table_name]);
+
+  // Add existing (hard-coded) states to the database
+  // Note: We can never shorten in an update situation, unless we update the database *contents* as well.
+  $states = array(
+    array(
+      'class' => 'calavailable',
+      'label' => 'Available',
+      'weight' => 1,
+    ),
+    array(
+      'class' => 'calnotavailable',
+      'label' => 'Fully booked',
+      'weight' => 2,
+    ),
+    array(
+      'class' => 'calnotavailableprov',
+      'label' => 'Provisionally booked',
+      'weight' => 3,
+    ),
+  );
+  $insert_query = "INSERT INTO {availability_calendars_states} (class,label,weight) VALUES ('%s','%s',%d)";
+  $success = true;
+  foreach ($states as $state) {
+    $success = db_query($insert_query, $state['class'], $state['label'], $state['weight']) !== false && $success;
+  }
+  $ret[] = array('success' => $success, 'query' => $insert_query);
+
   return $ret;
 }
