is = {
  webkit: /WebKit/i,
  iphone: /iPhone|iPod/i,
  ie: /MSIE/,
  firefox: /Firefox/
}

for (var k in is)
    is[k] = navigator.userAgent.match(is[k]);

// Handlers for various browser-specific hacks
(function() {
  function hideURLbar() { window.scrollTo(0, 1) }
  
  if (is.iphone) {
    addEventListener("load", function(){ setInterval(hideURLbar, 100) }, false);
  }
  
  if (is.firefox) {
    $(window).load(function() {
      // Makes it so the insertion point appears in Firefox's chatback
      $('#chatback').css({ position: 'fixed' });
    });
  }
})()/*
    json2.js
    2008-02-14

    Public Domain

    No warranty expressed or implied. Use at your own risk.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods:

        JSON.stringify(value, whitelist)
            value       any JavaScript value, usually an object or array.

            whitelist   an optional array parameter that determines how object
                        values are stringified.

            This method produces a JSON text from a JavaScript value.
            There are three possible ways to stringify an object, depending
            on the optional whitelist parameter.

            If an object has a toJSON method, then the toJSON() method will be
            called. The value returned from the toJSON method will be
            stringified.

            Otherwise, if the optional whitelist parameter is an array, then
            the elements of the array will be used to select members of the
            object for stringification.

            Otherwise, if there is no whitelist parameter, then all of the
            members of the object will be stringified.

            Values that do not have JSON representaions, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays will be replaced with null.
            JSON.stringify(undefined) returns undefined. Dates will be
            stringified as quoted ISO dates.

            Example:

            var text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'

        JSON.parse(text, filter)
            This method parses a JSON text to produce an object or
            array. It can throw a SyntaxError exception.

            The optional filter parameter is a function that can filter and
            transform the results. It receives each of the keys and values, and
            its return value is used instead of the original value. If it
            returns what it received, then structure is not modified. If it
            returns undefined then the member is deleted.

            Example:

            // Parse the text. If a key contains the string 'date' then
            // convert the value to a date.

            myData = JSON.parse(text, function (key, value) {
                return key.indexOf('date') >= 0 ? new Date(value) : value;
            });

    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    Use your own copy. It is extremely unwise to load third party
    code into your pages.
*/

/*jslint evil: true */

/*global JSON */

/*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
    charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length,
    parse, propertyIsEnumerable, prototype, push, replace, stringify, test,
    toJSON, toString
*/

