/**
  This is the base class for all preference stores. When sub-classed, you must
  override the commit, get, set, and remove methods and then add a reference
  to the new class to the Prefs._stores hash.
  
  PrefStore's are meant to be singletons.
*/
PrefStore = {
  no_value: 'No Value',
  not_implemented: 'Not Implemented',
  
  /**
    Write updated prefs to the actual preference storage.
  */
  commit: function() { throw this.not_implemented; },
  
  /**
    Retrieve a preference value. This will either return the value for 'name'
    or throw the PrefStore.no_value exception.
  */
  get: function(name) { throw this.not_implemented; },
  
  /**
    Set the value of a preference. This will set 'name' to 'value'.
  */
  set: function(name, value) { throw this.not_implemented; },
  
  /**
    Remove a preference from the store. 'name' is the preference to remove.
  */
  remove: function(name) { throw this.not_implemented; }
}

/**
  Used to implement cookie-based storage. This should be sub-classed and values
  set for the _name and _longevity member variables at least.
*/
CookieStore = Object.compose({}, PrefStore, {
  _name: null,
  _longevity: null,
  _buffer: null,
  
  _read_cookie: function() {
    if (this._buffer != null) return;
    
    var cookie = readCookie(this._name) || '';
    this._buffer = $H(cookie.split('&').inject({}, function(data, nv) {
      var a = nv.split('=');
      data[a[0]] = unescape(a[1]);
      return data;
    }));
  },
  
  _write_cookie: function() {
    if (this._buffer == null) return;
    
    var cookie = this._buffer.collect(function(pair) {
      if (pair.key == '') throw $continue;
      return [pair.key, escape(pair.value)].join('=');
    }).join('&');
    createCookie(this._name, cookie, this._longevity);
  },
  
  commit: function() {
    this._write_cookie();
    this._buffer = null;
  },
  
  get: function(name) {
    this._read_cookie();
    if (!this._buffer.keys().include(name)) throw this.no_value;
    return this._buffer[name];
  },
  
  set: function(name, value) {
    this._read_cookie();
    this._buffer[name] = value;
  },
  
  remove: function(name) {
    this._read_cookie();
    this._buffer = this._buffer.reject(function(pair) {
      return pair.key == name;
    });
  }
});

PersistentCookieStore = Object.compose({}, CookieStore,
  { _name: 'prefs', _longevity: 1000 }
);

SessionCookieStore = Object.compose({}, CookieStore,
  { _name: 'session_prefs', _longevity: null }
);

ServerStore = Object.compose({}, PrefStore, {
  commit: function() { PersistentCookieStore.commit(); },
  get: function(name) { PersistentCookieStore.get(name); },
  set: function(name, value) { PersistentCookieStore.set(name, value); },
  remove: function(name) { PersistentCookieStore.remove(name); }
});

//ServerStore = Object.compose({}, PrefStore, {
//  _url: '/prefs/',
//  _buffer: null,
//  _changed: [],
//  
//  _read_prefs: function() {
//    if (this._buffer != null) return;
//    
//    var req = new Ajax.Request(
//      this._url,
//      {
//        method: 'get',
//        asynchronous: false
//      }
//    );
//    
//    if (req.responseIsSuccess()) {
//      try {
//        var json = req.transport.responseText.replace('/*', '').replace('*/', '');
//        this._buffer = eval('(' + json + ')');
//      }
//      catch (e) { /* Do Nothing */ }
//    }
//  },
//  
//  _write_prefs: function() {
//    if (this._buffer == null) return;
//    
//    var content = {};
//    for (var i=0; i<this._changed.length; i++) {
//      content[this._changed[i]] = this._buffer[this._changed[i]];
//    }
//    
//    var body = '{' + $H(content).map(function(pair) {
//      return pair.map(Object.inspect).join(': ');
//    }).join(', ') + '}';
//    
//    var req = new Ajax.Request(
//      this.url,
//      {
//        method: 'post',
//        parameters: body,
//        asynchronous: false
//      }
//    );
//    
//    return req.responseIsSuccess();
//  },
//  
//  commit: function() {
//    if (this._write_prefs()) {
//      Prefs.bundle(function() {
//        for (var i=0; i<this.changed.length; i++)
//          Prefs.remove(this.changed[i], Prefs.COOKIE);
//      });
//      
//      this._buffer = null;
//      this._changed = [];
//    }
//  },
//  
//  get: function(name) {
//    this._read_prefs();
//    if (!this._buffer.keys().include(name)) throw this.no_value;
//    return this._buffer[name];
//  },
//  
//  set: function(name, value) {
//    this._read_prefs();
//    this._buffer[name] = value;
//    this._changed[this._changed.length] = name;
//  },
//  
//  remove: function(name) {
//    // 'Remove' not supported
//  }
//});

