// Generic Microformat Parser v0.1 Dan Webb (dan@danwebb.net)
// Licenced under the MIT Licence
// 
// var people = HCard.discover();
// people[0].fn => 'Dan Webb'
// people[0].urlList => ['http://danwebb.net', 'http://eventwax.com']
//
// TODO
//
// Fix _propFor to work with old safari
// Find and use unit testing framework on microformats.org test cases
// isue with hcard email?
// More formats: HFeed, HEntry, HAtom, RelTag, XFN?


// JavaScript 1.6 Iterators and generics cross-browser
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function(func, scope) {
    scope = scope || this;
    for (var i = 0, l = this.length; i < l; i++)
      func.call(scope, this[i], i, this); 
  }
}

if (typeof Prototype != 'undefined' || !Array.prototype.map) {
  Array.prototype.map = function(func, scope) {
    scope = scope || this;
    var list = [];
    for (var i = 0, l = this.length; i < l; i++)
        list.push(func.call(scope, this[i], i, this)); 
    return list;
  }
}

if (typeof Prototype != 'undefined' || !Array.prototype.filter) {
  Array.prototype.filter = function(func, scope) {
    scope = scope || this;
    var list = [];
    for (var i = 0, l = this.length; i < l; i++)
        if (func.call(scope, this[i], i, this)) list.push(this[i]); 
    return list;
  }
}

['forEach', 'map', 'filter', 'slice', 'concat'].forEach(function(func) {
    if (!Array[func]) Array[func] = function(object) {
      return this.prototype[func].apply(object, Array.prototype.slice.call(arguments, 1));
    }
});

// ISO8601 Date extension
Date.ISO8601PartMap = {
  Year : 1,
  Month : 3,
  Date : 5,
  Hours : 7,
  Minutes : 8,
  Seconds : 9 
}

Date.matchISO8601 = function(text) { 
  return text.match(/^(\d{4})(-?(\d{2}))?(-?(\d{2}))?(T(\d{2}):?(\d{2})(:?(\d{2}))?)?(Z?(([+\-])(\d{2}):?(\d{2})))?$/); 
}

Date.parseISO8601 = function(text) {
  var dateParts = this.matchISO8601(text);
  if (dateParts) {
    var date = new Date, parts, offset = 0;
    for (var prop in this.ISO8601PartMap) {
      if (part = dateParts[this.ISO8601PartMap[prop]]) 
        date['set' + prop]((prop == 'Month') ? parseInt(part)-1 : parseInt(part));
        else date['set' + prop]((prop == 'Date') ? 1 : 0);
    }
    
    if (dateParts[11]) {
      offset = (parseInt(dateParts[14]) * 60) + parseInt(dateParts[15]);
      offset *= ((parseInt[13] == '-') ? 1 : -1);
    }
    
    offset -= date.getTimezoneOffset();
    date.setTime(date.getTime() + (offset * 60 * 1000)); 
    
    return date;
  }
}

