(function($){
  trace = function(msg){
    // if (!window.console) return
    // var len = arguments.length
    // var args = []
    // for (var i=0; i<len; i++) args.push("arguments["+i+"]")
    // eval("console.log("+args.join(",")+")")
  }  


  // $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/
  $$ = function(node) {
    var data = $(node).data("$$");
    if (data) {
      return data;
    } else {
      data = {};
      $(node).data("$$", data);
      return data;
    }
  };
  
  /* Nano Templates (Tomasz Mazur, Jacek Becela) */
  $.nano = function(template, data){
    return template.replace(/\{([\w\-\.]*)}/g, function(str, key){
      var keys = key.split("."), value = data[keys.shift()]
      $.each(keys, function(){ 
        if (value.hasOwnProperty(this)) value = value[this] 
        else value = str
      })
      return value
    })
  }
  
  objcopy = function(old){
    if (old===undefined) return undefined
    if (old===null) return null
    
    if (old.parentNode) return old
    switch (typeof old){
      case "string":
      return old.substring(0)
      break
      
      case "number":
      return old + 0
      break
      
      case "boolean":
      return old === true
      break
    }

    var newObj = ($.isArray(old)) ? [] : {}
    $.each(old, function(ik, v){
      newObj[ik] = objcopy(v)
    })
    return newObj
  }
  
  
  objcmp = function(a, b, strict_ordering){
    if (!a || !b) return a===b // handle null+undef
    if (typeof a != typeof b) return false // handle type mismatch
    if (typeof a != 'object'){
      // an atomic type
      return a===b
    }else{
      // a collection type
      
      // first compare buckets
      if ($.isArray(a)){
        if (!($.isArray(b))) return false
        if (a.length != b.length) return false
      }else{
        var a_keys = []; for (var k in a) a_keys.push(k)
        var b_keys = []; for (var k in b) b_keys.push(k)
        if (!strict_ordering){
          a_keys.sort()
          b_keys.sort()
        }
        if (a_keys.join(',') !== b_keys.join(',')) return false
      }
      
      // then compare contents
      var same = true
      $.each(a, function(ik){
        var diff = objcmp(a[ik], b[ik])
        same = same && diff
        if (!same) return false
      })
      return same
    }
  }

  objkeys = function(obj){
    var keys = []
    $.each(obj, function(k,v){ keys.push(k) })
    return keys
  }

  uniq = function(arr){
    // keep in mind that this is only sensible with a list of strings
    // anything else, objkey type coercion will turn it into one anyway
    var len = arr.length
    var set = {}
    for (var i=0; i<len; i++){
      set[arr[i]] = true
    }

    return objkeys(set) 
  }


  capcase = function(str){
    str = str || ""
    var skip_words = /^(of|to|the|in|into|for|a)$/
    var bits = $.map(str.split(" "), function(word, i){
      if (i!=0 && word.match(skip_words)) return word.toLowerCase()      
      else return word.substr(0,1).toUpperCase() + word.substr(1)
    })
    return bits.join(" ")
  }


  createCookie = function(name,value,hrs) {
  	if (hrs) {
  		var date = new Date();
  		date.setTime(date.getTime()+(hrs*60*60*1000));
  		var expires = "; expires="+date.toGMTString();
  	}
  	else var expires = "";
  	document.cookie = name+"="+value+expires+"; path=/";
  }

  readCookie = function(name) {
  	var nameEQ = name + "=";
  	var ca = document.cookie.split(';');
  	for(var i=0;i < ca.length;i++) {
  		var c = ca[i];
  		while (c.charAt(0)==' ') c = c.substring(1,c.length);
  		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  	}
  	return null;
  }

  eraseCookie = function(name) {
  	createCookie(name,"",-1);
  }
  
  Timer = function(delay, repeat){
    var _timer = null
    var _count = 0
    
    var that = {
      delay:delay,
      repeat:repeat||0,
      running:false,
      
      init:function(){
        return that
      },
      
      start:function(){
        if (_timer!==null) return
        that.running = true
        _count = 0
        _timer = window.setInterval(that._tick, delay)
      },
      
      _tick:function(e){
        _count++
        $(that).trigger({type:'timer', count:_count})
        if (that.repeat!==0 && _count>=that.repeat) that.stop()
      },
      
      stop:function(){
        if (_timer===null) return
        that.running = false
        window.clearInterval(_timer)
        _timer = null
        _count = 0
      },
      
      reset:function(){
        var keep_running = (that._timer!==null)
        that.stop()
        if (keep_running) that.start()
      }
    }
    
    return that.init()
  }

  Cache = function(numWorkers){
    numWorkers = numWorkers || 2
    var that = {
      workers:[],
      next:0,
      init:function(){
        for (var i=0; i<numWorkers; i++){
          var w = Worker()
          $(w).bind('fetch', that._emit)
          that.workers.push(w)
        }
        $('body').append('<div id="renderbox"></div>')
        return that
      },
      fetch:function(imgUrl, nice){
        that.workers[that.next].fetch(imgUrl, nice)
        that.next = (that.next+1) % that.workers.length
      },
      nice:function(imgUrl){
        $.each(that.workers, function(i, worker){
          worker.nice(imgUrl)
        })
      },
      _emit:function(e){
        // e: {type:'fetch', url:imgUrl, img:_cache[imgUrl]}
        $(that).trigger(e)
      },
    }
    
    return that.init()    
  }
  

  Worker = function(){
    var _queue = [] // url strings yet to be fetched
    var _cache = {} // Image objects keyed by url
    var _fetching = null // {url:"", img:Image()}
    
    // will check the _fetching image's completeness since the load event can't be trusted
    var _watchdog = Timer(100) 
    
    var that = {
      init:function(){
        $(_watchdog).bind('timer', that._checkCompletion)
        return that
      },
      fetch:function(imgUrl, nice){
        if (typeof(imgUrl)!='string') return
        
        if (imgUrl in _cache){
          // fire the loaded event immediately
          that._emit(imgUrl)
          return
        }else if ($.inArray(imgUrl, _queue)==-1){
          // not already in the queue, so add it
          nice = nice || false
          if (nice) _queue.unshift(imgUrl)
          else _queue.push(imgUrl)
        }
        if (!_fetching && _queue.length>0) that._loadNext()
      },
      nice:function(imgUrl){
        // move url to front of queue
        var urlIdx = $.inArray(imgUrl, _queue)
        if (urlIdx>0){
          _queue.splice(urlIdx,1)
          _queue.unshift(imgUrl)
        }
      },
      
      _loadNext:function(){
        var nextUrl = _queue.shift()
        var nextImg = new Image()
        nextImg.src = nextUrl
        if (nextImg.complete){
          _cache[nextUrl] = nextImg
          _fetching = {url:nextUrl, img:nextImg}
          that._loadComplete()
        }else{
          _fetching = {url:nextUrl, img:nextImg}
          _watchdog.start()
        }
      },
      
      _checkCompletion:function(e){
        if (_fetching.img.complete){
          _watchdog.stop()
          that._loadComplete()
        }
      },
      
      _loadComplete:function(e){
        // trace(_fetching.url,'done')
        $("#renderbox").append("<img src='"+_fetching.url+"'>")
        _cache[_fetching.url] = _fetching.img
        that._emit(_fetching.url)
        _fetching = null
        
        if (_queue.length>0) that._loadNext()
      },
      _emit:function(imgUrl){
        $(that).trigger({type:'fetch', url:imgUrl, img:_cache[imgUrl]})
      }
    }
    
    return that.init()    
  }
  
  // monkey-patch a way to read from /dbname/_file into $.couch
  $.couch._orig_db = $.couch.db
  $.couch.db = function(name){
    var newDb = $.couch._orig_db(name)
    newDb.files = function(docId, attachment, options, ajaxOptions){
      // when called with the attachment name omitted, shift the args
      if (typeof attachment!='string' && !ajaxOptions){
        ajaxOptions = options, options = attachment, attachment = null
      }
      var path = encodeDocId(docId)
      if (attachment) path += '/' + attachment // BUG: needs escaping too...

      if (window.STATIC_DB){
        path += '/metadata.json'
        var fileUri = "/db/"+name+"/"
      }else{
        fileUri = "/_file/"+name+"/"
      }
      
      ajax({url: fileUri + path},
        options,
        "The document could not be retrieved",
        ajaxOptions
      );
    }
    return newDb
  }

  function ajax(obj, options, errorMessage, ajaxOptions) {
    options = $.extend({successStatus: 200}, options);
    
    var method = {}
    if ('delete' in options) $.extend(method, {type:"POST", data:{"delete":true}})
    else if ('rename' in options) $.extend(method, {type:"POST", data:{"rename":options.rename}})
    errorMessage = errorMessage || "Unknown error";


    $.ajax($.extend($.extend($.extend({
      type: "GET", 
      dataType: "json",
      data: "",
      complete: function(req) {
        var resp = $.httpData(req, "json");
        if (req.status == options.successStatus) {
          if (options.success) options.success(resp);
        } else if (options.error) {
          options.error(req.status, resp.error, resp.reason);
        } else {
          alert(errorMessage + ": " + resp.reason);
        }
      }
    }, obj), ajaxOptions), method));
  }

  function toJSON(obj) {
    return obj !== null ? JSON.stringify(obj) : null;
  }

  function encodeDocId(docID) {
    var parts = docID.split("/");
    if (parts[0] == "_design") {
      parts.shift();
      return "_design/" + parts[0] + "/" + encodeURIComponent(parts.slice(1).join('/'));
    }
    return encodeURIComponent(docID);
  };
  

})(jQuery)
