// Copyright (C) Alex Buckley (FlyerTalk: Tintagel) 2009
// Version 1.2 2009-03-28

// Set the elite variable to any value for 500 EQM minimum
// Comment the line below out to use actual miles for base EQMs
var elite = '1k';

// ==UserScript==
// @name          unitedcpeqm
// @description   Display cents per EQM for itineraries on united.com
// @include       http*://travel.united.com/ube/*.do*
// ==/UserScript==

/* Features, assumptions, limitations

   1. This script works for results viewed by 'Price' and 'Flexible dates'.
      It does not work for 'Schedule & Price' since the Depart/Return dates can be changed dynamically, which is hard to detect.

   2. This scripts works whether the user has selected miles or kilometers for 'Miles flown format' in their profile.
      (In fact, the miles/km flown are only used if no Award miles for a segment are available because it is operated by a partner.)

   3. If 'Flight details' for a segment says Economy, 100% EQM. If 'Flight details' for a segment says Business or First, 150% EQM.
      Y/B fares earn 150% EQM in real life but display as Economy so only earn 100% here. Still, who buys Y/B? :-)

   4. Partner segments are treated exactly like United segments for EQM minimum and multiplication purposes.

   Release history

   1.0 2009-03-15 Initial release
   1.1 2009-03-15 Handle non-USD currencies and distances (EUR: decimals separated by comma not dot; GBP: ppeqm not cpeqm)
   1.2 2009-03-28 Display EQMs as well as cpeqm

   Resources

   https://developer.mozilla.org/en/Introduction_to_using_XPath_in_JavaScript
   https://developer.mozilla.org/en/Gecko_DOM_Reference
   Use of . for context node: http://groups.google.com/group/mozilla.dev.tech.xml/browse_thread/thread/290e6f399f57e643
   http://www.regular-expressions.info/javascript.html
   http://www.evolt.org/article/Regular_Expressions_in_JavaScript/17/36435/
   Rounding to 1 d.p.: http://www.javascriptkit.com/javatutors/round.shtml
   Stupid weak typing cannot auto-convert string 'X,XXX' to int XXXX. Have to replace comma with nothing in the string, then parseInt it.
*/




// Each available itinerary is headed by a blue bar giving 'Price per adult'
var itinNodes = document.evaluate("//table[@class='hdr']", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var itinIdx = 0; itinIdx < itinNodes.snapshotLength; itinIdx++) {

  // Extract price and convert to cents or pence
  var priceNodes = document.evaluate(".//tr/td/h2", itinNodes.snapshotItem(itinIdx), null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  var priceMatch = /Price per adult (\w+) \b(.+)\b/.exec(priceNodes.snapshotItem(0).textContent);
  var currencyCode = priceMatch[1];
  var cents        = localizePrice(priceMatch[2], currencyCode);
  var miles        = 0;

  // Extract and sum EQMs for each segment
  // Each segment has a node '<div class="title">Flight details</div>' in the 'det' table
  // Segment data is in the div following that node
  // First, skip over stupid whitespace node between 'hdr' table and 'det' table
  var segmentsNode = itinNodes.snapshotItem(itinIdx).nextSibling.nextSibling;
  var segmentNodes = document.evaluate(".//div[@class='title']/following-sibling::div[1]", segmentsNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);  
  for (var segmentIdx = 0; segmentIdx < segmentNodes.snapshotLength; segmentIdx++) {
    var segmentText = segmentNodes.snapshotItem(segmentIdx).textContent;
    var milesForOneSegment = 0;

    // A United segment has 'Award miles'
    var milesForOneSegmentMatch = /\b(.+)\b Award miles/.exec(segmentText);
    if (milesForOneSegmentMatch != null) {
      milesForOneSegment = localizeDistance(milesForOneSegmentMatch[1]);
    } else {
      // A partner segment does not have 'Award miles'. Fall back to 'miles traveled'.
      milesForOneSegmentMatch = /\b(.+)\b miles traveled/.exec(segmentText);
      if (milesForOneSegmentMatch != null) {
        milesForOneSegment = localizeDistance(milesForOneSegmentMatch[1]);
      } else {
        // This is a partner segment (no 'Award miles') and user has set preferences to kilometers (no 'miles traveled'). Fall back to 'km traveled'.
        kmForOneSegmentMatch = /\b(.+)\b km traveled/.exec(segmentText);
        milesForOneSegment = localizeDistance(kmForOneSegmentMatch[1]) / 1.6;
      }
    }
    
    var classForOneSegmentMatch = /\b(Business|First)\b/.exec(segmentText);
    var classMultiplier = (classForOneSegmentMatch != null) ? 1.5 : 1.0;
    miles += atLeast500(milesForOneSegment) * classMultiplier;
  }

  var cpeqm = cents / miles;
  var cpeqmRounded = Math.round(cpeqm * 10) / 10;
  priceNodes.snapshotItem(0).innerHTML = priceNodes.snapshotItem(0).innerHTML + " &nbsp; (" + miles + " / " + cpeqmRounded + " " + getCurrencyUnit(currencyCode) + "peqm)";
}




function localizePrice(rawText, locale) {
  switch (locale) {
    // Hong Kong: X,XXX (no cents)
    case 'HKD' : case 'SGD' : case 'CNY' : return parseInt(rawText.replace(/,/g, '')) * 100;

    // US/UK:   X,XXX.YY
    // France:  X XXX,YY
    // Belgium: X.XXX,YY
    default    : return parseInt(rawText.replace(/\s+|\.|,/g, ''));
  }
}


function localizeDistance(rawText) { return parseInt(rawText.replace(/\s+|\.|,/g, '')); }


function getCurrencyUnit(majorCurrencyUnit) {
  switch (majorCurrencyUnit) {
    case 'GBP' : return 'p';
    case 'CNY' : return 'f';  // 1 yuan (CNY) = 100 fen
    case 'JPY' : return 'y';  // 1 yen (JPY) is not subdivided
    default    : return 'c';
  }
}


function atLeast500(miles) { if (miles < 500 && typeof(elite) != 'undefined') return 500; else return miles; }