function sendRequest(method, uri, headers, data, onSuccess, onError) {
  var xhr = new XMLHttpRequest();
  xhr.onload = function () {
    if (xhr.status === 200) {
      onSuccess(xhr.response);
    } else {
      if (onError) onError(xhr.status, xhr.response);
    }
  };
  xhr.onerror = function () {
    if (onError) onError();
  };
  xhr.open(method, WaveService.uriPrefix + uri, true);
  for (var header in headers) {
    xhr.setRequestHeader(header, headers[header]);
  }
  xhr.send(data);
  return xhr;
}

var WEB_ROOT = "{{WEB_ROOT}}";
var HTTP_PORT = "{{HTTP_PORT}}";
var HTTPS_PORT = "{{HTTPS_PORT}}";
var OPEN = "open";
var CLOSED = "closed";

var WaveService = {
  uriPrefix: WEB_ROOT,
  socket: {
    state: CLOSED,
    onMessage: function () {},
    onOpen: function () {},
    onClose: function () {},
    send: function () {},
    close: function () {},
    onStateChange: function () {},
  },
  // SESSIONS API
  createSession: function (configuration, onSuccess, onError) {
    var data = JSON.stringify({
      tests: configuration.tests,
      types: configuration.types,
      timeouts: configuration.timeouts,
      reference_tokens: configuration.referenceTokens,
      expiration_date: configuration.expirationDate,
      labels: configuration.labels,
    });
    sendRequest(
      "POST",
      "api/sessions",
      { "Content-Type": "application/json" },
      data,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject.token);
      },
      onError
    );
  },
  readSession: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/sessions/" + token,
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess({
          token: jsonObject.token,
          tests: jsonObject.tests,
          types: jsonObject.types,
          userAgent: jsonObject.user_agent,
          labels: jsonObject.labels,
          timeouts: jsonObject.timeouts,
          browser: jsonObject.browser,
          isPublic: jsonObject.is_public,
          referenceTokens: jsonObject.reference_tokens,
        });
      },
      onError
    );
  },
  readMultipleSessions: function (tokens, onSuccess, onError) {
    var requestsLeft = tokens.length;
    if (requestsLeft === 0) onSuccess([]);
    var configurations = [];
    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];
      WaveService.readSession(
        token,
        function (configuration) {
          requestsLeft--;
          configurations.push(configuration);
          if (requestsLeft === 0) onSuccess(configurations);
        },
        function (status) {
          if (status === 404) requestsLeft--;
          if (status !== 404 && onError) onError();
          if (requestsLeft === 0) onSuccess(configurations);
        }
      );
    }
  },
  readSessionStatus: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/sessions/" + token + "/status",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        var dateStarted = null;
        if (jsonObject.date_started) {
          dateStarted = new Date(jsonObject.date_started);
        }
        var dateFinished = null;
        if (jsonObject.date_finished) {
          dateFinished = new Date(jsonObject.date_finished);
        }
        var expirationDate = null;
        if (jsonObject.expiration_date) {
          expirationDate = new Date(jsonObject.expiration_date);
        }
        onSuccess({
          token: jsonObject.token,
          dateStarted: dateStarted,
          dateFinished: dateFinished,
          testFilesCount: jsonObject.test_files_count,
          testFilesCompleted: jsonObject.test_files_completed,
          status: jsonObject.status,
          expirationDate: expirationDate,
        });
      },
      function () {
        if (onError) onError();
      }
    );
  },
  readMultipleSessionStatuses: function (tokens, onSuccess, onError) {
    var requestsLeft = tokens.length;
    if (requestsLeft === 0) onSuccess([]);
    var statuses = [];
    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];
      WaveService.readSessionStatus(
        token,
        function (status) {
          requestsLeft--;
          statuses.push(status);
          if (requestsLeft === 0) onSuccess(statuses);
        },
        function () {
          requestsLeft--;
          if (requestsLeft === 0) onSuccess(statuses);
        }
      );
    }
  },
  readPublicSessions: function (onSuccess, onError) {
    sendRequest(
      "GET",
      "api/sessions/public",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject);
      },
      onError
    );
  },
  updateSession: function (token, configuration, onSuccess, onError) {
    var data = JSON.stringify({
      tests: configuration.tests,
      types: configuration.types,
      timeouts: configuration.timeouts,
      reference_tokens: configuration.referenceTokens,
      expiration_date: configuration.expirationDate,
      type: configuration.type,
    });
    sendRequest(
      "PUT",
      "api/sessions/" + token,
      { "Content-Type": "application/json" },
      data,
      function () {
        onSuccess();
      },
      onError
    );
  },
  updateLabels: function (token, labels, onSuccess, onError) {
    var data = JSON.stringify({ labels: labels });
    sendRequest(
      "PUT",
      "api/sessions/" + token + "/labels",
      { "Content-Type": "application/json" },
      data,
      function () {
        if (onSuccess) onSuccess();
      },
      onError
    );
  },
  findToken: function (fragment, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/sessions/" + fragment,
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject.token);
      },
      onError
    );
  },
  startSession: function (token, onSuccess, onError) {
    sendRequest(
      "POST",
      "api/sessions/" + token + "/start",
      null,
      null,
      function () {
        onSuccess();
      },
      onError
    );
  },
  pauseSession: function (token, onSuccess, onError) {
    sendRequest(
      "POST",
      "api/sessions/" + token + "/pause",
      null,
      null,
      function () {
        onSuccess();
      },
      onError
    );
  },
  stopSession: function (token, onSuccess, onError) {
    sendRequest(
      "POST",
      "api/sessions/" + token + "/stop",
      null,
      null,
      function () {
        onSuccess();
      },
      onError
    );
  },
  resumeSession: function (token, resumeToken, onSuccess, onError) {
    var data = JSON.stringify({ resume_token: resumeToken });
    sendRequest(
      "POST",
      "api/sessions/" + token + "/resume",
      { "Content-Type": "application/json" },
      data,
      function () {
        if (onSuccess) onSuccess();
      },
      function (response) {
        if (onError) onError(response);
      }
    );
  },
  deleteSession: function (token, onSuccess, onError) {
    sendRequest(
      "DELETE",
      "api/sessions/" + token,
      null,
      null,
      function () {
        onSuccess();
      },
      onError
    );
  },

  // TESTS API
  readTestList: function (onSuccess, onError) {
    sendRequest(
      "GET",
      "api/tests",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject);
      },
      onError
    );
  },
  readNextTest: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/tests/" + token + "/next",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject.next_test);
      },
      onError
    );
  },
  readLastCompletedTests: function (token, resultTypes, onSuccess, onError) {
    var status = "";
    if (resultTypes) {
      for (var i = 0; i < resultTypes.length; i++) {
        var type = resultTypes[i];
        status += type + ",";
      }
    }
    sendRequest(
      "GET",
      "api/tests/" + token + "/last_completed?status=" + status,
      null,
      null,
      function (response) {
        var tests = JSON.parse(response);
        var parsedTests = [];
        for (var status in tests) {
          for (var i = 0; i < tests[status].length; i++) {
            var path = tests[status][i];
            parsedTests.push({ path: path, status: status });
          }
        }
        onSuccess(parsedTests);
      },
      onError
    );
  },
  readMalfunctioningTests: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/tests/" + token + "/malfunctioning",
      null,
      null,
      function (response) {
        var tests = JSON.parse(response);
        onSuccess(tests);
      },
      function (response) {
        var errorMessage = JSON.parse(response).error;
        onError(errorMessage);
      }
    );
  },
  updateMalfunctioningTests: function (
    token,
    malfunctioningTests,
    onSuccess,
    onError
  ) {
    var data = JSON.stringify(malfunctioningTests);
    sendRequest(
      "PUT",
      "api/tests/" + token + "/malfunctioning",
      { "Content-Type": "application/json" },
      data,
      function () {
        onSuccess();
      },
      function (response) {
        var errorMessage = JSON.parse(response).error;
        onError(errorMessage);
      }
    );
  },
  readAvailableApis: function (onSuccess, onError) {
    sendRequest(
      "GET",
      "api/tests/apis",
      null,
      null,
      function (response) {
        var apis = JSON.parse(response);
        onSuccess(apis);
      },
      function (response) {
        if (!onError) return;
        var errorMessage = JSON.parse(response).error;
        onError(errorMessage);
      }
    );
  },

  // RESULTS API
  createResult: function (token, result, onSuccess, onError) {
    sendRequest(
      "POST",
      "api/results/" + token,
      { "Content-Type": "application/json" },
      JSON.stringify(result),
      function () {
        onSuccess();
      },
      onError
    );
  },
  readResults: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/results/" + token,
      null,
      null,
      function (response) {
        onSuccess(JSON.parse(response));
      },
      onError
    );
  },
  readResultsCompact: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/results/" + token + "/compact",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject);
      },
      onError
    );
  },
  readResultComparison: function (tokens, onSuccess, onError) {
    var comparison = {};
    var fetchComplete = function (results) {
      comparison.total = {};
      for (var i = 0; i < results.length; i++) {
        var result = results[i];
        var token = result.token;
        comparison[token] = {};
        for (var api in result) {
          if (api === "token") continue;
          comparison[token][api] = result[api].pass;
          if (!comparison.total[api]) {
            var total = 0;
            for (var status in result[api]) {
              total = total + result[api][status];
            }
            comparison.total[api] = total;
          }
        }
      }
      onSuccess(comparison);
    };
    var requestsLeft = tokens.length;
    if (requestsLeft === 0) onSuccess([]);
    var results = [];
    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];
      (function (token) {
        WaveService.readResultsCompact(
          token,
          function (result) {
            requestsLeft--;
            result.token = token;
            results.push(result);
            if (requestsLeft === 0) fetchComplete(results);
          },
          function (responseStatus) {
            if (responseStatus === 404) requestsLeft--;
            if (status !== 404 && onError) onError();
            if (requestsLeft === 0) fetchComplete(results);
          }
        );
      })(token);
    }
  },
  downloadResults: function (token) {
    location.href = "api/results/" + token + "/export";
  },
  downloadApiResult: function (token, api) {
    location.href = "api/results/" + token + "/" + api + "/json";
  },
  downloadAllApiResults: function (token, api) {
    location.href = "api/results/" + token + "/json";
  },
  downloadReport: function (token, api) {
    location.href = "api/results/" + token + "/" + api + "/report";
  },
  importResults: function (data, onSuccess, onError) {
    sendRequest(
      "POST",
      "api/results/import",
      { "Content-Type": "application/octet-stream" },
      data,
      function (response) {
        var token = JSON.parse(response).token;
        onSuccess(token);
      },
      function (status, response) {
        var errorMessage;
        if (status === 500) {
          errorMessage = "Internal server error.";
        } else {
          errorMessage = JSON.parse(response).error;
        }
        onError(errorMessage);
      }
    );
  },
  readReportUri: function (token, api, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/results/" + token + "/" + api + "/reporturl",
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject.uri);
      },
      onError
    );
  },
  downloadMultiReport: function (tokens, api) {
    location.href = "api/results/" + api + "/report?tokens=" + tokens.join(",");
  },
  readMultiReportUri: function (tokens, api, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/results/" + api + "/reporturl?tokens=" + tokens.join(","),
      null,
      null,
      function (response) {
        var jsonObject = JSON.parse(response);
        onSuccess(jsonObject.uri);
      },
      onError
    );
  },
  downloadResultsOverview: function (token) {
    location.href = "api/results/" + token + "/overview";
  },

  // DEVICES API
  _device_token: null,
  _deviceEventListeners: {},
  _deviceEventNumbers: {},
  registerDevice: function (onSuccess, onError) {
    sendRequest(
      "POST",
      "api/devices",
      null,
      null,
      function (response) {
        var data = JSON.parse(response);
        WaveService._device_token = data.token;
        onSuccess(data.token);
      },
      onError
    );
  },
  readDevice: function (token, onSuccess, onError) {
    sendRequest(
      "GET",
      "api/devices/" + token,
      null,
      null,
      function (response) {
        if (!onSuccess) return;
        var data = JSON.parse(response);
        onSuccess(data);
      },
      function (error) {
        if (!onError) return;
        onError(error);
      }
    );
  },
  DEVICE_ADDED_EVENT: "device_added",
  DEVICE_REMOVED_EVENT: "device_removed",
  START_SESSION: "start_session",
  addDeviceEventListener: function (token, callback) {
    var listeners = WaveService._deviceEventListeners;
    if (!listeners[token]) listeners[token] = [];
    listeners[token].push(callback);
    WaveService._deviceEventListeners = listeners;
    WaveService.listenDeviceEvents(token);
  },
  removeDeviceEventListener: function (callback) {
    var listeners = WaveService._deviceEventListeners;
    for (var token of Object.keys(listeners)) {
      var index = listeners[token].indexOf(callback);
      if (index === -1) continue;
      listeners[token].splice(index, 1);
      break;
    }
    WaveService._deviceEventListeners = listeners;
  },
  listenDeviceEvents: function (token) {
    var listeners = WaveService._deviceEventListeners;
    if (!listeners[token] || listeners.length === 0) return;
    var url = "api/devices/" + token + "/events";
    var lastEventNumber = WaveService._deviceEventNumbers[token];
    if (lastEventNumber) {
      url += "?last_active=" + lastEventNumber;
    }
    WaveService.listenHttpPolling(
      url,
      function (response) {
        if (!response) {
          WaveService.listenDeviceEvents(token);
          return;
        }
        for (var listener of listeners[token]) {
          listener(response);
        }
        WaveService._deviceEventNumbers[token] = lastEventNumber;
        WaveService.listenDeviceEvents(token);
      },
      function () {
        setTimeout(function () {
          WaveService.listenDeviceEvents();
        }, 1000);
      }
    );
  },
  sendDeviceEvent: function (device_token, event, onSuccess, onError) {
    var data = JSON.stringify({
      type: event.type,
      data: event.data,
    });
    sendRequest(
      "POST",
      "api/devices/" + device_token + "/events",
      { "Content-Type": "application/json" },
      data,
      onSuccess,
      onError
    );
  },
  addGlobalDeviceEventListener: function (callback) {
    WaveService._globalDeviceEventListeners.push(callback);
    WaveService.listenGlobalDeviceEvents();
  },
  removeGlobalDeviceEventListener: function (callback) {
    var index = WaveService._globalDeviceEventListeners.indexOf(callback);
    WaveService._globalDeviceEventListeners.splice(index, 1);
  },
  listenGlobalDeviceEvents: function () {
    var listeners = WaveService._globalDeviceEventListeners;
    if (listeners.length === 0) return;
    var query = "";
    if (WaveService._device_token) {
      query = "?device_token=" + WaveService._device_token;
    }
    WaveService.listenHttpPolling(
      "api/devices/events" + query,
      function (response) {
        if (!response) {
          WaveService.listenGlobalDeviceEvents();
          return;
        }
        for (var listener of listeners) {
          listener(response);
        }
        WaveService.listenGlobalDeviceEvents();
      },
      function () {
        setTimeout(function () {
          WaveService.listenGlobalDeviceEvents();
        }, 1000);
      }
    );
  },
  sendGlobalDeviceEvent: function (event, onSuccess, onError) {
    var data = JSON.stringify({
      type: event.type,
      data: event.data,
    });
    sendRequest(
      "POST",
      "api/devices/events",
      { "Content-Type": "application/json" },
      data,
      onSuccess,
      onError
    );
  },

  // GENERAL API
  readStatus: function (onSuccess, onError) {
    sendRequest(
      "GET",
      "api/status",
      null,
      null,
      function (response) {
        var data = JSON.parse(response);
        var configuration = {
          readSessionsEnabled: data.read_sessions_enabled,
          importResultsEnabled: data.import_results_enabled,
          reportsEnabled: data.reports_enabled,
          versionString: data.version_string,
          testTypeSelectionEnabled: data.test_type_selection_enabled,
          testFileSelectionEnabled: data.test_file_selection_enabled
        };
        onSuccess(configuration);
      },
      onError
    );
  },

  // UTILITY
  addRecentSession: function (token) {
    if (!token) return;
    var state = WaveService.getState();
    if (!state.recent_sessions) state.recent_sessions = [];
    if (state.recent_sessions.indexOf(token) !== -1) return;
    state.recent_sessions.unshift(token);
    WaveService.setState(state);
  },
  addRecentSessions: function (tokens) {
    for (var i = 0; i < tokens.length; i++) {
      var token = tokens[i];
      WaveService.addRecentSession(token);
    }
  },
  getPinnedSessions: function () {
    var state = WaveService.getState();
    if (!state || !state.pinned_sessions) return [];
    return state.pinned_sessions;
  },
  addPinnedSession: function (token) {
    if (!token) return;
    var state = WaveService.getState();
    if (!state.pinned_sessions) state.pinned_sessions = [];
    if (state.pinned_sessions.indexOf(token) !== -1) return;
    state.pinned_sessions.unshift(token);
    WaveService.setState(state);
  },
  getRecentSessions: function () {
    var state = WaveService.getState();
    if (!state || !state.recent_sessions) return [];
    return state.recent_sessions;
  },
  setRecentSessions: function (sessionTokens) {
    var state = WaveService.getState();
    state.recent_sessions = sessionTokens;
    WaveService.setState(state);
  },
  removePinnedSession: function (token) {
    if (!token) return;
    var state = WaveService.getState();
    if (!state.pinned_sessions) return;
    var index = state.pinned_sessions.indexOf(token);
    if (index === -1) return;
    state.pinned_sessions.splice(index, 1);
    WaveService.setState(state);
  },
  removeRecentSession: function (token) {
    var state = WaveService.getState();
    if (!state.recent_sessions) return;
    var index = state.recent_sessions.indexOf(token);
    if (index === -1) return;
    state.recent_sessions.splice(index, 1);
    WaveService.setState(state);
  },
  getState: function () {
    if (!window.localStorage) return null;
    var storage = window.localStorage;
    var state = JSON.parse(storage.getItem("wave"));
    if (!state) return {};
    return state;
  },
  setState: function (state) {
    if (!window.localStorage) return null;
    var storage = window.localStorage;
    storage.setItem("wave", JSON.stringify(state));
  },
  _globalDeviceEventListeners: [],
  _sessionEventListeners: {},
  _sessionEventNumbers: {},
  listenHttpPolling: function (url, onSuccess, onError) {
    var uniqueId = new Date().getTime();
    if (url.indexOf("?") === -1) {
      url = url + "?id=" + uniqueId;
    } else {
      url = url + "&id=" + uniqueId;
    }
    sendRequest(
      "GET",
      url,
      null,
      null,
      function (response) {
        if (!response) {
          onSuccess(null);
          return;
        }
        onSuccess(JSON.parse(response));
      },
      onError
    );
  },
  addSessionEventListener: function (token, callback) {
    var listeners = WaveService._sessionEventListeners;
    if (!listeners[token]) listeners[token] = [];
    if (listeners[token].indexOf(callback) >= 0) return;
    listeners[token].push(callback);
    WaveService._sessionEventListeners = listeners;
    WaveService.listenSessionEvents(token);
  },
  removeSessionEventListener: function (callback) {
    var listeners = WaveService._sessionEventListeners;
    for (var token of Object.keys(listeners)) {
      var index = listeners[token].indexOf(callback);
      if (index === -1) continue;
      listeners[token].splice(index, 1);
      break;
    }
    WaveService._sessionEventListeners = listeners;
  },
  listenSessionEvents: function (token) {
    var listeners = WaveService._sessionEventListeners;
    if (!listeners[token] || listeners.length === 0) return;
    var url = "api/sessions/" + token + "/events";
    var lastEventNumber = WaveService._sessionEventNumbers[token];
    if (lastEventNumber) {
      url += "?last_event=" + lastEventNumber;
    }
    WaveService.listenHttpPolling(
      url,
      function (response) {
        if (!response) {
          WaveService.listenSessionEvents(token);
          return;
        }
        var lastEventNumber = 0;
        for (var listener of listeners[token]) {
          for (var event of response) {
            if (event.number > lastEventNumber) {
              lastEventNumber = event.number;
            }
            listener(event);
          }
        }
        WaveService._sessionEventNumbers[token] = lastEventNumber;
        WaveService.listenSessionEvents(token);
      },
      function () {
        setTimeout(function () {
          WaveService.listenSessionEvents();
        }, 1000);
      }
    );
  },
  openSession: function (token) {
    location.href = "/results.html?token=" + token;
  },
};

if (!Object.keys)
  Object.keys = function (o) {
    if (o !== Object(o))
      throw new TypeError("Object.keys called on a non-object");
    var k = [],
      p;
    for (p in o) if (Object.prototype.hasOwnProperty.call(o, p)) k.push(p);
    return k;
  };
