module.exports = function init(global, jsUtil, cookieHandler, messages, base64, errorCodes, dependencyValidator, ponyfills) {
  var validSerializers = ['urlencoded', 'json', 'utf8', 'raw', 'multipart'];
  var validCertModes = ['default', 'nocheck', 'pinned', 'legacy'];
  var validClientAuthModes = ['none', 'systemstore', 'buffer'];
  var validHttpMethods = ['get', 'put', 'post', 'patch', 'head', 'delete', 'options', 'upload', 'download'];
  var validResponseTypes = ['text', 'json', 'arraybuffer', 'blob'];

  var interface = {
    b64EncodeUnicode: b64EncodeUnicode,
    checkClientAuthMode: checkClientAuthMode,
    checkClientAuthOptions: checkClientAuthOptions,
    checkDownloadFilePath: checkDownloadFilePath,
    checkFollowRedirectValue: checkFollowRedirectValue,
    checkForBlacklistedHeaderKey: checkForBlacklistedHeaderKey,
    checkForInvalidHeaderValue: checkForInvalidHeaderValue,
    checkSerializer: checkSerializer,
    checkSSLCertMode: checkSSLCertMode,
    checkTimeoutValue: checkTimeoutValue,
    checkUploadFileOptions: checkUploadFileOptions,
    getMergedHeaders: getMergedHeaders,
    processData: processData,
    handleMissingCallbacks: handleMissingCallbacks,
    handleMissingOptions: handleMissingOptions,
    injectCookieHandler: injectCookieHandler,
    injectFileEntryHandler: injectFileEntryHandler,
    injectRawResponseHandler: injectRawResponseHandler,
  };

  // expose all functions for testing purposes
  if (init.debug) {
    interface.mergeHeaders = mergeHeaders;
    interface.checkForValidStringValue = checkForValidStringValue;
    interface.checkKeyValuePairObject = checkKeyValuePairObject;
    interface.checkHttpMethod = checkHttpMethod;
    interface.checkResponseType = checkResponseType;
    interface.checkHeadersObject = checkHeadersObject;
    interface.checkParamsObject = checkParamsObject;
    interface.resolveCookieString = resolveCookieString;
    interface.createFileEntry = createFileEntry;
    interface.getCookieHeader = getCookieHeader;
    interface.getMatchingHostHeaders = getMatchingHostHeaders;
    interface.getAllowedDataTypes = getAllowedDataTypes;
  }

  return interface;

  // Thanks Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22
  function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
      return String.fromCharCode('0x' + p1);
    }));
  }

  function mergeHeaders(globalHeaders, localHeaders) {
    var globalKeys = Object.keys(globalHeaders);
    var key;

    for (var i = 0; i < globalKeys.length; i++) {
      key = globalKeys[i];

      if (!localHeaders.hasOwnProperty(key)) {
        localHeaders[key] = globalHeaders[key];
      }
    }

    return localHeaders;
  }

  function checkForValidStringValue(list, value, onInvalidValueMessage) {
    if (jsUtil.getTypeOf(value) !== 'String') {
      throw new Error(onInvalidValueMessage + ' ' + list.join(', '));
    }

    value = value.trim().toLowerCase();

    if (list.indexOf(value) === -1) {
      throw new Error(onInvalidValueMessage + ' ' + list.join(', '));
    }

    return value;
  }

  function checkKeyValuePairObject(obj, allowedChildren, onInvalidValueMessage) {
    if (jsUtil.getTypeOf(obj) !== 'Object') {
      throw new Error(onInvalidValueMessage);
    }

    var keys = Object.keys(obj);

    for (var i = 0; i < keys.length; i++) {
      if (allowedChildren.indexOf(jsUtil.getTypeOf(obj[keys[i]])) === -1) {
        throw new Error(onInvalidValueMessage);
      }
    }

    return obj;
  }

  function checkArray(array, allowedDataTypes, onInvalidValueMessage) {
    if (jsUtil.getTypeOf(array) !== 'Array') {
      throw new Error(onInvalidValueMessage);
    }

    for (var i = 0; i < array.length; ++i) {
      if (allowedDataTypes.indexOf(jsUtil.getTypeOf(array[i])) === -1) {
        throw new Error(onInvalidValueMessage);
      }
    }

    return array;
  }

  function checkHttpMethod(method) {
    return checkForValidStringValue(validHttpMethods, method, messages.INVALID_HTTP_METHOD);
  }

  function checkResponseType(type) {
    return checkForValidStringValue(validResponseTypes, type, messages.INVALID_RESPONSE_TYPE);
  }

  function checkSerializer(serializer) {
    return checkForValidStringValue(validSerializers, serializer, messages.INVALID_DATA_SERIALIZER);
  }

  function checkSSLCertMode(mode) {
    return checkForValidStringValue(validCertModes, mode, messages.INVALID_SSL_CERT_MODE);
  }

  function checkClientAuthMode(mode) {
    return checkForValidStringValue(validClientAuthModes, mode, messages.INVALID_CLIENT_AUTH_MODE);
  }

  function checkClientAuthOptions(mode, options) {
    options = options || {};

    // none
    if (mode === validClientAuthModes[0]) {
      return {
        alias: null,
        rawPkcs: null,
        pkcsPassword: ''
      };
    }

    if (jsUtil.getTypeOf(options) !== 'Object') {
      throw new Error(messages.INVALID_CLIENT_AUTH_OPTIONS);
    }

    // systemstore
    if (mode === validClientAuthModes[1]) {
      if (jsUtil.getTypeOf(options.alias) !== 'String'
        && jsUtil.getTypeOf(options.alias) !== 'Undefined') {
        throw new Error(messages.INVALID_CLIENT_AUTH_ALIAS);
      }

      return {
        alias: jsUtil.getTypeOf(options.alias) === 'Undefined' ? null : options.alias,
        rawPkcs: null,
        pkcsPassword: ''
      };
    }

    // buffer
    if (mode === validClientAuthModes[2]) {
      if (jsUtil.getTypeOf(options.rawPkcs) !== 'ArrayBuffer') {
        throw new Error(messages.INVALID_CLIENT_AUTH_RAW_PKCS);
      }

      if (jsUtil.getTypeOf(options.pkcsPassword) !== 'String') {
        throw new Error(messages.INVALID_CLIENT_AUTH_PKCS_PASSWORD);
      }

      return {
        alias: null,
        rawPkcs: options.rawPkcs,
        pkcsPassword: options.pkcsPassword
      }
    }
  }

  function checkForBlacklistedHeaderKey(key) {
    if (key.toLowerCase() === 'cookie') {
      throw new Error(messages.ADDING_COOKIES_NOT_SUPPORTED);
    }

    return key;
  }

  function checkForInvalidHeaderValue(value) {
    if (jsUtil.getTypeOf(value) !== 'String') {
      throw new Error(messages.INVALID_HEADER_VALUE);
    }

    return value;
  }

  function checkTimeoutValue(timeout) {
    if (jsUtil.getTypeOf(timeout) !== 'Number' || timeout < 0) {
      throw new Error(messages.INVALID_TIMEOUT_VALUE);
    }

    return timeout;
  }

  function checkFollowRedirectValue(follow) {
    if (jsUtil.getTypeOf(follow) !== 'Boolean') {
      throw new Error(messages.INVALID_FOLLOW_REDIRECT_VALUE);
    }

    return follow;
  }

  function checkHeadersObject(headers) {
    return checkKeyValuePairObject(headers, ['String'], messages.TYPE_MISMATCH_HEADERS);
  }

  function checkParamsObject(params) {
    return checkKeyValuePairObject(params, ['String', 'Array'], messages.TYPE_MISMATCH_PARAMS);
  }

  function checkDownloadFilePath(filePath) {
    if (!filePath || jsUtil.getTypeOf(filePath) !== 'String') {
      throw new Error(messages.INVALID_DOWNLOAD_FILE_PATH);
    }

    return filePath;
  }

  function checkUploadFileOptions(filePaths, names) {
    if (jsUtil.getTypeOf(filePaths) === 'String') {
      filePaths = [filePaths];
    }

    if (jsUtil.getTypeOf(names) === 'String') {
      names = [names];
    }

    var opts = {
      filePaths: checkArray(filePaths, ['String'], messages.TYPE_MISMATCH_FILE_PATHS),
      names: checkArray(names, ['String'], messages.TYPE_MISMATCH_NAMES)
    };

    if (!opts.filePaths.length) {
      throw new Error(messages.EMPTY_FILE_PATHS);
    }

    if (!opts.names.length) {
      throw new Error(messages.EMPTY_NAMES);
    }

    return opts;
  }

  function resolveCookieString(headers) {
    var keys = Object.keys(headers || {});

    for (var i = 0; i < keys.length; ++i) {
      if (keys[i].match(/^set-cookie$/i)) {
        return headers[keys[i]];
      }
    }

    return null;
  }

  function createFileEntry(rawEntry) {
    var entry = new (require('cordova-plugin-file.FileEntry'))();

    entry.isDirectory = rawEntry.isDirectory;
    entry.isFile = rawEntry.isFile;
    entry.name = rawEntry.name;
    entry.fullPath = rawEntry.fullPath;
    entry.filesystem = new FileSystem(rawEntry.filesystemName || (rawEntry.filesystem == global.PERSISTENT ? 'persistent' : 'temporary'));
    entry.nativeURL = rawEntry.nativeURL;

    return entry;
  }

  function injectCookieHandler(url, cb) {
    return function (response) {
      cookieHandler.setCookieFromString(url, resolveCookieString(response.headers));
      cb(response);
    }
  }

  function injectRawResponseHandler(responseType, success, failure) {
    return function (response) {
      var dataType = jsUtil.getTypeOf(response.data);

      // don't need post-processing if it's already binary type (on browser platform)
      if (dataType === 'ArrayBuffer' || dataType === 'Blob') {
        return success(response);
      }

      if (!response.data) {
        // return null as data if response data is not set
        response.data = null;
        return success(response);
      }

      try {
        // json
        if (responseType === validResponseTypes[1]) {
          response.data = JSON.parse(response.data);
        }

        // arraybuffer
        else if (responseType === validResponseTypes[2]) {
          response.data = base64.toArrayBuffer(response.data);
        }

        // blob
        else if (responseType === validResponseTypes[3]) {
          var buffer = base64.toArrayBuffer(response.data);
          var type = response.headers['content-type'] || '';
          var blob = new Blob([ buffer ], { type: type });
          response.data = blob;
        }

        success(response);
      } catch (error) {
        failure({
          status: errorCodes.POST_PROCESSING_FAILED,
          error: messages.POST_PROCESSING_FAILED + ' ' + error.message,
          url: response.url,
          headers: response.headers
        });
      }
    }
  }

  function injectFileEntryHandler(cb) {
    return function (response) {
      cb(createFileEntry(response.file));
    }
  }

  function getCookieHeader(url) {
    var cookieString = cookieHandler.getCookieString(url);

    if (cookieString.length) {
      return { Cookie: cookieHandler.getCookieString(url) };
    }

    return {};
  }

  function getMatchingHostHeaders(url, headersList) {
    var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
    var domain = matches && matches[1];

    return headersList[domain] || null;
  }

  function getMergedHeaders(url, requestHeaders, predefinedHeaders) {
    var globalHeaders = predefinedHeaders['*'] || {};
    var hostHeaders = getMatchingHostHeaders(url, predefinedHeaders) || {};
    var mergedHeaders = mergeHeaders(globalHeaders, hostHeaders);

    mergedHeaders = mergeHeaders(mergedHeaders, requestHeaders);
    mergedHeaders = mergeHeaders(mergedHeaders, getCookieHeader(url));

    return mergedHeaders;
  }

  function getAllowedDataTypes(dataSerializer) {
    switch (dataSerializer) {
      case 'utf8':
        return ['String'];
      case 'urlencoded':
        return ['Object'];
      case 'json':
        return ['Array', 'Object'];
      case 'raw':
        return ['Uint8Array', 'ArrayBuffer'];
      default:
        return [];
    }
  }

  function getAllowedInstanceTypes(dataSerializer) {
    return dataSerializer === 'multipart' ? ['FormData'] : null;
  }

  function processData(data, dataSerializer, cb) {
    var currentDataType = jsUtil.getTypeOf(data);
    var allowedDataTypes = getAllowedDataTypes(dataSerializer);
    var allowedInstanceTypes = getAllowedInstanceTypes(dataSerializer);

    if (allowedInstanceTypes) {
      var isCorrectInstanceType = false;

      allowedInstanceTypes.forEach(function(type) {
        if ((global[type] && data instanceof global[type]) || (ponyfills[type] && data instanceof ponyfills[type])) {
          isCorrectInstanceType = true;
        }
      });

      if (!isCorrectInstanceType) {
        throw new Error(messages.INSTANCE_TYPE_MISMATCH_DATA + ' ' + allowedInstanceTypes.join(', '));
      }
    }

    if (!allowedInstanceTypes && allowedDataTypes.indexOf(currentDataType) === -1) {
      throw new Error(messages.TYPE_MISMATCH_DATA + ' ' + allowedDataTypes.join(', '));
    }

    switch (dataSerializer) {
      case 'utf8':
        return cb({ text: data });
      case 'raw':
        return cb(currentDataType === 'Uint8Array' ? data.buffer : data);
      case 'multipart':
        return processFormData(data, cb);
      default:
        return cb(data);
    }
  }

  function processFormData(data, cb) {
    dependencyValidator.checkBlobApi();
    dependencyValidator.checkFileReaderApi();
    dependencyValidator.checkTextEncoderApi();
    dependencyValidator.checkFormDataInstance(data);

    var textEncoder = new global.TextEncoder('utf8');
    var iterator = data.entries();

    var result = {
      buffers: [],
      names: [],
      fileNames: [],
      types: []
    };

    processFormDataIterator(iterator, textEncoder, result, cb);
  }

  function processFormDataIterator(iterator, textEncoder, result, onFinished) {
    var entry = iterator.next();

    if (entry.done) {
      return onFinished(result);
    }

    if (entry.value[1] instanceof global.Blob || entry.value[1] instanceof global.File) {
      var reader = new global.FileReader();

      reader.onload = function() {
        result.buffers.push(base64.fromArrayBuffer(reader.result));
        result.names.push(entry.value[0]);
        result.fileNames.push(entry.value[1].name || 'blob');
        result.types.push(entry.value[1].type || '');
        processFormDataIterator(iterator, textEncoder, result, onFinished);
      };

      return reader.readAsArrayBuffer(entry.value[1]);
    }

    if (jsUtil.getTypeOf(entry.value[1]) === 'String') {
      result.buffers.push(base64.fromArrayBuffer(textEncoder.encode(entry.value[1]).buffer));
      result.names.push(entry.value[0]);
      result.fileNames.push(null);
      result.types.push('text/plain');

      return processFormDataIterator(iterator, textEncoder, result, onFinished)
    }

    // skip items which are not supported
    processFormDataIterator(iterator, textEncoder, result, onFinished);
  }

  function handleMissingCallbacks(successFn, failFn) {
    if (jsUtil.getTypeOf(successFn) !== 'Function') {
      throw new Error(messages.MANDATORY_SUCCESS);
    }

    if (jsUtil.getTypeOf(failFn) !== 'Function') {
      throw new Error(messages.MANDATORY_FAIL);
    }
  }

  function handleMissingOptions(options, globals) {
    options = options || {};

    return {
      data: jsUtil.getTypeOf(options.data) === 'Undefined' ? null : options.data,
      filePath: options.filePath,
      followRedirect: checkFollowRedirectValue(options.followRedirect || globals.followRedirect),
      headers: checkHeadersObject(options.headers || {}),
      method: checkHttpMethod(options.method || validHttpMethods[0]),
      name: options.name,
      params: checkParamsObject(options.params || {}),
      responseType: checkResponseType(options.responseType || validResponseTypes[0]),
      serializer: checkSerializer(options.serializer || globals.serializer),
      timeout: checkTimeoutValue(options.timeout || globals.timeout),
    };
  }
};
