// -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; -*-
// vim:set ft=javascript ts=2 sw=2 sts=2 cindent:
var Util = (function(window, undefined) {

    var fontLoadTimeout = 5000; // 5 seconds

    var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

    var isMac = navigator.platform == 'MacIntel'; // XXX should we go broader?

    var cmp = function(a,b) {
      return a < b ? -1 : a > b ? 1 : 0;
    }

    var cmpArrayOnFirstElement = function(a,b) {
      a = a[0];
      b = b[0];
      return a < b ? -1 : a > b ? 1 : 0;
    }

    var unitAgo = function(n, unit) {
      if (n == 1) return "" + n + " " + unit + " ago";
      return "" + n + " " + unit + "s ago";
    };

    var formatTimeAgo = function(time) {
      if (time == -1000) {
        return "never"; // FIXME make the server return the server time!
      }

      var nowDate = new Date();
      var now = nowDate.getTime();
      var diff = Math.floor((now - time) / 1000);
      if (!diff) return "just now";
      if (diff < 60) return unitAgo(diff, "second");
      diff = Math.floor(diff / 60);
      if (diff < 60) return unitAgo(diff, "minute");
      diff = Math.floor(diff / 60);
      if (diff < 24) return unitAgo(diff, "hour");
      diff = Math.floor(diff / 24);
      if (diff < 7) return unitAgo(diff, "day");
      if (diff < 28) return unitAgo(Math.floor(diff / 7), "week");
      var thenDate = new Date(time);
      var result = thenDate.getDate() + ' ' + monthNames[thenDate.getMonth()];
      if (thenDate.getYear() != nowDate.getYear()) {
        result += ' ' + thenDate.getFullYear();
      }
      return result;
    }

    var realBBox = function(span) {
      var box = span.rect.getBBox();
      var chunkTranslation = span.chunk.translation;
      var rowTranslation = span.chunk.row.translation;
      box.x += chunkTranslation.x + rowTranslation.x;
      box.y += chunkTranslation.y + rowTranslation.y;
      return box;
    }

    var escapeHTML = function(str) {
      return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }

    var escapeHTMLandQuotes = function(str) {
      return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');
    }

    var escapeHTMLwithNewlines = function(str) {
      return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br/>');
    }

    var escapeQuotes = function(str) {
      // we only use double quotes for HTML attributes
      return str.replace(/\"/g,'&quot;');
    }

    var getSpanLabels = function(spanTypes, spanType) {
      var type = spanTypes[spanType];
      return type && type.labels || [];
    }

    var spanDisplayForm = function(spanTypes, spanType) {
      var labels = getSpanLabels(spanTypes, spanType);
      return labels[0] || spanType;
    }

    var getArcLabels = function(spanTypes, spanType, arcType, relationTypesHash) {
      var type = spanTypes[spanType];
      var arcTypes = type && type.arcs || [];
      var arcDesc = null;
      // also consider matches without suffix number, if any
      var noNumArcType;
      if (arcType) {
          var splitType = arcType.match(/^(.*?)(\d*)$/);
          noNumArcType = splitType[1];
      }
      $.each(arcTypes, function(arcno, arcDescI) {
        if (arcDescI.type == arcType || arcDescI.type == noNumArcType) {
          arcDesc = arcDescI;
          return false;
        }
      });
      // fall back to relation types for unconfigured or missing def
      if (!arcDesc) {
        arcDesc = $.extend({}, relationTypesHash[arcType] || relationTypesHash[noNumArcType]);
      }
      return arcDesc && arcDesc.labels || [];
    }

    var arcDisplayForm = function(spanTypes, spanType, arcType, relationTypesHash) {
      var labels = getArcLabels(spanTypes, spanType, arcType, relationTypesHash);
      return labels[0] || arcType;
    }

    // TODO: switching to use of $.param(), this function should
    // be deprecated and removed.
    var objectToUrlStr = function(o) {
      a = [];
      $.each(o, function(key,value) {
        a.push(key+"="+encodeURIComponent(value));
      });
      return a.join("&");
    }

    // color name RGB list, converted from
    // http://www.w3schools.com/html/html_colornames.asp
    // with perl as
    //     perl -e 'print "var colors = {\n"; while(<>) { /(\S+)\s+\#([0-9a-z]{2})([0-9a-z]{2})([0-9a-z]{2})\s*/i or die "Failed to parse $_"; ($r,$g,$b)=(hex($2),hex($3),hex($4)); print "    '\''",lc($1),"'\'':\[$r,$g,$b\],\n" } print "};\n" '
    var colors = {
        'aliceblue':[240,248,255],
        'antiquewhite':[250,235,215],
        'aqua':[0,255,255],
        'aquamarine':[127,255,212],
        'azure':[240,255,255],
        'beige':[245,245,220],
        'bisque':[255,228,196],
        'black':[0,0,0],
        'blanchedalmond':[255,235,205],
        'blue':[0,0,255],
        'blueviolet':[138,43,226],
        'brown':[165,42,42],
        'burlywood':[222,184,135],
        'cadetblue':[95,158,160],
        'chartreuse':[127,255,0],
        'chocolate':[210,105,30],
        'coral':[255,127,80],
        'cornflowerblue':[100,149,237],
        'cornsilk':[255,248,220],
        'crimson':[220,20,60],
        'cyan':[0,255,255],
        'darkblue':[0,0,139],
        'darkcyan':[0,139,139],
        'darkgoldenrod':[184,134,11],
        'darkgray':[169,169,169],
        'darkgrey':[169,169,169],
        'darkgreen':[0,100,0],
        'darkkhaki':[189,183,107],
        'darkmagenta':[139,0,139],
        'darkolivegreen':[85,107,47],
        'darkorange':[255,140,0],
        'darkorchid':[153,50,204],
        'darkred':[139,0,0],
        'darksalmon':[233,150,122],
        'darkseagreen':[143,188,143],
        'darkslateblue':[72,61,139],
        'darkslategray':[47,79,79],
        'darkslategrey':[47,79,79],
        'darkturquoise':[0,206,209],
        'darkviolet':[148,0,211],
        'deeppink':[255,20,147],
        'deepskyblue':[0,191,255],
        'dimgray':[105,105,105],
        'dimgrey':[105,105,105],
        'dodgerblue':[30,144,255],
        'firebrick':[178,34,34],
        'floralwhite':[255,250,240],
        'forestgreen':[34,139,34],
        'fuchsia':[255,0,255],
        'gainsboro':[220,220,220],
        'ghostwhite':[248,248,255],
        'gold':[255,215,0],
        'goldenrod':[218,165,32],
        'gray':[128,128,128],
        'grey':[128,128,128],
        'green':[0,128,0],
        'greenyellow':[173,255,47],
        'honeydew':[240,255,240],
        'hotpink':[255,105,180],
        'indianred':[205,92,92],
        'indigo':[75,0,130],
        'ivory':[255,255,240],
        'khaki':[240,230,140],
        'lavender':[230,230,250],
        'lavenderblush':[255,240,245],
        'lawngreen':[124,252,0],
        'lemonchiffon':[255,250,205],
        'lightblue':[173,216,230],
        'lightcoral':[240,128,128],
        'lightcyan':[224,255,255],
        'lightgoldenrodyellow':[250,250,210],
        'lightgray':[211,211,211],
        'lightgrey':[211,211,211],
        'lightgreen':[144,238,144],
        'lightpink':[255,182,193],
        'lightsalmon':[255,160,122],
        'lightseagreen':[32,178,170],
        'lightskyblue':[135,206,250],
        'lightslategray':[119,136,153],
        'lightslategrey':[119,136,153],
        'lightsteelblue':[176,196,222],
        'lightyellow':[255,255,224],
        'lime':[0,255,0],
        'limegreen':[50,205,50],
        'linen':[250,240,230],
        'magenta':[255,0,255],
        'maroon':[128,0,0],
        'mediumaquamarine':[102,205,170],
        'mediumblue':[0,0,205],
        'mediumorchid':[186,85,211],
        'mediumpurple':[147,112,216],
        'mediumseagreen':[60,179,113],
        'mediumslateblue':[123,104,238],
        'mediumspringgreen':[0,250,154],
        'mediumturquoise':[72,209,204],
        'mediumvioletred':[199,21,133],
        'midnightblue':[25,25,112],
        'mintcream':[245,255,250],
        'mistyrose':[255,228,225],
        'moccasin':[255,228,181],
        'navajowhite':[255,222,173],
        'navy':[0,0,128],
        'oldlace':[253,245,230],
        'olive':[128,128,0],
        'olivedrab':[107,142,35],
        'orange':[255,165,0],
        'orangered':[255,69,0],
        'orchid':[218,112,214],
        'palegoldenrod':[238,232,170],
        'palegreen':[152,251,152],
        'paleturquoise':[175,238,238],
        'palevioletred':[216,112,147],
        'papayawhip':[255,239,213],
        'peachpuff':[255,218,185],
        'peru':[205,133,63],
        'pink':[255,192,203],
        'plum':[221,160,221],
        'powderblue':[176,224,230],
        'purple':[128,0,128],
        'red':[255,0,0],
        'rosybrown':[188,143,143],
        'royalblue':[65,105,225],
        'saddlebrown':[139,69,19],
        'salmon':[250,128,114],
        'sandybrown':[244,164,96],
        'seagreen':[46,139,87],
        'seashell':[255,245,238],
        'sienna':[160,82,45],
        'silver':[192,192,192],
        'skyblue':[135,206,235],
        'slateblue':[106,90,205],
        'slategray':[112,128,144],
        'slategrey':[112,128,144],
        'snow':[255,250,250],
        'springgreen':[0,255,127],
        'steelblue':[70,130,180],
        'tan':[210,180,140],
        'teal':[0,128,128],
        'thistle':[216,191,216],
        'tomato':[255,99,71],
        'turquoise':[64,224,208],
        'violet':[238,130,238],
        'wheat':[245,222,179],
        'white':[255,255,255],
        'whitesmoke':[245,245,245],
        'yellow':[255,255,0],
        'yellowgreen':[154,205,50],
    };

    // color parsing function originally from
    // http://plugins.jquery.com/files/jquery.color.js.txt
    // (with slight modifications)

    // Parse strings looking for color tuples [255,255,255]
    var rgbNumRE = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/;
    var rgbPercRE = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/;
    var rgbHash6RE = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/;
    var rgbHash3RE = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/;

    var strToRgb = function(color) {
      var result;

      // Check if we're already dealing with an array of colors
//         if ( color && color.constructor == Array && color.length == 3 )
//             return color;
      
      // Look for rgb(num,num,num)
      if (result = rgbNumRE.exec(color))
        return [parseInt(result[1]), parseInt(result[2]), parseInt(result[3])];
      
      // Look for rgb(num%,num%,num%)
      if (result = rgbPercRE.exec(color))
        return [parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55];
      
      // Look for #a0b1c2
      if (result = rgbHash6RE.exec(color))
        return [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16)];
      
      // Look for #fff
      if (result = rgbHash3RE.exec(color))
        return [parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16)];
      
      // Otherwise, we're most likely dealing with a named color
      return colors[$.trim(color).toLowerCase()];
    }

    var rgbToStr = function(rgb) {
      // TODO: there has to be a better way, even in JS
      var r = Math.floor(rgb[0]).toString(16);
      var g = Math.floor(rgb[1]).toString(16);
      var b = Math.floor(rgb[2]).toString(16);
      // pad
      r = r.length < 2 ? '0' + r : r;
      g = g.length < 2 ? '0' + g : g;
      b = b.length < 2 ? '0' + b : b;        
      return ('#'+r+g+b);
    }
    
    // Functions rgbToHsl and hslToRgb originally from 
    // http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
    // implementation of functions in Wikipedia
    // (with slight modifications)

    // RGB to HSL color conversion
    var rgbToHsl = function(rgb) {
      var r = rgb[0]/255, g = rgb[1]/255, b = rgb[2]/255;
      var max = Math.max(r, g, b), min = Math.min(r, g, b);
      var h, s, l = (max + min) / 2;

      if (max == min) {
        h = s = 0; // achromatic
      } else {
        var d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
        switch (max) {
          case r: h = (g - b) / d + (g < b ? 6 : 0); break;
          case g: h = (b - r) / d + 2; break;
          case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
      }
      
      return [h, s, l];
    }

    var hue2rgb = function(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1/6) return p + (q - p) * 6 * t;
      if (t < 1/2) return q;
      if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    }

    var hslToRgb = function(hsl) {
      var h = hsl[0], s = hsl[1], l = hsl[2];

      var r, g, b;

      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
      }
      
      return [r * 255, g * 255, b * 255];
    }

    var adjustLightnessCache = {};

    // given color string and -1<=adjust<=1, returns color string
    // where lightness (in the HSL sense) is adjusted by the given
    // amount, the larger the lighter: -1 gives black, 1 white, and 0
    // the given color.
    var adjustColorLightness = function(colorstr, adjust) {
      if (!(colorstr in adjustLightnessCache)) {
        adjustLightnessCache[colorstr] = {}
      }
      if (!(adjust in adjustLightnessCache[colorstr])) {
        var rgb = strToRgb(colorstr);
        if (rgb === undefined) {
          // failed color string conversion; just return the input
          adjustLightnessCache[colorstr][adjust] = colorstr;
        } else {
          var hsl = rgbToHsl(rgb);
          if (adjust > 0.0) {
            hsl[2] = 1.0 - ((1.0-hsl[2])*(1.0-adjust));
          } else {
            hsl[2] = (1.0+adjust)*hsl[2];
          }
          var lightRgb = hslToRgb(hsl);
          adjustLightnessCache[colorstr][adjust] = rgbToStr(lightRgb);
        }
      }
      return adjustLightnessCache[colorstr][adjust];
    }

    // Partially stolen from: http://documentcloud.github.com/underscore/
    // MIT-License
    // TODO: Mention in LICENSE.md
    var isEqual = function(a, b) {
      // Check object identity.
      if (a === b) return true;
      // Different types?
      var atype = typeof(a), btype = typeof(b);
      if (atype != btype) return false;
      // Basic equality test (watch out for coercions).
      if (a == b) return true;
      // One is falsy and the other truthy.
      if ((!a && b) || (a && !b)) return false;
      // If a is not an object by this point, we can't handle it.
      if (atype !== 'object') return false;
      // Check for different array lengths before comparing contents.
      if (a.length && (a.length !== b.length)) return false;
      // Nothing else worked, deep compare the contents.
      for (var key in b) if (!(key in a)) return false;
      // Recursive comparison of contents.
      for (var key in a) if (!(key in b) || !isEqual(a[key], b[key])) return false;
      return true;
    };

    var keyValRE = /^([^=]+)=(.*)$/; // key=value
    var isDigitsRE = /^[0-9]+$/;

    var deparam = function(str) {
      var args = str.split('&');
      var len = args.length;
      if (!len) return null;
      var result = {};
      for (var i = 0; i < len; i++) {
        var parts = args[i].match(keyValRE);
        if (!parts || parts.length != 3) break;
        var val = [];
        var arr = parts[2].split(',');
        var sublen = arr.length;
        for (var j = 0; j < sublen; j++) {
          var innermost = [];
          // map empty arguments ("" in URL) to empty arrays
          // (innermost remains [])
          if (arr[j].length) {
            var arrsplit = arr[j].split('~');
            var subsublen = arrsplit.length;
            for (var k = 0; k < subsublen; k++) {
              if(arrsplit[k].match(isDigitsRE)) {
                // convert digits into ints ...
                innermost.push(parseInt(arrsplit[k], 10));
              }
              else {
                // ... anything else remains a string.
                innermost.push(arrsplit[k]);
              }
            }
          }
          val.push(innermost);
        }
        result[parts[1]] = val;
      }
      return result;
    };

    var paramArray = function(val) {
      val = val || [];
      var len = val.length;
      var arr = [];
      for (var i = 0; i < len; i++) {
        if ($.isArray(val[i])) {
          arr.push(val[i].join('~'));
        } else {
          // non-array argument; this is an error from the caller
          console.error('param: Error: received non-array-in-array argument [', i, ']', ':', val[i], '(fix caller)');
        }
      }
      return arr;
    };

    var param = function(args) {
      if (!args) return '';
      var vals = [];
      for (var key in args) {
        if (args.hasOwnProperty(key)) {
          var val = args[key];
          if (val == undefined) {
            console.error('Error: received argument', key, 'with value', val);
            continue;
          }
          // values normally expected to be arrays, but some callers screw
          // up, so check
          if ($.isArray(val)) {
            var arr = paramArray(val);
            vals.push(key + '=' + arr.join(','));
          } else {
            // non-array argument; this is an error from the caller
            console.error('param: Error: received non-array argument', key, ':', val, '(fix caller)');
          }
        }
      }
      return vals.join('&');
    };

    var profiles = {};
    var profileStarts = {};
    var profileOn = false;
    var profileEnable = function(on) {
      if (on === undefined) on = true;
      profileOn = on;
    }; // profileEnable
    var profileClear = function() {
      if (!profileOn) return;
      profiles = {};
      profileStarts = {};
    }; // profileClear
    var profileStart = function(label) {
      if (!profileOn) return;
      profileStarts[label] = new Date();
    }; // profileStart
    var profileEnd = function(label) {
      if (!profileOn) return;
      var profileElapsed = new Date() - profileStarts[label]
      if (!profiles[label]) profiles[label] = 0;
      profiles[label] += profileElapsed;
    }; // profileEnd
    var profileReport = function() {
      if (!profileOn) return;
      if (window.console) {
        $.each(profiles, function(label, time) {
          console.log("profile " + label, time);
        });
        console.log("-------");
      }
    }; // profileReport
    
    // container: ID or jQuery element
    // collData: the collection data (in the format of the result of
    //   http://.../brat/ajax.cgi?action=getCollectionInformation&collection=...
    // docData: the document data (in the format of the result of
    //   http://.../brat/ajax.cgi?action=getDocument&collection=...&document=...
    // returns the embedded visualizer's dispatcher object
    var embed = function(container, collData, docData, webFontURLs,
                         dispatcher, options) {
      if (dispatcher === undefined) {
          dispatcher = new Dispatcher();
      }
      if (options === undefined) {
          options = {};
      }

      var visualizer = new Visualizer(dispatcher, container, webFontURLs,
                                      options);

      docData.collection = null;

      dispatcher.post('collectionLoaded', [collData]);
      dispatcher.post('requestRenderData', [docData]);

      return dispatcher;
    };

    // container: ID or jQuery element
    // collDataURL: the URL of the collection data, or collection data
    //   object (if pre-fetched)
    // docDataURL: the url of the document data (if pre-fetched, use
    //   simple `embed` instead)
    // callback: optional; the callback to call afterwards; it will be
    //   passed the embedded visualizer's dispatcher object
    var embedByURL = function(container, collDataURL, docDataURL, webFontURLs, callback) {
      var collData, docData;
      var handler = function() {
        if (collData && docData) {
          var dispatcher = embed(container, collData, docData, webFontURLs);
          if (callback) callback(dispatcher);
        }
      };
      if (typeof(container) == 'string') {
        $.getJSON(collDataURL, function(data) { collData = data; handler(); });
      } else {
        collData = collDataURL;
      }
      $.getJSON(docDataURL, function(data) { docData = data; handler(); });
    };

    var fontsLoaded = false;
    var fontNotifyList = false;

    var proceedWithFonts = function() {
      if (fontsLoaded) return;

      fontsLoaded = true;
      $.each(fontNotifyList, function(dispatcherNo, dispatcher) {
        dispatcher.post('triggerRender');
      });
      fontNotifyList = null;
    };

    var loadFonts = function(webFontURLs, dispatcher) {
      if (fontsLoaded) {
        dispatcher.post('triggerRender');
        return;
      }

      if (fontNotifyList) {
        fontNotifyList.push(dispatcher);
        return;
      }

      fontNotifyList = [dispatcher];

      webFontURLs = webFontURLs || [
        'static/fonts/Astloch-Bold.ttf',
        'static/fonts/PT_Sans-Caption-Web-Regular.ttf',
        'static/fonts/Liberation_Sans-Regular.ttf'
      ];

      var families = [];
      $.each(webFontURLs, function(urlNo, url) {
        if (/Astloch/i.test(url)) families.push('Astloch');
        else if (/PT.*Sans.*Caption/i.test(url)) families.push('PT Sans Caption');
        else if (/Liberation.*Sans/i.test(url)) families.push('Liberation Sans');
      });

      webFontURLs = {
        families: families,
        urls: webFontURLs
      }

      var webFontConfig = {
        custom: webFontURLs,
        active: proceedWithFonts,
        inactive: proceedWithFonts,
        fontactive: function(fontFamily, fontDescription) {
          // Note: Enable for font debugging
          // console.log("font active: ", fontFamily, fontDescription);
        },
        fontloading: function(fontFamily, fontDescription) {
          // Note: Enable for font debugging
          // console.log("font loading:", fontFamily, fontDescription);
        },
      };

      WebFont.load(webFontConfig);

      setTimeout(function() {
        if (!fontsLoaded) {
          console.error('Timeout in loading fonts');
          proceedWithFonts();
        }
      }, fontLoadTimeout);
    };

    var areFontsLoaded = function() {
      return fontsLoaded;
    };


    return {
      profileEnable: profileEnable,
      profileClear: profileClear,
      profileStart: profileStart,
      profileEnd: profileEnd,
      profileReport: profileReport,
      formatTimeAgo: formatTimeAgo,
      realBBox: realBBox,
      getSpanLabels: getSpanLabels,
      spanDisplayForm: spanDisplayForm,
      getArcLabels: getArcLabels,
      arcDisplayForm: arcDisplayForm,
      escapeQuotes: escapeQuotes,
      escapeHTML: escapeHTML,
      escapeHTMLandQuotes: escapeHTMLandQuotes,
      escapeHTMLwithNewlines: escapeHTMLwithNewlines,
      cmp: cmp,
      rgbToHsl: rgbToHsl,
      hslToRgb: hslToRgb,
      adjustColorLightness: adjustColorLightness,
      objectToUrlStr: objectToUrlStr,
      isEqual: isEqual,
      paramArray: paramArray,
      param: param,
      deparam: deparam,
      embed: embed,
      embedByURL: embedByURL,
      isMac: isMac,
      loadFonts: loadFonts,
      areFontsLoaded: areFontsLoaded,
    };

})(window);
