/* globals Promise, window, done, assert_true, on_event, promise_test */

/**
 * Creates an ATTAcomm object.  If the parameters are supplied
 * it sets up event listeners to send the test data to an ATTA if one
 * is available.  If the ATTA does not respond, it will assume the test
 * is being done manually and the results are being entered in the
 * parent test window.
 *
 * @constructor
 * @param {object} params
 * @param {string} [params.test] - object containing JSON test definition
 * @param {string} [params.testFile] - URI of a file with JSON test definition
 * @param {string} params.ATTAuri - URI to use to exercise the window
 * @event DOMContentLoaded Calls go once DOM is fully loaded
 * @returns {object} Reference to the new object
 *
 */

function ATTAcomm(params) {
  'use strict';

  this.Params = null;       // parameters passed in
  this.Promise = null;      // master Promise that resolves when intialization is complete
  this.Properties = null;   // testharness_properties from the opening window
  this.Tests = null;        // test object being processed
  this.testName = "";       // name of test being run
  this.log = "";            // a buffer to capture log information for debugging
  this.startReponse = {};   // startTest response will go in here for debugging

  this.loading = true;

  this.timeout = 5000;

  var pending = [] ;

  // set up in case DOM finishes loading early
  pending.push(new Promise(function(resolve) {
    on_event(document, "DOMContentLoaded", function() {
        resolve(true);
    }.bind(this));
  }.bind(this)));

  // if we are under runner, then there are props in the parent window
  //
  // if "output" is set in that, then pause at the end of running so the output
  // can be analyzed. @@@TODO@@@
  if (window && window.opener && window.opener.testharness_properties) {
    this.Properties = window.opener.testharness_properties;
  }

  this.Params = params;

  if (this.Params.hasOwnProperty("ATTAuri")) {
    this.ATTAuri = this.Params.ATTAuri;
  } else {
    this.ATTAuri = "http://localhost:4119";
  }

  if (this.Params.hasOwnProperty("title")) {
    this.testName = this.Params.title;
  }

  // start by loading the test (it might be inline, but
  // loadTest deals with that
  pending.push(this.loadTest(params)
    .then(function(tests) {
      // if the test is NOT an object, turn it into one
      if (typeof tests === 'string') {
        tests = JSON.parse(tests) ;
      }

      this.Tests = tests;

    }.bind(this)));

  this.Promise = new Promise(function(resolve, reject) {
    // once the DOM and the test is loaded... set us up
    Promise.all(pending)
    .then(function() {
      // Everything is loaded
      this.loading = false ;
      // run the automated tests (or setup for manual testing)
      this.go();
      resolve(this);
    }.bind(this))
    .catch(function(err) {
      // loading the components failed somehow - report the errors and mark the test failed
      test( function() {
        assert_true(false, "Loading of test components failed: " +JSON.stringify(err)) ;
      }, "Loading test components");
      this.dumpLog();
      done() ;
      reject("Loading of test components failed: "+JSON.stringify(err));
      return ;
    }.bind(this));
  }.bind(this));

  return this;
}