if (!this.JSON) {

    JSON = function () {

        function f(n) {    // Format integers to have at least two digits.
            return n < 10 ? '0' + n : n;
        }

        Date.prototype.toJSON = function () {

// Eventually, this method will be based on the date.toISOString method.

            return this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z';
        };


        var m = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        };

        function stringify(value, whitelist) {
            var a,          // The array holding the partial texts.
                i,          // The loop counter.
                k,          // The member key.
                l,          // Length.
                r = /["\\\x00-\x1f\x7f-\x9f]/g,
                v;          // The member value.

            switch (typeof value) {
            case 'string':

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe sequences.

                return r.test(value) ?
                    '"' + value.replace(r, function (a) {
                        var c = m[a];
                        if (c) {
                            return c;
                        }
                        c = a.charCodeAt();
                        return '\\u00' + Math.floor(c / 16).toString(16) +
                                                   (c % 16).toString(16);
                    }) + '"' :
                    '"' + value + '"';

            case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

                return isFinite(value) ? String(value) : 'null';

            case 'boolean':
            case 'null':
                return String(value);

            case 'object':

// Due to a specification blunder in ECMAScript,
// typeof null is 'object', so watch out for that case.

                if (!value) {
                    return 'null';
                }

// If the object has a toJSON method, call it, and stringify the result.

                if (typeof value.toJSON === 'function') {
                    return stringify(value.toJSON());
                }
                a = [];
                if (typeof value.length === 'number' &&
                        !(value.propertyIsEnumerable('length'))) {

// The object is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                    l = value.length;
                    for (i = 0; i < l; i += 1) {
                        a.push(stringify(value[i], whitelist) || 'null');
                    }

// Join all of the elements together and wrap them in brackets.

                    return '[' + a.join(',') + ']';
                }
                if (whitelist) {

// If a whitelist (array of keys) is provided, use it to select the components
// of the object.

                    l = whitelist.length;
                    for (i = 0; i < l; i += 1) {
                        k = whitelist[i];
                        if (typeof k === 'string') {
                            v = stringify(value[k], whitelist);
                            if (v) {
                                a.push(stringify(k) + ':' + v);
                            }
                        }
                    }
                } else {

// Otherwise, iterate through all of the keys in the object.

                    for (k in value) {
                        if (typeof k === 'string') {
                            v = stringify(value[k], whitelist);
                            if (v) {
                                a.push(stringify(k) + ':' + v);
                            }
                        }
                    }
                }

// Join all of the member texts together and wrap them in braces.

                return '{' + a.join(',') + '}';
            }
        }

        return {
            stringify: stringify,
            parse: function (text, filter) {
                var j;

                function walk(k, v) {
                    var i, n;
                    if (v && typeof v === 'object') {
                        for (i in v) {
                            if (Object.prototype.hasOwnProperty.apply(v, [i])) {
                                n = walk(i, v[i]);
                                if (n !== undefined) {
                                    v[i] = n;
                                } else {
                                    delete v[i];
                                }
                            }
                        }
                    }
                    return filter(k, v);
                }


// Parsing happens in three stages. In the first stage, we run the text against
// regular expressions that look for non-JSON patterns. We are especially
// concerned with '()' and 'new' because they can cause invocation, and '='
// because it can cause mutation. But just to be safe, we want to reject all
// unexpected forms.

// We split the first stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace all backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

                if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the second stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                    j = eval('(' + text + ')');

// In the optional third stage, we recursively walk the new structure, passing
// each name/value pair to a filter function for possible transformation.

                    return typeof filter === 'function' ? walk('', j) : j;
                }

// If the text is not JSON parseable, then a SyntaxError is thrown.

                throw new SyntaxError('parseJSON');
            }
        };
    }();
}(function(/* namespace cynocast; */) {
var API_URL = 'http://www.cynocast.net/';
// Utility methods
Util = {
  alert: function() {
    var args = Array.apply(this,arguments);
    alert(Util.collect(args, function(k,v){ return Util.str(v) }).join('\n'));
  },
  
  autoLink: function(str, target) {
    if (!target) target = '_blank';
    return Util.escapeHTML(str).replace(/((ht|f)tp(s?):[^\s]+)/g, '<a href="$1" target="'+target+'">$1</a>');
  },
  
  choose: function(item, choices) {
    if (item in choices) return choices[item]
    else if ('notFound' in choices) return choices.notFound
    else return null;
  },
  
  escapeHTML: function(str) {
    return str.replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  },
  
  console: '',
  log: function(msg) {
    try {
      Util.console += String(msg) + '\n';
    } catch(e) {
      alert('ERROR: '+msg+'\nexception:'+e);
    }
    if (is.ie) {
      //alert('IE LOG\n======\n'+msg);
      return;
    }
    try {
      console.log(msg);
    } catch (e) {
    }
  },
  
  // Executes a function after a certain amount of milliseconds. Like setTimeout,
  // but returns an async "token" object with two methods: reset() and stop().
  // reset() resets the timer, causing the delay to be postponed for "delay"
  // milliseconds, while stop() prevents the action from ever being executed.
  soon: function(delay, func) {
    var token = {
      ref: null,
      reset: function(newDelay) {
        if (newDelay) delay = newDelay;
        token.stop();
        token.start();
      },
      start: function() { token.ref = window.setTimeout(func, delay); },
      stop: function() { window.clearTimeout(token.ref); }
    }
    token.start();
    return token;
  },
  
  // Add all properties from b to a
  extend: function(a, b) { for (var k in b) a[k] = b[k]; },
  
  // Function generator that forwards its calls to the parameter
  fw: function(f,t) {
    if (!t) t = this;
    return function() { return f.apply(t, arguments) };
  },
  
  // Creates a source string for the passed-in object
  str: function(o) {
    var s;
    try { s = o.toString(); }
    catch (e) { return 'null'; }
    
    if (o instanceof Array) {
      s = [];
      for (var i = 0; i < o.length; i ++)
        s.push(Util.str(o[i]));
      s = '[ '+s.join(', ')+' ]';
    }
    
    else if (o instanceof Error || s.match(/^\[object[^\]]+\]$/)) {
      s = [];
      for (var k in o)
        s.push(Util.str(k)+': '+Util.str(o[k]));
      s = '{ '+s.join(', ')+' }';
    }
    
    return s;
  },
  
  _hex: "0123456789abcdef".split(''),
  toHex: function(text) {
    var res='';
    for (var i = 0; i < text.length; i ++) {
      var c = text.charCodeAt(i), r = c % 16;
      res += Util._hex[(c-r)/16] + Util._hex[r];
    }
    return res;
  },
  
  // Enumeration methods
  collect: function(o, f) {
    var res = [];
    Util.each(o, function(k,v){ res.push(f(k,v)); });
    return res;
  },
  
  dispatchEach: function(obj, dispatcher) {
    Util.each(obj, function(k,v) {
      var func = Util.choose(k,dispatcher);
      if (func instanceof Function)
        func(v);
      else if (typeof func == 'string')
        eval(func);
    })
  },
  
  each: function(obj, func) {
    if (obj && 'length' in obj) {
      // This loop can tend to be faster. Also, this is the only one that works with the 
      // implicit "arguments" parameter to a function.
      for (var i = 0; i < obj.length; i++) func(i, obj[i]);
    } else {
      for (var k in obj) func(k, obj[k]);
    }
  },
  
  // Creates an array of all keys in the passed-in object
  keys: function(o) { return Util.collect(o, function(k){ return k }) },
  error: function (title, message) {
    if (!$('#error').length) {
      $('<div id="error"/>').appendTo($('body')).click(function(){ $(this).hide() });
    }
    $('#error').html('<h3>'+title+'</h3><p>'+message+'</p>').css('opacity',0.9).show();
  },
  
  recur: function(d,f,me) {
    if (!me) me = this;
    window.setTimeout(function(){ f.call(me); Util.recur(d,f,me); }, d);
  },
  
  // Checks a condition every 100 milliseconds, and calls the continuation 
  // passed in when done. Returns a function that can be called to abort.
  // If it's aborted, the continuation does NOT get called.
  waitUntil: function(condition, continuation) {
    var abort = false;
    var abortFunc = function(){ abort = true };
    var doWaitUntil = function() {
      if (abort)
        return;
      else if (condition())
        continuation();
      else
        window.setTimeout(doWaitUntil, 100);
    }
    doWaitUntil();
    return abortFunc;
  }
};

Util.extend(Date.prototype, {
  // Simple date formatting function
  format: function(str) {
    var methods = { 
      M:'getMonth', 
      D:'getDate', 
      y:'getYear', 
      Y:'getFullYear', 
      h:'getHours',
      H:'getHours', 
      m:'getMinutes', 
      s:'getSeconds' 
    };
    var result = '';
    for (var i = 0; i < str.length; i ++) {
      var s = str.charAt(i);
      if (s in methods) {
        var m = this[methods[s]]();
        if (/[Hhms]/.exec(s) && m < 10)
          m = '0' + m;
        if (s == 'H' && m > 12)
          m -= 12;
        result += m;
      }
      else if (s == 'a') result += this.getHours() > 12 ? 'PM' : 'AM';
      else result += s;
    }
    return result;
  },
  setTimeOnly: function(t) {
    this.setHours(0);
    this.setMinutes(0);
    this.setSeconds(0);
    this.setTime(t * 1000 + this.getTime());
  }
});

window.$Log = Util.log;

window.coveringDiv = function() {
  if (!window._coveringDiv) {
    var w = window._coveringDiv = document.createElement('DIV');
    document.body.appendChild(w);
    w.style.position = 'absolute';
    w.style.left = '0';
    w.style.top = '0';
    w.style.right = '0';
    w.style.bottom = '0';
    w.style.zIndex = '1';
    // FOR IE
    $(w).css({
      background: '#eee',
      opacity: 0.01,
      width: '100%',
      height: '100%'
    });
    w.hide = function() { w.style.display = 'none'; };
    w.show = function() { w.style.display = 'block'; };
  }
  return window._coveringDiv;
};
// Easy way to define OOP-like classes.
// Special keys can be used in the anonymous object passed in:
// init: the function's constructor
// static: an object containing properties and methods to add to the class object itself
// superclass: a class that this is a subclass of
function Class(obj) {
  var klass = function() { return this.init.apply(this, arguments); };
  Class.current = obj.__class = klass;
  if ('static' in obj) {
    Util.extend(klass, obj.static);
    delete obj.static;
  }
  klass.prototype = obj;
  if ('superclass' in obj) {
    klass.prototype = obj.superclass;
    klass.prototype.prototype = obj;
  }
  if (!('init' in obj)) obj.init = new Function;
  return klass;
}window.clientMessage = function(message,params) {
  switch (message) {
    case 'Alert':
      alert(params[0]);
      break;
    
    case 'SetStatus':
      $('#urlFrame').attr('src', 'http://www.cynocast.com/available_'+params[0]+'.html');
    
    default: // do nothing
  }
};

// The following functions are automatically created at runtime.
var SoundCallables = [
  'sendMessage'
];

Sound = new Class({
  static: {
    playCache: [],
    inputStreamName: null,
    outputStreamName: null,
    player: null,
    
    playerExists: function() {
      return Sound.player && Sound.player.playSound;
    },
    
    isReady: function() {
      return Sound.playerExists() && Sound.player.isReady();
    },
    
    load: function() {
      Util.log("BEGIN SOUND LOAD");
      
      Sound.player = swfobject.getObjectById('soundplayer');
      
      if (!Sound.playerExists()) {
        Util.log("SOUND LOAD FAILED!");
        return;
      }
      
      while (Sound.playCache.length > 0)
        Sound.playCache.shift().play();
      
      if (Sound.inputStreamName != null)
        Sound.player.setInputStream(Sound.inputStreamName);
      
      if (Sound.outputStreamName != null)
        Sound.player.setOutputStream(Sound.outputStreamName);
      
      Sound.updateMuteButtons();
      
      Util.log("END SOUND LOAD");
    },
    
    call: function(name, args) {
      args = Array.apply(args, args);
      
      if (Sound.isReady()) {
        Util.log("Executing Sound.player."+name+"("+args+")");
        return Sound.player[name].apply(Sound.player, args);
      }
      
      else window.setTimeout(function(){ Sound.call(name, args); }, 100);
    },
    
    cynoSound: {
      play: function() {
        if (!Sound.playerExists()) {
          Sound.load();
          return;
        }
        Sound.player.playCynoCastSound();
      }
    },
    
    getMute: function() {
      var mute = window.prefs.get('mute', 'off');
      return mute;
    },
    
    setMute: function(val) {
      window.prefs.set('mute', val);
      Sound.updateMuteButtons(val);
    },
    
    isMuted: function() {
      var isMuted = Sound.getMute() == 'on';
      return isMuted;
    },
    
    setIsMuted: function(muted) {
      Sound.setMute(muted ? 'on' : 'off');
    },
    
    toggleIsMuted: function() {
      Sound.setIsMuted(!Sound.isMuted());
    },
    
    updateMuteButtons: function(m) {
      m = m || Sound.getMute();
      Sound.setVolume(m == 'on' ? 0.0 : 1.0);
      $('.mute img').attr('src', API.URL + 'images/mute_'+m+'.gif');
      $('.mute_s').attr('src', API.URL + 'images/mute_'+m+'_s.gif');
    },
    
    getVolume: function() {
      if (!Sound.playerExists()) return;
      var volume = Sound.player.getSoundVolume();
      return volume;
    },
    
    setInputStream: function(name) {
      if (!Sound.playerExists()) {
        if (name != null)
          window.setTimeout('Sound.setInputStream("'+name+'")', 100);
        return;
      }
      
      Sound.player.setInputStream(name);
      
      // Set up a callback that syncs the timer and opens the player at the end.
      var callback = window.setInterval(function() {
        var pos = Sound.player.getStreamPosition();
        
        if (pos == -1)
          return;
        
        window.timer.set(pos * 1000);
        
        if (window.cynocast && !window.cynocast.live && !Sound.player.isStreamPlaying()) {
          //Util.error(pos, Sound.player.isStreamPlaying());
          window.clearInterval(callback);
          window.cynocast.audioEndReached();
        }
      }, 250);
    },
    
    setOutputStream: function(name) {
      if (!Sound.playerExists()) {
        if (name != null)
          window.setTimeout('Sound.setOutputStream("'+name+'")', 100);
        return;
      }
      Sound.player.setOutputStream(name);
      //window.setTimeout('window.timer.set(Sound.player.getStreamPosition("'+name+'"))',200);
      //this.updateCallback();
      //this.debugInfo();
    },
    
    setVolume: function(volume) {
      if (!Sound.playerExists()) return;
      Sound.player.setSoundVolume(volume);
    }
  },
  
  callback: null,
  callbackTimer: null,
  url: null,
  isStream: false,
  
  init: function(url, isStream) {
    this.url = url;
    this.isStream = isStream;
  },
  
  debugInfo: function() {
    var sound = this;
    window.setTimeout(function(){ sound.debugInfo(); }, 50);
    Util.error('SOUND DEBUG INFO','position: '+Math.round(this.position())+'<br>isPlaying: '+this.isPlaying());
  },
  
  isPlaying: function() {
    if (!Sound.playerExists()) return false;
    return Sound.player.isSoundPlaying(this.url);
  },
  
  play: function() {
    if (!Sound.playerExists())
      Sound.playCache.push(this);
    else {
      Sound.player.playSound(this.url);
      this.updateCallback();
      //this.debugInfo();
    }
  },
  
  position: function() {
    if (!Sound.playerExists()) return -1;
    return Sound.player.getSoundPosition(this.url);
  },
  
  removeCallback: function() {
    window.clearInterval(this.callbackTimer);
    this.callback = this.callbackTimer = null;
  },
  
  setCallback: function(cb) {
    this.callback = cb;
    this.updateCallback();
  },
  
  setVolume: function(volume) {
    if (!Sound.playerExists()) return;
    Sound.player.setSoundVolume(this.url, volume);
  },
  
  stop: function() {
    if (!Sound.playerExists()) return;
    Sound.player.stopSound(this.url);
  },
  
  toString: function() {
    return '[object Sound]';
  },
  
  updateCallback: function() {
    var self = this;
    var cb = this.callback;
    
    if (this.callbackTimer != null) {
      window.clearInterval(this.callbackTimer);
      this.callbackTimer = null;
    }
    
    if (Sound.player != null && cb != null && this.isPlaying())
      this.callbackTimer = window.setInterval(function(){ cb(self); }, 250);
  },
  
  volume: function() {
    if (!Sound.playerExists()) return -1;
    return Sound.player.getSoundVolume(this.url);
  }
});

for (var i = 0; i < SoundCallables.length; i++) {
  var name = SoundCallables[i];
  Sound[name] = function() { return Sound.call(name, arguments); };
}Timer = new Class({
  init: function(start,dest,cb) {
    this.reset();
    if (dest) this.setDest(dest, cb?cb:null);
    if (start) this.start();
  },
  
  running: false,
  date: new Date,
  dest: null,
  elapsed: 0,
  intervalID: null,
  updateCallback: null,
  
  reset: function() {
    this.date = new Date;
    this.elapsed = 0;
  },
  set: function(val) {
    this.reset();
    this.elapsed = val;
  },
  setDest: function(sel,cb) {
    this.dest = sel;
    this.updateCallback = cb ? cb : null;
    this.updateDest();
  },
  start: function() {
    this.running = true;
    this.date.setTime((new Date).getTime())
  },
  stop: function() {
    this.elapsed = this.value();
    this.running = false;
  },
  toString: function() {
    var v = (this.value() / 1000).toFixed(0);
    var h = '0' + String(Math.floor(v / 60));
    var s = '0' + String(v - h*60);
    return h.substr(h.length-2, 2) + ':' + s.substr(s.length-2, 2);
  },
  updateDest: function() {
    if (this.dest) {
      $(this.dest).html(this.toString());
      var self = this;
      window.setTimeout(function(){self.updateDest()}, 1000);
    }
    if (this.updateCallback) this.updateCallback(this.value());
  },
  value: function() {
    return this.elapsed + (this.running ? (new Date).getTime() - this.date.getTime() : 0);
  }
});Prefs = new Class({
  data: {},
  
  init: function() {
    var cookies = document.cookie.split('; ');
    for (var i = 0; i < cookies.length; i++) {
      var c = cookies[i], n = c.indexOf('=');
      this.data[c.slice(0,n)] = c.slice(n+1);
    }
  },
  
  get: function(key, def) {
    var res = this.data[key];
    if (!res && def) this.set(key, res = def);
    return res;
  },
  getObject: function(key) {
    var res = this.get(key);
    try {
      res = JSON.parse(res);
    } catch (e) { return null }
    
    return res;
  },
  isSet: function(key) { return key in this.data },
  save: function() {
    var date = new Date;
    date.setTime(date.getTime()+365*24*60*60*1000);
    for (var k in this.data)
      document.cookie = k+"="+this.data[k]+"; expires="+date.toGMTString()+"; path=/";
  },
  set: function(key, val) {
    this.data[key] = val;
    this.save();
    return val;
  },
  setObject: function(key, obj) {
    this.set(key, JSON.stringify(obj));
  },
  toggle: function(key, def) {
    var val = this.get(key, def) == 'on' ? 'off' : 'on';
    return this.set(key, val);
  },
  toString: function() {
    return '[Prefs ' + Util.str(this.data) + ']';
  }
});

window.prefs = new Prefs;Bookmarks = new Class({
  data: null,
  
  init: function() {
    this.data = prefs.getObject('bookmarks');
    if (!this.data || !this.data.length) this.data = [];
    for (var i = 0; i < this.data.length; i++) {
      this.addItem(this.data[i]);
    }
  },
  add: function(t,u) {
    var o = {t:t,u:u};
    this.data.push(o);
    this.addItem(o);
    this.save();
  },
  addItem: function(o) {
    var item = $('<div><b>'+o.t+'</b><a href="'+o.u+'" target="_blank">'+o.u+'</a></div>');
    item.appendTo($('#bookmarks'));
  },
  clear: function() {
    this.data = [];
    this.save();
  },
  save: function() {
    prefs.setObject('bookmarks', this.data);
  }
});// The API object allows for easy communication with the CynoCast API.
API = {
  URL: API_URL,
  key: null,
  permission: 'client',
  
  callbacks: {},
  currentID: 0,
  
  call: function (q,f) {
    var s = document.createElement('script');
    var reqID = API.currentID++;
    s.src = API.getBaseURL() + q + '.' + reqID + '.js';
    s.type = 'text/javascript';
    
    if (f)
      API.callbacks[reqID.toString()] = f;
    
    $('head')[0].appendChild(s);
    var scripts = $('head script');
    
    if (scripts.length > 5)
      delete scripts.eq(0).remove()[0];
  },
  callComplete: function(o,reqID) {
    if (reqID in API.callbacks) {
      API.callbacks[reqID](o);
      return;
    }
    Util.each(o, function(k,v) {
      if (API.handlers[k]) API.handlers[k](v);
      else if (API.defaultHandler) API.defaultHandler(k,v);
    });
  },
  defaultHandler: function(k,v) {
    Util.error('Warning: Unknown Object', Util.str({key:k, value:v}));
  },
  getBaseURL: function() {
    return API.URL + 'api/' + API.permission + ':' + API.key + '/';
  },
  handlers: {
    //FIXME: Deprecated
    CynoCastID: function(id) {
      // Just compare the returned ID with the filed one as a sanity check
      if (!id || id != window.cynocast.id)
        Util.error('Client Error', "The CynoCast returned by the server (ID: "+id+") doesn't match the CynoCast being played (ID: "+window.cynocast.id+").");
    },
    Error: function(e) {
      Util.error('Sorry, Error #'+e.ID+' Occurred.', e.Message);
    },
    History: function(items) {
      //Util.log('#### handlers.History; items.length:'+items.length)
      window.cynocast.addToHistory(items);
      
      if (!window.timer.running)
        window.timer.start();
    },
    Info: function(s) {
      window.cynocast.setInfo(s);
    },
    Message: function(m) {
      if (!HasFlash)
        window.cynocast.messageReceived(m);
    }
  }
};

window.$SyncMessages = function(values) {
  Util.log('$SyncMessages: '+Util.str(values));
  window.cynocast.addToHistory(values);
};

window.$SyncStreams = function(name, oldValue, newValue) {
  if (TalkBack.streamName != null)
    TalkBack.setHasLock(newValue[TalkBack.streamName].lock == TalkBack.clientID);
  else for (var k in newValue) {
    if (newValue[k].lock == 0)
      TalkBack.streamName = k;
  }
};

var adminMessageHandlers = {
  launch: function(item) {
    //cynocast.launchAndAdd(item);
  }
};

window.adminMessage = function(message, params) {
  if (message in adminMessageHandlers)
    adminMessageHandlers[message].apply(null, params);
  else
    $Log('UNHANDLED ADMIN MESSAGE: '+message+' params: '+Util.str(params));
};
// The Archive class represents a recorded CynoCast and adds API convenience methods.
var Archive = window.Archive = new Class({
  info: {},
  URL: null,
  
  init: function(id) {
    this.id = id;
    this.URL = 'archive:' + id;
    this.load();
  },
  
  callAPI: function(c,d,f) {
    return API.call(this.URL+'/'+c+(d ? '/'+d : ''), f);
  },
  
  load: function() {
    var self = this;
    this.callAPI('info', null, function(i){ self.setInfo(i); });
  },
  
  endReached: function(hasAudio) {
    switch (this.info.EndAction) {
      case "stop":
        if (this.info.ShowPlayerAtEnd && !Player.beenOpened)
          Player.openSoon(hasAudio ? 0 : 10);
        
        Menu.setIsVisible(this.info.ShowURLsAtEnd);
        break;
      
      case "live":
      case "url":
        window.setTimeout('document.location.replace("'+this.info.EndURL+'")', 1000);
        break;
    }
  },
  
  setInfo: function(info) {
    this.info = info;
  },
  
  toString: function() {
    return '[Archive #'+this.id+': '+this.info.Name+']';
  }
});// The CynoCast class represents a CynoCast and adds API convenience methods.
var CynoCast = window.CynoCast = new Class({
  static: {
    load: function(id, live, aid) {
      if (window.cynocast)
        return window.cynocast;
      
      if (live == 'archive')
        window.archive = new Archive(aid);
      
      var c = window.cynocast = new CynoCast(id, live == 'live');
      c.load();
      return c;
    }
  },
  
  file: null,
  history: [],
  info: {},
  lastID: 0,
  lastLaunchID: 0,
  lastLaunchURL: '',
  inputStreamName: null,
  outputStreamName: null,
  
  // On Demand Handlers
  handlers: {
    dispatch: function(item) {
      var func = cynocast.handlers[item.Message||'launch'];
      if (!func) {
        Util.error("HANDLER NOT FOUND ("+cynocast.type()+")", 
          "The following message was not understood by the "+cynocast.type()+" player:<br><br>"+Util.str(item));
      } else {
        func(item);
      }
    },
    
    chatback: function(item) { Util.log('ChatBack ignored: \n'+Util.str(item)); },
    clear: function(item) { Util.log('Clear ignored: \n'+Util.str(item)); },
    clientmenu: function(item) {
      Menu.setIsVisible(Util.choose(item.Params, {
        show: true,
        hide: false,
        notFound: Menu.isVisible()
      }));
    },
    player: function(item) {
      var playerOpen = Player.isOpen();
      playerOpen = Util.choose(item.Params, {
        hide: false,
        show: true,
        toggle: !playerOpen,
        notFound: playerOpen
      });
      Player.setIsOpen(playerOpen);
      Player.beenOpened = true;
    },
    setchatbackprivacy: function(item) { Util.log('Set ChatBack Privacy ignored: \n'+Util.str(item)); }
  },
  
  queue: {},
  
  init: function(id, live) {
    this.id = id;
    this.live = live;
    this.URL = 'cynocast:' + this.id;
  },
  
  addToHistory: function(h) {
    if (h.length) {
      for (var i = 0; i < h.length; i ++) {
        var item = h[i];
        this.history.push(item);
        this.addToQueue(item);
        
        if (!this.live)
          window.carousel.add(item, false);
      }
      //if (this.live)
      //  this.launchLastItem();
      this.updateQueue();
    }
  },

  addToQueue: function(item) {
    var key = item.Message == '' ? 'launch' : item.Message;
    if (!(key in this.queue))
      this.queue[key] = [item];
    else
      this.queue[key].push(item);
  },
  
  endReached: false,
  
  audioEndReached: function() {
    if (this.endReached == true)
      return;
    
    this.endReached = true;
    
    //Util.error('DEBUG', "AUDIO END REACHED");
    if (window.archive) {
      window.archive.endReached(true);
      return;
    }
    
    if (!Player.beenOpened)
      Player.open();
  },
  
  callAPI: function(c,d,f) {
    return API.call(this.URL+'/'+c+(d ? '/'+d : ''), f);
  },
  callAPIRecurring: function(c,d,f) {
    Util.recur(d, function() {
      var ff = f ? f.call(this) : false;
      var url = this.URL+'/'+c+(ff?'/'+ff:'');
      //$('.cctype').html('Launching URL: '+url);
      //window.setTimeout("$('.cctype').html('');", 1000);
      API.call(url);/*, function() {
        var s = this;
        window.setTimeout(function(){ delete s }, 2000);
      })*/
    }, this)
  },
  findItem: function(id) {
    for (var i = 0; i < this.history.length; i++) {
      var item = this.history[i];
      if (item.ID == id) return item;
    }
    return false;
  },
  findItemByTime: function(time) {
    //Util.log('#### findByTime');
    var ret = false;
    for (var i = 0; i < this.history.length; i++) {
      var item = this.history[i];
      //ret = 'item.Time:'+item.Time+'<br>time:'+time+'<br>item.Time==time:'+(item.Time==time);
      if (item.Time == time) ret = item;
    }
    //if (time == 0 && ret) alert('FIND BY TIME 0: ');
    //Util.error('findByTime:',Util.str(ret));
    return ret;
  },
  frameURL: function() {
    return API.getBaseURL() + 'cynocast:'+this.id + '/frameurl';
  },
  getItemIndex: function(item) {
    var index = 0;
    for (var i = 0; i < this.history.length; i++) {
      if (this.history[i] == item) return index;
      if (this.history[i].URL != '') index ++;
    }
    return false;
  },
  hasAudio: function() {
    return this.info.FileID > 0 || this.info.HasStream;
  },
  lastItem: function() {
    for (var i = this.history.length - 1; i >= 0; --i)
      if (String(this.history[i].URL) != '') return this.history[i];
  
    return null;
  },
  lastURL: function() {
    var lastItem = this.lastItem();
    return lastItem ? lastItem.URL : null;
  },
  launchAtTime: function(sec) {
    var item = this.findItemByTime(sec);
    
    if (item)
      this.launch(item);
  },
  launch: function(item,i,ignoreCheck) {
    if (!item) return;
    var index = this.getItemIndex(item);
    
    // On demand client opens the player after last launch. 
    if (item == this.lastItem() && !this.live) {
      if (window.archive)
        if (!this.hasAudio())
          window.archive.endReached();
      else if (!Player.beenOpened)
        Player.openSoon(10);
    }
    
    if (item.Message != '' && item.Message != 'launch')
      return this.messageReceived(item);
    
    if (!ignoreCheck && (this.lastID >= item.ID || this.lastLaunchURL == item.URL))
      return;
    
    this.lastID = this.lastLaunchID = item.ID;
    this.lastLaunchURL = item.URL;
    
    // Only the live client should add items here. On demand clients add them
    // in API.handlers.History. However, on demand clients should select the item here.
    if (this.live)
      carousel.add(item, this.live);
    else
      carousel.selectItem(index);
    
    cynosound.play();
    
    if (item.URL.match(/userfiles\/images\/.+?(jpe?g|png|gif)$/i) && item.Params != '0')
      item.URL = API.getBaseURL()+'launchlistitem:'+item.Params+"/imageview.go";
    else if (item.URL.match(/\.swf$/i))
      item.URL = API.URL + 'assets/swfwrap.php?u=' + escape(item.URL);
    
    // If the params == 1, we have a live cynocast or some other URL we want to 
    // force out of the frame.
    if (item.Params == '1')
      top.location.href = item.URL;
    else
      $('#urlFrame').attr('src', item.URL);
    
    //carousel.selectItem(i||index);
  },
  launchFrameURL: function() {
    this.lastLaunchURL = this.frameURL();
    $('#urlFrame').attr('src', this.lastLaunchURL);
  },
  launchLastItem: function() {
    this.launch(this.history[this.history.length-1]);
  },
  load: function() {
    Util.log("BEGIN CYNOCAST LOAD");
    
    this.callAPI('info');
    var self = this;
    
    if (swfobject.hasFlashPlayerVersion('9.0.0'))
      window.HasFlash = true;
    else
      window.HasFlash = false;
    
    $(function(){
      if (self.live) {
        $('.hideOD').show();
        $('.hideLive').hide();
        
        // Only the Live player should check for new URLs.
        // If they have Flash 9, we use NetStreams to connect.
        if (window.HasFlash)
          window.timer.start();
        
        // Otherwise, we use HTTP as a last resort.
        else {
          self.callAPIRecurring('history', 2000, function() {
            return self.history.length ? (self.history[self.history.length-1].ID + 1) : 0;
          });
          window.HasFlash = false;
        }
      } else {
        $('.hideLive').show();
        $('.hideOD').hide();
        self.callAPI('history');
        // The On Demand player should always show the menu.
        Menu.setIsVisible(true);
      }
    });
    
    Util.log("END CYNOCAST LOAD");
  },
  
  // This method is only for On Demand CynoCasts.
  // Live CynoCasts are handled in updateQueue().
  messageReceived: function(item) {
    if (!this.live) {
      this.handlers.dispatch(item);
    } else {
      this.addToQueue(item);
      this.updateQueue();
    }
  },
  
  loadStreams: function() {
    Sound.setInputStream(this.inputStreamName);
    Sound.setOutputStream(this.outputStreamName);
    
    if (cynocast.live)
      Sound.sendMessage('Join', '');
  },
  
  // This is whether the queue has been updated since the client was opened (used by chatback, etc.)
  queueUpdated: false,
  
  setInfo: function(info) {
    this.info = info;
    this.file = new File(parseInt(info.FileID), this);
    
    if (info.HasStream)
      this.inputStreamName = 'CynoCast_'+info.ID;
    
    if (this.live)
      this.outputStreamName = 'CynoCast_Client_'+info.ID;
    
    this.loadStreams();
    
    if (info.ExtraURL == '')
      info.ExtraURL = info.MysiteURL;
    
    document.title = info.Type + ' CynoCast';
    
    $('#historyTab').html('<div>CYNOCAST</div>' + this.type());
    $('#loading').hide();
    $('#ccwrap').show();
    $('.idname').html(this.title());
    $('.pname').html(info.ProjectName);
    $('.ccname').html(info.Name);
    $('.cchref').attr('href', info.MysiteURL);
    $('.idhref').attr('href', info.ExtraURL);
    $('.cctype').html(document.title);
    
    if (window.cynocast.file)
      window.cynocast.file.playAudio();
  },
  
  updateQueue: function() {
    var q = this.queue;
    
    // This method is only for Live CynoCasts.
    // On Demand CynoCasts are handled in messageReceived().
    if (!this.live)
      return;
    //take care of chatback users first so they appear before messages on reload
    Util.each(q.chatbackuser, function(k,v) {
      Chatback.addUser(v.Params, parseInt(v.ID), v.AdminMode);
    });
    q.chatbackuser = [];
    
    Util.dispatchEach(q, {
      notFound: function(items) {
        if (items.length > 0)
          Util.log('UNHANDLED MESSAGE: '+Util.str(items));
      },
      
      mask: function(items) {
        for (var i = 0; i < items.length; i++) {
          var p = items[i].Params;
          if (p == 'show')
            $(window.coveringDiv()).show();
          else if (p == 'hide')
            $(window.coveringDiv()).hide();
          
          Util.log('MASK MESSAGE: PARAMS: '+p);
        }
      },
      player: function(items) {
        var playerOpen = Player.isOpen();
        Util.each(items, function(k,item) {
          playerOpen = Util.choose(item.Params, {
            hide: false,
            show: true,
            toggle: !playerOpen,
            notFound: playerOpen
          })
        })
        q.player = [];
        Player.setIsOpen(playerOpen);
        
        if (items.length >= 1)
          Player.beenOpened = true;
      },
      chatback: function(items) {
        var messages = Util.each(items, function(k,v) {
          Chatback.add(v.Params, v.Description, parseInt(v.ID), v.AdminMode);
        });
        q.chatback = [];
      },
      clear: function(items) {
        for (var i in items) {
          if (items[i].Params == 'chatback') {
            Chatback.clear();
            //q.chatback = [];
          }
          // Reset some variables in the case of the history being cleared
          if (items[i].Params == 'history') {
            cynocast.history = [];
            cynocast.lastID = 0;
            cynocast.lastLaunchID = 0;
            cynocast.lastLaunchURL = '';
            carousel.reset();
            Chatback.clear();
          }
          //Util.log("CLEAR MESSAGE: "+Util.str(items[i]));
        }
        if (items.length > 0)
          q.clear = [];
      },
      clientmenu: function(items) {
        var visible = Menu.isVisible();
        Util.each(items, function(k,item) {
          visible = Util.choose(item.Params, {
            show: true,
            hide: false,
            notFound: visible
          })
        })
        q.clientmenu = [];
        Menu.setIsVisible(visible);
      },
      launch: function(items) {
        if (cynocast.live) {
          carousel.selectItem(null);
          var lastItem = items.pop();
          Util.each(items, function(k,item){ carousel.add(item, true); });
          cynocast.launch(lastItem);
          cynocast.queue.launch = [];
          carousel.scrollToEnd();
        }
      },
      setchatbackprivacy: function(items) {
        var private = Chatback.isPrivate();
        var tabVisible = Chatback.tabVisible();
        Util.each(items, function(k,item) {
          if (item.Params == 'private')
            private = true;
          else if (item.Params == 'public')
            private = false;
          
          tabVisible = (item.Params != 'disable');
        });
        q.setchatbackprivacy = [];
        Chatback.setIsPrivate(private);
        Chatback.setTabVisible(tabVisible);
      }
    });
    
    // The menu should be visible by default
    if (Menu.isVisible() == null) 
      Menu.setIsVisible(true);
    
    this.queueUpdated = true;
  },
  
  // Clears the queue, then goes through the history and re-adds all items.
  resetQueue: function() {
    // Crude hackery to work around "this" occasionally being broken
    var self = window.cynocast;
    var addToQueue = function(){ self.addToQueue.apply(self,arguments) };
    Util.each(this.history, function(k,v){ addToQueue(v) });
  },
  
  title: function() {
    if (this.info.UseProjectName)
      return this.info.ProjectName + ' - ' + this.info.Name;
    else
      return this.info.Name;
  },
  
  toString: function() { return '['+this.type()+' CynoCast #'+this.id+': '+this.info.Name+']'; },
  
  type: function() { return this.live ? 'LIVE' : 'ON DEMAND' }
});var File = window.File = new Class({
  cynocast: null,
  id: null,
  info: {},
  sound: null,
  URL: null,
  
  init: function(id, cynocast) {
    if (id == 0) return null;
    if (cynocast) this.cynocast = cynocast;
    this.id = id;
    this.URL = 'file:' + this.id;
    this.refreshInfo();
  },
  
  audioUpdate: function(snd) {
    // If the sound exists we use it to sync the timer.
    if (this.cynocast && window.timer != null && snd.id && snd.isPlaying())
      timer.set(snd.position());
    
    // If the sound stops playing, show the player (on demand).
    if (this.cynocast && !this.cynocast.live && !snd.isPlaying()) {
      snd.removeCallback();
      this.cynocast.audioEndReached();
    }
  },
  
  callAPI: function(c,d,f) {
    return API.call(this.URL + '/' + c + (d ? '/' + d : ''), f);
  },
  
  fileURL: function() {
    return API.getBaseURL() + this.URL + '/fileurl.go';
  },
  
  playAudio: function() {
    if (!this.sound)
      this.sound = new Sound(this.fileURL());
    
    this.sound.setCallback(this.audioUpdate);
    this.sound.play();
  },
  
  refreshInfo: function() {
    //FIXME: figure out how to store this
    //this.callAPI('info');
  },
  
  toString: function() {
    return '[File #' + this.id + ': ' + this.info.Name + ']';
  }
});var c = window.carousel = {
  sel:      '#history',
  itemSize: 66,
  visible:  5,
  nextSel:  '#btnNext',
  prevSel:  '#btnPrev',
  
  load: function() {
    $(c.sel).css({
      height: (c.itemSize * c.visible)+'px'
    });
    
    c.scrollTo(0);

    $(c.prevSel).click(function(){ c.scrollBy(-c.visible) });
    $(c.nextSel).click(function(){ c.scrollBy(c.visible) });
  },
  add: function(h,isLive) {
    var html = c.formatItem(h,isLive);
    var item = $(html);
    
    $(c.sel+' li').removeClass('selected');
    
    if (is.ie) {
      var ul = $("<ul/>");
      ul[0].innerHTML += html;
      item = $('#hi'+h.ID, ul);
    }
    
    $(c.sel+' ul').eq(0).append(item);
    
    // On demand clients attach a handler that launches the clicked item.
    if (!isLive)
      item.click(launchClickedItem).css('cursor', 'pointer');
    
    $('.saveBookmark').unbind('click').click(function(){ try{saveBookmark.apply(this)} catch(e){alert(e.name+':\n'+e.message)} return false; });
      
    return item;
  },
  formatItem: function(item,isLive) {
    if (typeof item.URL == 'undefined') return '';
    var d = new Date;
    if (isLive)
      d.setTime(item.Time * 1000);
    else
      d.setTimeOnly(item.Time);
    var time = d.format(isLive ? 'H:m:s a<br>M/D/Y' : 'H:m:s');
    //Util.error('COMPARE',cynocast.lastLaunchID+'; '+cynocast.lastLaunchURL+'<br>'+Util.str(item)+'<br>'+(cynocast.lastLaunchURL == item.URL));
    var cls = cynocast.lastLaunchURL == item.URL ? ' class="selected">' : '>';
    var html = '<li id="hi' + item.ID + '"' + cls +
      '<div class="time">'+ time + '</div>'+
      '<div class="url"><table><tr><td valign="middle">'+
        '<h3 id="urltitle_' + item.ID + '">' + item.Title + '</h3>'+
        '<a target="_new" href="' + item.URL + '" title="'+item.URL+'" class="nw">' + item.URL + '</a>'+
      '</td></tr></table></div>'+
      '<a href="#'+item.ID+'" class="saveBookmark">&nbsp;</a>'+
      '</div></li>';
    
    return html;
  },
  get: function(){ return $(c.sel+' ul') },
  size: function(){ return $(c.sel+' li').length },
  pos: function() {
    return parseInt(c.get().css('margin-top')) / -c.itemSize;
  },
  reset: function() {
    $(c.sel+' li').remove();
    c.scrollTo(0);
  },
  scrollBy: function(i) {
    c.scrollTo(c.pos() + i);
  },
  scrollTo: function(i) {
    with(Math) {
      $(c.prevSel+','+c.nextSel).removeClass('disabled');
      var s = c.size(), top = min(0, -c.itemSize * min(s - c.visible, i));
      c.get().animate({ marginTop:top });
      var p = top / -c.itemSize;
      if (p == 0)
        $(c.prevSel).addClass('disabled'); 
      if (p == s - c.visible)
        $(c.nextSel).addClass('disabled');
    }
  },
  scrollToEnd: function() { c.scrollTo(c.size() - 1) },
  selectItem: function (i) {
    $(c.sel + ' li').removeClass('selected');
    if (i != null) {
      $(c.sel + ' li').eq(i).addClass('selected');
      c.scrollTo(i);
    }
  },
  selectLastItem: function () { c.selectItem(c.size() - 1) }
};
var Player = window.Player = {
  beenOpened: false,
  
  close:  function(cb) { Player.setIsOpen(false,cb) },
  open:   function(cb) { Player.setIsOpen(true,cb) },
  
  openSoon: function(delay,cb) {
    window.setTimeout(function(){ Player.open(cb) }, delay * 1000);
  },
  
  toggle: function(cb) { Player.setIsOpen(!Player.isOpen(),cb); return false; },
  
  isOpen: function() { return $('#b').width() == 292; },
  
  // Hides the player instantly with no animation.
  hide: function() {
    $('.mute_s').show();
    $('#chatback').css('right', 25);
    $('#playerToggle').html('Show Player');
    $('#b').hide().css({ padding:0, width:0 });
    $('#a').css('right', 0);
  },
  
  setIsOpen: function(visible,callback) {
    var a = $('#a'), b = $('#b'), cb = $('#chatback'), d = 400;
    var doneFunc = function() {
      $('#playerToggle').html((visible?'Hide':'Show')+' Player');
      if (callback && callback instanceof Function)
        callback();
      b.css('width',visible?292:0);
      a.css('right',visible?308:0);
    };
    
    if (!visible) {
      $('.mute_s').fadeIn(d);
      b.animate({ width:0 }, d, function() { b.hide().css('padding', 0); });
      a.animate({ right:0 }, d, doneFunc);
      cb.animate({ right:25 }, d);
    } else {
      $('.mute_s').fadeOut(d);
      b.css('padding',5).show().animate({ width:292 }, d);
      a.animate({ right:308 }, d, doneFunc);
      cb.animate({ right:333 }, d);
    }
  }
};var Chatback = window.Chatback = {
  cachedIDs: {},
  
  close:  function() { Chatback.setIsOpen(false); return false; },
  open:   function() { Chatback.setIsOpen(true); return false; },
  toggle: function() { Chatback.setIsOpen(!Chatback.isOpen()); return false; },
  
  username: function() {
    return window.prefs.get('username');
  },
  
  setUsername: function(name) {
    window.prefs.set('username', name);
    Sound.player.addCynoCastMessage('chatbackuser', { Params: name });
    window.cynocast.callAPI('sendmessage/chatbackuser/'+escape(encodeURIComponent(name)));
    Chatback.updateSendBox();
  },
  
  add: function(msg, name, id, adminMode) {
    if (id in Chatback.cachedIDs) return;
    
    var options = this.getOptions(adminMode, name);
    if (Chatback.isPrivate() && options.private) return;
    
    Chatback.cachedIDs[id] = true;
    msg = Util.autoLink(msg);
    
    Chatback.appendHTML('<div class="'+options.classes+'"><span style="color:'+options.color+'">'+(name||'Somebody')+' says:</span> '+msg+'</div>');
  },
  
  addUser: function(user, id, adminMode) {
    if (id in Chatback.cachedIDs) return;
    if (Chatback.isPrivate() && user != Chatback.username()) return;
    
    Chatback.cachedIDs[id] = true;
    
    Chatback.appendHTML('<div class="cbItem cbPrivate"><span style="color:#e66203">'+user+' is on ChatBack</span></div>');
  },
  
  appendHTML: function(html) {
    var textView = $('#cbText');
    textView.append(html);
    // We don't need to actually animate anything until the CynoCast has fully loaded.
    if (window.cynocast.queueUpdated) {
      Chatback.open();
      textView.animate({scrollTop:textView[0].scrollHeight}, 10);
    }
  },
  
  clear: function() {
    Chatback.cachedIDs = {};
    $('#cbText .cbItem').remove();
    Chatback.close();
  },
  
  getOptions: function(adminMode, name) {
    if (adminMode)
      return { color:'#9900cc', classes:'cbItem', private:false };
    if (name == Chatback.username())
      return { color:'#ff0000', classes:'cbItem', private:false };
    
    return { color:'#0000ff', classes:'cbItem cbPrivate', private:true };
  },
  
  getTab: function() { return $('#cbTab'); },
  
  _tabVisible: true,
  tabVisible: function() { return Chatback._tabVisible; },
  
  hideTab: function() { this.setTabVisible(false); },
  showTab: function() { this.setTabVisible(true); },
  
  setTabVisible: function(visible) {
    if (Chatback._tabVisible != visible) {
      Chatback._tabVisible = visible;
      this.getTab()[visible?'show':'hide']();
    }
  },
  
  _isOpen: false,
  isOpen: function() { return Chatback._isOpen },
  
  _isPrivate: false,
  isPrivate: function() { return Chatback._isPrivate },
  
  sendFromForm: function() {
    var text = $('#cbInput').val();
    var username = Chatback.username();
    $('#cbInput').val('');
    
    if (!username)
      Chatback.setUsername(text);
    else {
      var URL = 'chatback/'+Util.toHex(text)+'/'+Util.toHex(username);
      Sound.player.addCynoCastMessage('chatback', { Params: text, Description: username });
      window.cynocast.callAPI(URL);
    }
    return false;
  },
  
  setIsOpen: function(visible) {
    if (Chatback._isOpen != visible) {
      Chatback._isOpen = visible;
      //if (visible) $('#cbBox').show()
      $('#cbForm').show().animate({ height:(visible?200:0) }, 400, function() {
        //if (visible)
         // $('#cbInput').focus(function(e){ alert([e,this]) });
        /*else
          $('#cbBox').hide()*/
      });
    }
  },
  
  setIsPrivate: function(private) {
    if (Chatback._isPrivate != private) {
      Chatback._isPrivate = private;
      if (private)
        $('.cbPrivate').hide();
      else
        $('.cbPrivate').show();
    }
  },
  
  updateSendBox: function() {
    if (!window.prefs.isSet('username')) {
      $('#cbButton').val('Send Name');
      $('#cbInput').val('Please Enter Your Name').css('color', '#333');
    } else {
      $('#cbButton').val('Send Message');
      $('#cbInput').val('').css('color', '#000');
    }
  }
};var FailedAssertionError = function(message) {
  this.name = 'FailedAssertionError';
  this.message = message;
}

FailedAssertionError.prototype.toString = function() {
  return this.name + ': ' + this.message;
};

if (!assert) {
  var assert = function(c, m) {
    if (!c) throw new FailedAssertionError(m || '');
  }
}
Util.extend(assert, {
  equals: function(a,b) {
    try {
      assert(a == b, Util.str(a)+' != '+Util.str(b));
    } catch(e) {
      console.log('EXCEPTION CAUGHT: %o',e);
      throw e;
    }
  }
})

var Tests = window.Tests = {
  tests: {
    cynocastItems: function(K) {
      assert(cynocast, "CynoCast Object Doesn't Exist");
      assert(cynocast.history, "CynoCast History Object Doesn't Exist");
      
      K(true);
    },
    //alert: function() { Util.alert('Just','a','test'); return true },
    messageQueue: function(K) {
      cynocast.resetQueue();
      var total = 0;
      Util.each(cynocast.queue,function(k,v){ total += v.length });
      K(total == cynocast.history.length);
    },
    player: function(K) {
      assert(Player, "Player object doesn't exist");
      
      // NOTE: The anonymous functions below should get called after the animation completes.
      // Since this function will have returned by then, these exceptions won't be caught
      // by the test rig, but by the browser.
      Player.open(function() {
        assert(Player.isOpen(), "Player should be open after Player.open()");
        Player.close(function() {
          assert(!Player.isOpen(), "Player should be closed after Player.close()");
          Player.toggle(function() {
            assert(Player.isOpen(), "Player should be open after Player.toggle()");
            K(true);
          });
        });
      });
    },
    updateQueue: function(K) {
      cynocast.updateQueue();
      K(true);
    },
    waitUntil: function(K) {
      var done = false;
      var abort = Util.waitUntil(function(){ return done }, function(){ K(true) });
      assert(abort instanceof Function, "Util.waitUntil should return abort function");
      done = true;
    },
    utilChoose: function(K) {
      var choices = {
        1: 'one',
        2: 'two',
        notFound: 'notFound'
      };
      assert.equals(Util.choose(3,choices), 'notFound');
      assert.equals(Util.choose(2,choices), 'two');
      K(true);
    }
  },
  abort: function(){/* PLACEHOLDER */},
  run: function() {
    var errors = [];
    var testCount = 0;
    
    console.log('##### STARTING TESTS: %o', Util.keys(Tests.tests));
    
    function resultHandler(message) {
      return function(success) {
        console.log('##### COMPLETED TEST %o', message);
        testCount--;
        if (!success) errors.push(new FailedAssertionError(message || "Unknown"));
      };
    }
    
    Util.each(Tests.tests, function(name, func) {
      testCount++;
      try {
        func(resultHandler(name));
      } catch(e) {
        errors.push(e);
      }
    });
    
    Tests.abort = Util.waitUntil(function(){ return testCount == 0 },function() {
      if (errors.length > 0)
        alert('TESTS FAILED\n===========\n\n'+errors.join('\n'))
      else
        alert('ALL TESTS PASSED!');
    });
  },
  assert: assert
};var Menu = window.Menu = {
  hider: null,
  _isVisible: null,
  
  get: function() { return $('#dropdown') },
  
  hide: function() {
    if (Menu.isOpen())
      Menu.get().slideUp(200);
    return true;
  },
  
  isOpen: function() {
    return !Menu.get().is(':hidden');
  },
  
  isVisible: function() { return Menu._isVisible; },
  
  show: function() {
    if (!Menu.hider)
      Menu.hider = Util.soon(3000, Menu.hide);
    Menu.get().slideDown(200);
    Menu.hider.reset();
    return false;
  },
  
  setIsVisible: function(visible) {
    if (Menu._isVisible != visible) {
      Menu._isVisible = visible;
      if (visible) {
        $('.menu').show();
        $('.mute_s').css('margin-right',0);
      } else {
        $('.menu').hide();
        $('.mute_s').css('margin-right','40px');
      }
    }
  },
  
  toggle: function() {
    if (Menu.isOpen())
      Menu.hide();
    else
      Menu.show();
    
    return false;
  }
};
var TalkBack = window.TalkBack = {
  streamName: null,
  clientID: 1,
  hasLock: false,
  micStatus: null,
  
  allStreamsUsed: function() {
    alert('Sorry, all available TalkBack channels are being used.');
  },
  
  setClientVisible: function(visible) {
    var width = 213, height = 136;
    var sw = $('#soundwrap');
    var val = visible ? Math.floor(($('body').width() - width) / 2) : 0;
    sw.css({
      left: val + 'px',
      width: (visible ? width : 1) + 'px',
      height: (visible ? height : 1) + 'px'
    });
  },
  
  setHasLock: function(lock) {
    Util.log("SET LOCK: "+lock+"; STREAM: "+TalkBack.streamName);
    TalkBack.hasLock = lock;
  },
  
  toggleLock: function() {
    if (TalkBack.streamName == null) {
      TalkBack.allStreamsUsed();
      return;
    }
    if (TalkBack.hasLock) {
      $('#tb').html('TalkBack');
      Sound.player.unlockStream();
    } else {
      if (TalkBack.micStatus == null) {
        Sound.player.setUpMicrophone();
        TalkBack.setClientVisible(true);
      } else {
        TalkBack.lockStreamFinish();
      }
    }
  },
  
  lockStreamFinish: function() {
    var locked = Sound.player.lockStream(TalkBack.streamName);
    if (locked) {
      $('#tb').html('Stop&nbsp;Talking');
    } else {
      TalkBack.allStreamsUsed();
    }
  }
};

window.micStatus = function(status) {
  TalkBack.micStatus = status;
  TalkBack.setClientVisible(false);
  
  Util.log('MIC STATUS: '+status);
  
  if (status == "Microphone.Unmuted")
    TalkBack.lockStreamFinish();
  else
    alert("You need to grant permission to use your microphone to the Flash Player, if you want to TalkBack.");
};
// The following are handlers that are bound to UI elements in the client.

function toggleMute() {
  Sound.toggleIsMuted();
  return false;
}

function updateMute(m) {
  Sound.updateMuteButtons(m);
}

function swapFunc(a,b) {
  return function() {
    $(this).addClass('selTab').siblings().removeClass('selTab');
    $(a).slideUp(400, function() {
      $(this).hide();
      $(b).slideDown(400);
    });
    return false;
  }
}

function saveBookmark() {
  var item = cynocast.findItem(this.href.split('#')[1]);
  if (item) {
    bookmarks.add(item.Title,item.URL);
    $(this).fadeOut(200);
  }
  return false;
}

function emailFriend() {
  var url = API.URL + 'assets/emailafriend.php';
  var l = (screen.width - 400) /2, t = (screen.height - 250) /2;
  var w = window.open(url + '?loc=' + location.href, '_blank', 'height=250,width=400,top='+t+',left='+l+',toolbar=no,location=no,menubar=no,resizable=no,status=no,scrollbars=no');
  return false;
}

function flagCynocast() {
  alert('TODO');
  return false;
}

function saveCynoCastAsFavorite() {
  alert('TODO');
  return false;
}

function launchClickedItem(e) {
  if (e.target.className == 'nw') 
    return true;
  
  var id = Number(this.id.split('hi')[1]);
  //Util.error('test',id)
  //Util.log('launchClickedItem: '+id+' e.target:'+e.target+' e.currentTarget:'+e.currentTarget);
  window.cynocast.launch(window.cynocast.findItem(id), null, true);
  return false;
}

function timerUpdate(s) {
  var c = window.cynocast;
  if (!c || c.live) return;
  c.launchAtTime(Math.floor(s/1000));
}

// When the DOM is ready, the handlers get attached via jQuery.
$(function() {
  Util.log("BEGIN CLIENT HANDLERS");
  //Util.log('##### onDOMReady');
  
  Player.hide();
  
  $('#loading small').html('<a href="javascript:history.go(0)">Click here if the CynoCast does not start.</a>').hide();
  window.setTimeout("$('#loading small').show()", 3000);
  
  $('.menu, .menu *, #dropdown, #dropdown *').mouseover(Menu.show);
  window.timer = new Timer(false, '.timer b', timerUpdate);
  
  $('#a').css('background', '#fff');
  
  window.cynocast.launchFrameURL();
  
  // Click handlers
  $('#historyTab').click(swapFunc("#bookmarks","#historyWrap"));
  $('#bookmarksTab').click(swapFunc("#historyWrap","#bookmarks"));
  $('#dropdown a').click(Menu.hide);
  $('#playerToggle').click(Player.toggle);
  $('.mute,.mute_s').click(toggleMute);
  $(".faveBtn").click(saveCynoCastAsFavorite);
  $(".emailBtn").click(emailFriend);
  $(".flagBtn").click(flagCynocast);
  $('.menu').click(Menu.toggle);
  $('#cbTab').click(Chatback.toggle);
  
  $('#cbInput').focus(function(e) {
    //var k = String.fromCharCode(e.charCode ? e.charCode : e.keyCode);
    if (this.value.match(/Please/))
      this.value = '';
  });
  
  Chatback.updateSendBox();
  $('#cbForm').submit(Chatback.sendFromForm);
  //$('#cbBox').css('opacity', 0.85);
  
  // Hide On Demand ChatBack for now.
  if (!cynocast.live)
    Chatback.hideTab();
  
  window.cynosound = Sound.cynoSound;
  window.carousel.load();
  window.bookmarks = new Bookmarks;
  
  updateMute();
  
  $('#wrap').show();
  
  Util.log("END CLIENT HANDLERS");
});
})()