// Main Microformat namespace
Microformat = {
  define : function(name, spec) {
    var mf = function(node, data) {
      this.parentElement = node;
      Microformat.extend(this, data);
    };
    
    mf.container = name;
    mf.format = spec;
    mf.prototype = Microformat.Base;
    return Microformat.extend(mf, Microformat.SingletonMethods);
  },
  SingletonMethods : {
    discover : function(context) {
      return Microformat.$$(this.container, context).map(function(node) {
        return new this(node, this._parse(this.format, node));
      }, this);
    },
    _parse : function(format, node) {
      var data = {};
      this._process(data, format.one, node, true);
      this._process(data, format.many, node);
      return data;
    },
    _process : function(data, format, context, firstOnly) {
      var selection, first;
      format = format || [];
      format.forEach(function(item) {
        if (typeof item == 'string') {
          selection = Microformat.$$(item, context);
          
          if (firstOnly && (first = selection[0])) {
            data[this._propFor(item)] = this._extractData(first, 'simple', data);
          } else if (selection.length > 0) {
            data[this._propFor(item) + 'List'] = selection.map(function(node) {
              return this._extractData(node, 'simple', data);
            }, this);
          }
            
        } else {
          
            for (var cls in item) {
              selection = Microformat.$$(cls, context);
              
              if (firstOnly && (first = selection[0])) {
                data[this._propFor(cls)] = this._extractData(first, item[cls], data);
              } else if (selection.length > 0) {
                data[this._propFor(cls + 'List')] = selection.map(function(node) {
                  return this._extractData(node, item[cls], data);
                }, this);
              }
            }
              
        }
        
      }, this);
      return data;
    },
    _extractData : function(node, dataType, data) {
      if (dataType._parse) return dataType._parse(dataType.format, node);
      if (typeof dataType == 'function') return dataType.call(this, node, data);
      
      var values = Microformat.$$('value', node);
      if (values.length > 0) return this._extractClassValues(node, values);
      
      switch (dataType) {
        case 'simple': return this._extractSimple(node);
        case 'url': return this._extractURL(node);
      }
      return this._parse(dataType, node);
    },
    _extractURL : function(node) {
      var href;
      switch (node.nodeName.toLowerCase()) {
        case 'img':    href = node.src;
                       break;
        case 'area':
        case 'a':      href = node.href;
                       break;
        case 'object': href = node.data;
      }
      if (href) {
        if (href.indexOf('mailto:') == 0) 
          href = href.replace(/^mailto:/, '').replace(/\?.*$/, '');
        return href;
      }
      
      return this._coerce(this._getText(node));
    },
    _extractSimple : function(node) {
      switch (node.nodeName.toLowerCase()) {
        case 'abbr': return this._coerce(node.title);
        case 'img': return this._coerce(node.alt);
      }
      return this._coerce(this._getText(node));
    },
    _extractClassValues : function(node, values) {
      var value = new String(values.map(function(value) {
        return this._extractSimple(value);
      }, this).join(''));
      var types = Microformat.$$('type', node);
      var t = types.map(function(type) {
        return this._extractSimple(type);
      }, this);
      value.types = t;
      return value;
    },
    _getText : function(node) {
      if (node.innerText) return node.innerText;
      return Array.map(node.childNodes, function(node) {
        if (node.nodeType == 3) return node.nodeValue;
        else return this._getText(node);
      }, this).join('').replace(/\s+/g, ' ').replace(/(^\s+)|(\s+)$/g, '');
    },
    _coerce : function(value) {
      var date, number;
      if (value == 'true') return true;
      if (value == 'false') return false;
      if (date = Date.parseISO8601(value)) return date;
      return String(value);
    },
    _propFor : function(name) {
      this.__propCache = this.__propCache || {};
      if (prop = this.__propCache[name]) return prop;
      return this.__propCache[name] = name.replace(/(-(.))/g, function() {
        // this isn't going to work on old safari without the fix....hmmm
        return arguments[2].toUpperCase();
      });
    },
    _handle : function(prop, item, data) {
      if (this.handlers[prop]) this.handlers[prop].call(this, item, data);
    }
  },
  // In built getElementsByClassName
  $$ : function(className, context) {
    if (typeof Sizzle == 'function') {
      return Sizzle('.'+className, context);
    } else if (typeof Selector == 'function') {
      return Selector.findChildElements(context, $A(['.'+className]));
    } else if (typeof jQuery == 'function') {
      return jQuery('.'+className, context)
    } else {
      context = context || document;
      var nodeList;
      
      if (context == document || context.nodeType == 1) {
        if (typeof document.evaluate == 'function') {
          var xpath = document.evaluate(".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]", 
                                        context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
          var els = [];
          for (var i = 0, l = xpath.snapshotLength; i < l; i++)
           els.push(xpath.snapshotItem(i));
          return els;
        } else nodeList = context.getElementsByTagName('*');
      } else nodeList = context;
      
      var re = new RegExp('(^|\\s)' + className + '(\\s|$)');
      return Array.filter(nodeList, function(node) {  return node.className.match(re) });
    }
  },
  // In built Object.extend equivilent
  extend : function(dest, source) {
    for (var prop in source) dest[prop] = source[prop];
    return dest;
  },
  // methods available to all instances of a microformat
  Base : {}
};


var HCard = Microformat.define('vcard', {
  one : ['bday', 'tz', 'sort-string', 'uid', 'class', {
    'n' : {
      one : ['family-name', 'given-name', 'additional-name'],
      many : ['honorific-prefix', 'honorific-suffix']
    },
    'geo' : function(node) {
      var m;
      if ((node.nodeName.toLowerCase() == 'abbr') && (m = node.title.match(/^([\-\d\.]+);([\-\d\.]+)$/))) {
        return { latitude : m[1], longitude : m[2] };
      }
      
      return this._extractData(node, { one : ['latitude', 'longitude'] });
    },
    // implied n
    'fn' : function(node, data) {
      var m, fn = this._extractData(node, 'simple');
      
      if (m = fn.match(/^(\w+) (\w+)$/)) {
        data.n = data.n || {};
        data.n.givenName = data.n.givenName || m[1];
        data.n.familyName = data.n.familyName || m[2];
      }
      
      if (m = fn.match(/^(\w+),? (\w+)\.?$/)) {
        data.n = data.n || {};
        data.n.givenName = data.n.givenName || m[2];
        data.n.familyName = data.n.familyName || m[1];
      }
      
      return fn;
    }
  }],
  many : ['label', 'sound', 'title', 'role', 'key', 'mailer', 'rev', 'nickname', 'category', 'note', 'tel', { 
      'url' : 'url', 'logo' : 'url', 'photo' : 'url', 'email' : 'url' 
    }, {
    'adr' : {
      one : ['post-office-box', 'extended-address', 'street-address', 'locality', 'region',
             'postal-code', 'country-name']
    },
    // implied org
    'org' : function(node) {
      var org = this._extractData(node, {
        one : ['organization-name'],
        many : ['organization-unit']
      });
      
      if (!org.organizationName) 
        org.organizationName = this._extractData(node, 'simple');
        
      return org;
    }
  }]
});

var Map = {
  infoWindowContent: function(hcard) {
    var address = hcard.adrList[0]
    return '<b>'
      +(hcard.urlList ? '<a href="'+hcard.urlList[0]+'">'+hcard.fn+'</a>' : hcard.fn)
      +'</b><br/>'
      +address.streetAddress
      +'<br/>'
      +address.locality+', '+address.region+' '+address.postalCode+' '+address.country
  },
  
  showPoint: function(hcard) {
    var point = new google.maps.LatLng(hcard.geo.latitude, hcard.geo.longitude);
    var marker = new google.maps.Marker({
        position: point, 
        map: Map.map, 
        title: hcard.fn,
        icon: Map.coloredPin((hcard.categoryList ? hcard.categoryList[0] : 'red')),
        shadow: Map.shadow(),
        shape: { coord: [1, 1, 1, 20, 18, 20, 18 , 1], type: 'poly'}
    });
    
    var infoWindow = new google.maps.InfoWindow({
      content: Map.infoWindowContent(hcard),
      size: new google.maps.Size(250,50)
    });
    
    google.maps.event.addListener(marker, 'click', function() {
      infoWindow.open(Map.map, marker);
    });
    
    Map.bounds.extend(point);    
  },
  
  hcards: function() {
    if (!Map._hcards) {
      Map._hcards = HCard.discover(eval(Map.hcardSelector)).select(function(card) { return card.geo })
    }
    return Map._hcards;
  },
  
  domId: 'map',
    
  options: {
    zoom: 8,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  },
  
  maxZoom: 13,
  
  display: function() {
    if (Map.hcards().length == 0) return

    Map.map = new google.maps.Map(document.getElementById(Map.domId), Map.options);

    // Map.map.enableScrollWheelZoom();
    // Map.map.addControl(new GOverviewMapControl());

    var center = new google.maps.LatLng(Map.hcards()[0].geo.latitude, Map.hcards()[0].geo.longitude);
    Map.bounds = new google.maps.LatLngBounds(center);
    
    Map.hcards().forEach(Map.showPoint); // forEach is defined in microformat.js

    // Fit all points in view
    if (Map.hcards().length == 1) {
      Map.map.set_zoom(Map.maxZoom);
      Map.map.set_center(center);
    } else {
      Map.map.fitBounds(Map.bounds); 
    }
  },
  
  icons: {},
  
  coloredPin: function(color) {
    if (!Map.icons[color]) {
      Map.icons[color] = new google.maps.MarkerImage("http://labs.google.com/ridefinder/images/mm_20_"+(color || 'red')+".png",
        new google.maps.Size(12, 20),
        new google.maps.Point(0,0),
        new google.maps.Point(6,20));
    }
    return Map.icons[color];
  },
  
  shadow: function() {
    return new google.maps.MarkerImage("http://labs.google.com/ridefinder/images/mm_20_shadow.png",
      new google.maps.Size(22, 20),
      new google.maps.Point(5,1),
      new google.maps.Point(0,20));          
  }
}

// Dunno why IE doesn't like dom:loaded.
if (Prototype.Browser.IE) {
  Event.observe(window, "load", Map.display);
} else {
  Event.observe(window, "dom:loaded", Map.display);
}


Map.hcardSelector = "$('mainpanel')";
Map.maxZoom = 13;
Map.options.disableDefaultUI = true;

Event.observe(window, 'load', function() {  
  Event.observe('map', 'click', function(e) {
    window.location = $('map-link').getAttribute('href');
  });
});