Prefs = {
  COOKIE: 'persistent',       // Persistent client-side cookie
  SESSION_COOKIE: 'session',  // Non-Persistent client-side cookie
  SERVER: 'server',           // Server-side preference storage
  
  // Locations that prefs can be stored in. The key will be used as the store's
  // name in functions that require it.
  _stores: {
    persistent: PersistentCookieStore,
    session: SessionCookieStore,
    server: ServerStore
  },
  _default_location: 'persistent',
  _search_order: ['persistent', 'session', 'server'],
  
  /**
    Commit's all changes to preference values to their associated preference
    stores. There is no need to ever call this function directly, but it won't
    hurt anything if you do.
  */
  commit: function() {
    for (var i=0; i<this._search_order.length; i++) {
      var location = this._search_order[i];
      this._stores[location].commit();
    }
  },
  
  /**
    Retrieve the preference 'name' from the preference store. If no value is
    found, then 'fallback' will be returned. The locations are searched in the
    order defined by Prefs._search_order
  */
  get: function(name, fallback) {
    var value;
    for (var i=0; i<this._search_order.length && !value; i++) {
      try {
        var location = this._search_order[i];
        value = this._stores[location].get(name);
      }
      catch(e) { if (e != PrefStore.no_value) throw e; }
    }
    
    if (value) try {
      return eval('(' + value + ')');
    }
    catch(e) { /* Do Nothing */ }
    return fallback;
  },
  
  /**
    Set the preference 'name' to 'value' and store it in 'location'. If no
    location is specified, then the Prefs._default_location is used.
  */
  set: function(name, value, location) {
    var location = location || this._default_location;
    var value = value.inspect ? value.inspect() : (value + '');
    
    this._stores[location].set(name, value);
    this.commit();
  },
  
  /**
    Remove the preference 'name' from 'location'.
  */
  remove: function(name, location) {
    var location = location || this._default_location;
    
    this._stores[location].remove(name);
    this.commit();
  },
  
  /**
    Prevent changes to any preferences while 'method' is running. A call to
    lockout will call the passed 'method' while the Prefs object is in a state
    that will prevent changes to any values. The Prefs object will be reverted
    to normal operation after 'method' returns.
  */
  lockout: function(method) {
    var orig_set = this.set;
    this.set = function() { /* Do Nothing */ }
    try {
      method();
    }
    catch(e) {
      this.set = orig_set;
      throw e;
    }
    this.set = orig_set;
  },
  
  /**
    This buffers preference changes until 'method' is complete. The Prefs object
    is put into a state that will cause preference changes to accumulate while
    'method' is run. Upon completion, the preference changes will be sent to
    the various locations at once.
  */
  bundle: function(method) {
    var orig_commit = this.commit;
    this.commit = function() { /* Do Nothing */ }
    try {
      method();
    }
    catch(e) {
      this.commit = orig_commit;
      this.commit();
      throw e;
    }
    this.commit = orig_commit;
    this.commit();
  }
}