ATTAcomm.prototype = {

  /**
   * go sets up the connection to the ATTA
   *
   * If that succeeds and the tests in this test file have methods for
   * the API supported by the ATTA, then it automatically runs those tests.
   *
   * Otherwise it sets up for manualt testing.
   */
  go: function() {
    'use strict';
    // everything is ready.  Let's talk to the ATTA
    this.startTest().then(function(res) {

      // start was successful - iterate over steps
      var API = res.body.API;

      var subtestsForAPI = false;

      // check main and potentially nested lists of tests for
      // tests with this API.  If any step is missing this API
      // mapping, then we need to be manual
      this.Tests.forEach(function(subtest) {
        if (subtest.hasOwnProperty("test") &&
            subtest.test.hasOwnProperty(API)) {
          // there is at least one subtest for this API so
          // this is a test that needs to be looked at by an atta
          subtestsForAPI = true;
        } else if (Array.isArray(subtest)) {
          subtest.forEach(function(st) {
            if (st.hasOwnProperty("test") &&
                st.test.hasOwnProperty(API)) {
              subtestsForAPI = true;
            }
          });
        }
      });

      if (subtestsForAPI) {
        this.runTests(API, this.Tests)
        .then(function() {
          // the tests all ran; close it out
          this.endTest().then(function() {
            this.dumpLog();
            done();
          }.bind(this));
        }.bind(this))
        .catch(function(err) {
          this.endTest().then(function() {
            this.dumpLog();
            done();
          }.bind(this));
        }.bind(this));
      } else {
        // we don't know this API for this test
        // but we ARE talking to an ATTA; skip this test
        this.dumpLog();
        if (window.opener && window.opener.completion_callback) {
          window.opener.completion_callback([], { status: 3, message: "No steps for AT API " + API } );
        } else {
          done();
        }
        // this.setupManualTest("Unknown AT API: " + API);
      }
    }.bind(this))
    .catch(function(res) {
      // startTest failed so just sit and wait for a manual test to occur
      if (res.timeout || res.status === 102) {
        this.setupManualTest("No response from ATTA at " + this.ATTAuri);
      } else if (res.status === 200 ) {
        this.setupManualTest(res.message);
      } else if (res.statusText === "No response from ATTA") {
        this.setupManualTest("");
      } else {
        this.setupManualTest("Error from ATTA: " + res.status + ": " + res.statusText);
      }
    }.bind(this));
  },

  runTests: function(API, collection) {
    // this method returns a promise

    return new Promise(function(resolve, reject) {
      // accumulate promises; complete when done
      var pending = [];
      var testCount = 0;

      this.sendEvents(API, collection)
      .then(function(eventStatus) {

        /* Loop strategy...
         *
         * If the step is a 'test' then push it into the pending queue as a promise
         *
         * If the step is anything else, then if there is anything in pending, wait on it
         * Once it resolves, clear the queue and then execute the other step.
         *
         */
        collection.forEach(function(subtest) {
          //  what "type" of step in the sequence is this?
          var theType = "test" ;
          if (Array.isArray(subtest)) {
            // it is a group
            Promise.all(pending).then(function() {
              pending = [];
              // recursively run the tests
              pending.push(this.runTests(API, subtest));
            }.bind(this));
          } else if (subtest.hasOwnProperty("type")) {
            theType = subtest.type;
          }
          testCount++;
          if (theType === "test") {
            // this is a set of assertions that should be evaluated
            pending.push(this.runTest(testCount, API, subtest));
          } else if (theType === "script") {
            Promise.all(pending).then(function() {
              pending = [];
              // execute the script
              this.runScript(testCount, subtest);
            }.bind(this));
          } else if (theType === "attribute") {
            Promise.all(pending).then(function() {
              pending = [];
              // raise the event
              this.handleAttribute(testCount, subtest);
            }.bind(this));
          // } else {
          } else if (theType === "event") {
            Promise.all(pending).then(function() {
              pending = [];
              // raise the event
              this.raiseEvent(testCount, subtest);
            }.bind(this));
          // } else {
          }
        }.bind(this));

        Promise.all(pending)
        .then(function() {
          // this collection all ran
          if (eventStatus !== "NOEVENTS") {
            // there were some events at the beginning
            this.sendStopListen().then(function() {
              resolve(true);
            });
          } else {
            resolve(true);
          }
        }.bind(this));
      }.bind(this));
    }.bind(this));
  },

  setupManualTest: function(message) {
    // if we determine the test should run manually, then expose all of the conditions that are
    // in the TEST data structure so that a human can to the inspection and calculate the result
    //
    'use strict';

    var ref = document.getElementById("manualMode");
    if (ref) {
      // we have a manualMode block.  Populate it
      var content = "<h2>Manual Mode Enabled</h2><p>"+message+"</p>";
      if (this.Tests.hasOwnProperty("description")) {
        content += "<p>" + this.Tests.description + "</p>";
      }
      var theTable = "<table id='steps'><tr><th>Step</th><th>Type</th><th>Element ID</th><th>Assertions</th></tr>";
      this.Tests.forEach(function(subtest) {
        var type = "test";
        if (subtest.hasOwnProperty("type")) {
          type = subtest.type;
        }
        var id = "" ;
        if (subtest.hasOwnProperty("element")) {
          id = subtest.element;
        }
        theTable += "<tr><td class='step'>" + subtest.title +"</td>";
        theTable += "<td class='type'>" + type + "</td>";
        theTable += "<td class='element'>" + id +"</td>";

        // now what do we put over here? depends on the type
        if (type === "test") {
          // it is a test; dump the assertions
          theTable += "<td>" + this.buildAssertionTable(subtest.test) + "</td>";
        } else if (type === "attribute" ) {
          if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("value") && subtest.hasOwnProperty("element")) {
            if (subtest.value === "none") {
              theTable += "<td>Remove attribute <code>" + subtest.attribute + "</code> from the element with ID <code>" + subtest.element + "</code></td>";
            } else {
              theTable += "<td>Set attribute <code>" + subtest.attribute + "</code> on the element with ID <code>" + subtest.element + "</code> to the value <code>" + subtest.value + "</code></td>";
            }
          }
        } else if (type === "event" ) {
          // it is some events
          if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) {
            theTable += "<td>Send event <code>" + subtest.event + "</code> to the element with ID <code>" + subtest.element + "</code></td>";
          }
        } else if (type === "script" ) {
          // it is a script fragment
          theTable += "<td>Script: " + subtest.script + "</td>";
        } else {
          theTable += "<td>Unknown type: " + type + "</td>";
        }
        theTable += "</tr>";


      }.bind(this));

      theTable += "</table>";
      ref.innerHTML = content + theTable ;
    }
  },

  buildAssertionTable:  function(asserts) {
    "use strict";
    var output = "<table class='api'><tr><th>API Name</th><th colspan='4'>Assertions</th></tr>";
    var APIs = [] ;
    for (var k in asserts) {
      if (asserts.hasOwnProperty(k)) {
        APIs.push(k);
      }
    }

    APIs.sort().forEach(function(theAPI) {
      var rows = asserts[theAPI] ;
      var height = rows.length;
      output += "<tr><td rowspan='" + height + "' class='apiName'>"+theAPI+"</td>";
      var lastRow = rows.length - 1;
      rows.forEach(function(theRow, index) {
        var span = 4 - theRow.length;
        var colspan = span ? " colspan='"+span+"'" : "";
        theRow.forEach(function(item) {
          output += "<td" + colspan + ">" + item + "</td>";
        });
        output += "</tr>";
        if (index < lastRow) {
          output += "<tr>";
        }
      });
    });

    output += "</table>";
    return output;
  },

  // eventList - find the events for an API
  //
  // @param {string} API
  // @param {array} collection - a collection of tests
  // @returns {array} list of event names

  eventList: function(API, collection) {
    var eventHash = {};

    if (!API || API === "") {
      return [];
    }

    collection.forEach(function(subtest) {
      if (subtest.hasOwnProperty("test") &&
          subtest.test.hasOwnProperty(API)) {
        // this is a subtest for this API; look at the events
        subtest.test[API].forEach(function(assert) {
          // look for event names
          if (assert[0] === "event" && assert[1] === "type" && assert[2] === "is") {
            eventHash[assert[3]] = 1;
          }
        });
      }
    });

    return Object.keys(eventHash);
  },

  // handleAttribute - set or clear an attribute
  /**
   * @param {integer} testNum - The subtest number
   * @param {object} subtest - attribute information to set
   */
  handleAttribute: function(testNum, subtest) {
    "use strict";
    if (subtest) {
      if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("element") && subtest.hasOwnProperty("value")) {
        // update an attribute
        try {
          var node = document.getElementById(subtest.element);
          if (node) {
            if (subtest.value === "none") {
              // remove this attribute
              node.removeAttribute(subtest.attribute);
            } else if (subtest.value === '""') {
              node.setAttribute(subtest.attribute, "");
            } else if (subtest.value.match(/^"/) ) {
              var v = subtest.value;
              v = v.replace(/^"/, '');
              v = v.replace(/"$/, '');
              node.setAttribute(subtest.attribute, v);
            } else {
              node.setAttribute(subtest.attribute, subtest.value);
            }
          }
        }
        catch (e) {
          test(function() {
            assert_true(false, "Subtest attribute failed to update: " +e);
          }, "Attribute subtest " + testNum);
        }
      } else {
        test(function() {
          var err = "";
          if (!subtest.hasOwnProperty("attribute")) {
            err += "Attribute subtest has no attribute property; ";
          } else if (!subtest.hasOwnProperty("value")) {
            err += "Attribute subtest has no value property; ";
          } else if (!subtest.hasOwnProperty("element")) {
            err += "Attribute subtest has no element property; ";
          }
          assert_true(false, err);
        }, "Attribute subtest " + testNum );
      }
    }
    return;
  },



  // raiseEvent - throw an event at an item
  /**
   * @param {integer} testNum - The subtest number
   * @param {object} subtest - event information to throw
   */
  raiseEvent: function(testNum, subtest) {
    "use strict";
    var evt;
    if (subtest) {
      var kp = function(target, key) {
        evt = document.createEvent("KeyboardEvent");
        evt.initKeyEvent ("keypress", true, true, window,
                          0, 0, 0, 0, 0, "e".charCodeAt(0));
        target.dispatchEvent(evt);
      };
      if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) {
        // throw an event
        try {
          var node = document.getElementById(subtest.element);
          if (node) {
            if (subtest.event === "focus") {
              node.focus();
            } else if (subtest.event === "select") {
              node.click();
            } else if (subtest.event.startsWith('key:')) {
              var key = subtest.event.replace('key:', '');
              evt = new KeyboardEvent("keypress", { "key": key});
              node.dispatchEvent(evt);
            } else {
              evt = new Event(subtest.element);
              node.dispatchEvent(evt);
            }
          }
        }
        catch (e) {
          test(function() {
            assert_true(false, "Subtest event failed to dispatch: " +e);
          }, "Event subtest " + testNum);
        }
      } else {
        test(function() {
          var err = "";
          if (!subtest.hasOwnProperty("event")) {
            err += "Event subtest has no event property; ";
          } else if (!subtest.hasOwnProperty("element")) {
            err += "Event subtest has no element property; ";
          }
          assert_true(false, err);
        }, "Event subtest " + testNum );
      }
    }
    return;
  },

  // runScript - run a script in the context of the window
  /**
   * @param {integer} testNum - The subtest number
   * @param {object} subtest - script and related information
   */
  runScript: function(testNum, subtest) {
    "use strict";
    if (subtest) {
      if (subtest.hasOwnProperty("script") && typeof subtest.script === "string") {
        try {
          /* jshint evil:true */
          eval(subtest.script);
        }
        catch (e) {
          test(function() {
            assert_true(false, "Subtest script " + subtest.script + " failed to evaluate: " +e);
          }, "Event subtest " + testNum);
        }
      } else {
        test(function() {
          assert_true(false, "Event subtest has no script property");
        }, "Event subtest " + testNum );
      }
    }
    return;
  },

  // runTest - process subtest
  /**
   * @param {integer} testNum - The subtest number
   * @param {string} API - name of the API being tested
   * @param {object} subtest - a subtest to run; contains 'title', 'element', and
   * 'test array'
   * @returns {Promise} - a Promise that resolves when the test completes
   */
  runTest: function(testNum, API, subtest) {
    'use strict';

    var data = {
      "title" : subtest.title,
      "id" : subtest.element,
      "data": this.normalize(subtest.test[API])
    };

    return new Promise(function(resolve) {
      var ANNO = this;
      if (subtest.test[API]) {
        // we actually have a test to run
        promise_test(function() {
          // force a resolve of the promise regardless
          this.add_cleanup(function() { resolve(true); });
          return ANNO.sendTest(data)
            .then(function(res) {
              if (typeof res.body === "object" && res.body.hasOwnProperty("status")) {
                // we got some sort of response
                if (res.body.status === "OK") {
                  // the test ran - yay!
                  var messages = "";
                  var thisResult = null;
                  var theLog = "";
                  var assertionCount = 0;
                  res.body.results.forEach( function (a) {
                    if (typeof a === "object") {
                      // we have a result for this assertion
                      // first, what is the assertion?
                      var aRef = data.data[assertionCount];
                      var assertionText = '"' + aRef.join(" ") +'"';

                      if (a.hasOwnProperty("log") && a.log !== null && a.log !== '' ) {
                        // there is log data - save it
                        theLog += "\n--- Assertion " + assertionCount + " ---";
                        theLog += "\nAssertion: " + assertionText + "\nLog data: "+a.log ;
                      }

                      // is there a message?
                      var theMessage = "";
                      if (a.hasOwnProperty("message")) {
                        theMessage = a.message;
                      }
                      if (!a.hasOwnProperty("result")) {
                        messages += "ATTA did not report a result " + theMessage + "; ";
                      } else if (a.result === "ERROR") {
                        messages += "ATTA reported ERROR with message: " + theMessage + "; ";
                      } else if (a.result === "FAIL") {
                        thisResult = false;
                        messages += assertionText + " failed " + theMessage + "; ";
                      } else if (a.result === "PASS" && thisResult === null) {
                        // if we got a pass and there was no other result thus far
                        // then we are passing
                        thisResult = true;
                      }
                    }
                    assertionCount++;
                  });
                  if (theLog !== "") {
                    ANNO.saveLog("runTest", theLog, subtest);
                  }
                  if (thisResult !== null) {
                    assert_true(thisResult, messages);
                  } else {
                    assert_true(false, "ERROR: No results reported from ATTA; " + messages);
                  }
                } else if (res.body.status === "ERROR") {
                  assert_true(false, "ATTA returned ERROR with message: " + res.body.statusText);
                } else {
                  assert_true(false, "ATTA returned unknown status " + res.body.status + " with message: " + res.body.statusText);
                }
              } else {
                // the return wasn't an object!
                assert_true(false, "ATTA failed to return a result object: returned: "+JSON.stringify(res));
              }
            });
        }, subtest.name );
      } else {
        // there are no test steps for this API.  fake a subtest result
        promise_test(function() {
          // force a resolve of the promise regardless
          this.add_cleanup(function() { resolve(true); });
          return new Promise(function(innerResolve) {
            innerResolve(true);
          })
          .then(function(res) {
            var theLog = "\nSUBTEST NOTRUN: No assertions for API " + API + "\n";
            if (theLog !== "") {
              ANNO.saveLog("runTest", theLog, subtest);
            }
            assert_false(true, "NOTRUN: No assertion for API " + API);
          });
        }, subtest.name );
      }
    }.bind(this));
  },

  // loadTest - load a test from an external JSON file
  //
  // returns a promise that resolves with the contents of the
  // test

  loadTest: function(params) {
    'use strict';

    if (params.hasOwnProperty('stepFile')) {
      // the test is referred to by a file name
      return this._fetch("GET", params.stepFile);
    } // else
    return new Promise(function(resolve, reject) {
      if (params.hasOwnProperty('steps')) {
        resolve(params.steps);
      } else {
        reject("Must supply a 'steps' or 'stepFile' parameter");
      }
    });
  },

  /* dumpLog - put log information into the log div on the page if it exists
   */

  dumpLog: function() {
    'use strict';
    if (this.log !== "") {
      var ref = document.getElementById("ATTAmessages");
      if (ref) {
        // we have a manualMode block.  Populate it
        var content = "<h2>Logging information recorded</h2>";
        if (this.startResponse && this.startResponse.hasOwnProperty("API")) {
          content += "<h3>ATTA Information</h3>";
          content += "<pre>"+JSON.stringify(this.startResponse, null, "  ")+"</pre>";
        }
        content += "<textarea rows='50' style='width:100%'>"+this.log+"</textarea>";
        ref.innerHTML = content ;
      }
    }
  },

  /* saveLog - capture logging information so that it can be displayed on the page after testing is complete
   *
   * @param {string} caller name
   * @param {string} log message
   * @param {object} subtest
   */

  saveLog: function(caller, message, subtest) {
    'use strict';

    if (typeof message === "string" && message !== "") {
      this.log += "============================================================\n";
      this.log += "Message from " + caller + "\n";
      if (subtest && typeof subtest === "object") {
        var API = this.startResponse.API;
        this.log += "\n    SUBTEST TITLE: " + subtest.title;
        this.log += "\n  SUBTEST ELEMENT: " + subtest.element;
        this.log += "\n     SUBTEST DATA: " + JSON.stringify(subtest.test[API]);
        this.log += "\n\n";
      }
      this.log += message;
    }
    return;
  },

  // startTest - send the test start message
  //
  // @returns {Promise} resolves if the start is successful, or rejects with

  startTest: function() {
    'use strict';

    return new Promise(function(resolve, reject) {
      var params = {
        test: this.testName || window.title,
        url: document.location.href
      };

      this._fetch("POST", this.ATTAuri + "/start", null, params)
      .then(function(res) {
        if (res.body.hasOwnProperty("status")) {
          if (res.body.status === "READY") {
            this.startResponse = res.body;
            if (res.body.hasOwnProperty("log")) {
              // there is some logging data - capture it
              this.saveLog("startTest", res.body.log);
            }
            // the system is ready for us - is it really?
            if (res.body.hasOwnProperty("API")) {
              resolve(res);
            } else {
              res.message = "No API in response from ATTA";
              reject(res);
            }
          } else {
            // the system reported something else - fail out with the statusText as a result
            res.message = "ATTA reported an error: " + res.body.statusText;
            reject(res);
          }
        } else {
          res.message = "ATTA did not report a status";
          reject(res);
        }
      }.bind(this))
      .catch(function(res) {
        reject(res);
      });
    }.bind(this));
  },

  // sendEvents - send the list of events the ATTA needs to listen for
  //
  // @param {string} API
  // @param {array} collection - a list of tests
  // @returns {Promise} resolves if the message is successful, or rejects with

  sendEvents: function(API, collection) {
    'use strict';

    return new Promise(function(resolve, reject) {
      var eList = this.eventList(API, collection) ;
      if (eList && eList.length) {
        var params = {
          events: eList
        };

        this._fetch("POST", this.ATTAuri + "/startlisten", null, params)
        .then(function(res) {
          if (res.body.hasOwnProperty("status")) {
            if (res.body.status === "READY") {
              if (res.body.hasOwnProperty("log")) {
                // there is some logging data - capture it
                this.saveLog("sendEvents", res.body.log);
              }
              resolve(res.body.status);
            } else {
              // the system reported something else - fail out with the statusText as a result
              res.message = "ATTA reported an error: " + res.body.statusText;
              reject(res);
            }
          } else {
            res.message = "ATTA did not report a status";
            reject(res);
          }
        }.bind(this))
        .catch(function(res) {
          reject(res);
        });
      } else {
        // there are no events
        resolve("NOEVENTS");
      }
    }.bind(this));
  },

  sendStopListen: function() {
    'use strict';

    return this._fetch("POST", this.ATTAuri + "/stoplisten", null, null);
  },

  // sendTest - send test data to an ATTA and wait for a response
  //
  // returns a promise that resolves with the results of the test

  sendTest: function(testData) {
    'use strict';

    if (typeof testData !== "string") {
      testData = JSON.stringify(testData);
    }
    var ret = this._fetch("POST", this.ATTAuri + "/test", null, testData, true);
    ret.then(function(res) {
      if (res.body.hasOwnProperty("log")) {
        // there is some logging data - capture it
        this.saveLog("sendTest", res.body.log);
      }
    }.bind(this));
    return ret;
  },

  endTest: function() {
    'use strict';

    return this._fetch("GET", this.ATTAuri + "/end");
  },

  /* normalize - ensure subtest data conforms to ATTA spec
   */

  normalize: function( data ) {
    'use strict';

    var ret = [] ;

    if (data) {
      data.forEach(function(assert) {
        var normal = [] ;
        // ensure if there is a value list it is compressed
        if (Array.isArray(assert)) {
          // we have an array
          normal[0] = assert[0];
          normal[1] = assert[1];
          normal[2] = assert[2];
          if ("string" === typeof assert[3] && assert[3].match(/^\[.*\]$/)) {
            // it is a string and matches the valuelist pattern
            normal[3] = assert[3].replace(/, +/, ',');
          } else {
            normal[3] = assert[3];
          }
          ret.push(normal);
        } else {
          ret.push(assert);
        }
      });
    }
    return ret;
  },

  // _fetch - return a promise after sending data
  //
  // Resolves with the returned information in a structure
  // including:
  //
  // xhr - a raw xhr object
  // headers - an array of headers sent in the request
  // status - the status code
  // statusText - the text of the return status
  // text - raw returned data
  // body - an object parsed from the returned content
  //

  _fetch: function (method, url, headers, content, parse) {
    'use strict';
    if (method === null || method === undefined) {
      method = "GET";
    }
    if (parse === null || parse === undefined) {
      parse = true;
    }
    if (headers === null || headers === undefined) {
      headers = [];
    }


    // note that this Promise always resolves - there is no reject
    // condition

    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();

      // this gets returned when the request completes
      var resp = {
        xhr: xhr,
        headers: null,
        status: 0,
        statusText: "",
        body: null,
        text: ""
      };

      xhr.open(method, url);

      // headers?
      headers.forEach(function(ref) {
        xhr.setRequestHeader(ref[0], ref[1]);
      });

      //if (this.timeout) {
      //  xhr.timeout = this.timeout;
      //}

      xhr.ontimeout = function() {
        resp.timeout = this.timeout;
        resolve(resp);
      };

      xhr.onerror = function() {
        if (this.status) {
          resp.status = this.status;
          resp.statusText = xhr.statusText;
        } else if (this.status === 0) {
          resp.status = 0;
          resp.statusText = "No response from ATTA";
        }
        reject(resp);
      };

      xhr.onload = function () {
        resp.status = this.status;
        if (this.status >= 200 && this.status < 300) {
          var d = xhr.response;
          // return the raw text of the response
          resp.text = d;
          // we have it; what is it?
          if (parse) {
            try {
              d = JSON.parse(d);
              resp.body = d;
            }
            catch(err) {
              resp.body = null;
            }
          }
          resolve(resp);
        } else {
          reject({
            status: this.status,
            statusText: xhr.statusText
          });
        }
      };

      if (content !== null && content !== undefined) {
        if ("object" === typeof(content)) {
          xhr.send(JSON.stringify(content));
        } else if ("function" === typeof(content)) {
          xhr.send(content());
        } else if ("string" === typeof(content)) {
          xhr.send(content);
        }
      } else {
        xhr.send();
      }
    });
  },

};

// vim: set ts=2 sw=2:
