import { UPLOAD, DOWNLOAD, RPC } from './constants';
import { downloadRequest } from './download-request';
import { uploadRequest } from './upload-request';
import { rpcRequest } from './rpc-request';

/* eslint-disable */
// Polyfill object.assign for legacy browsers
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof Object.assign !== 'function') {
  (function () {
    Object.assign = function (target) {
      'use strict';
      var output;
      var index;
      var source;
      var nextKey;
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert undefined or null to object');
      }

      output = Object(target);
      for (index = 1; index < arguments.length; index++) {
        source = arguments[index];
        if (source !== undefined && source !== null) {
          for (nextKey in source) {
            if (source.hasOwnProperty(nextKey)) {
              output[nextKey] = source[nextKey];
            }
          }
        }
      }
      return output;
    };
  }());
}

// Polyfill Array.includes for legacy browsers
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
  Object.defineProperty(Array.prototype, 'includes', {
    value: function(searchElement, fromIndex) {

      if (this == null) {
        throw new TypeError('"this" is null or not defined');
      }

      // 1. Let O be ? ToObject(this value).
      var o = Object(this);

      // 2. Let len be ? ToLength(? Get(O, "length")).
      var len = o.length >>> 0;

      // 3. If len is 0, return false.
      if (len === 0) {
        return false;
      }

      // 4. Let n be ? ToInteger(fromIndex).
      //    (If fromIndex is undefined, this step produces the value 0.)
      var n = fromIndex | 0;

      // 5. If n ≥ 0, then
      //  a. Let k be n.
      // 6. Else n < 0,
      //  a. Let k be len + n.
      //  b. If k < 0, let k be 0.
      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);

      function sameValueZero(x, y) {
        return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
      }

      // 7. Repeat, while k < len
      while (k < len) {
        // a. Let elementK be the result of ? Get(O, ! ToString(k)).
        // b. If SameValueZero(searchElement, elementK) is true, return true.
        if (sameValueZero(o[k], searchElement)) {
          return true;
        }
        // c. Increase k by 1.
        k++;
      }

      // 8. Return false
      return false;
    }
  });
}
/* eslint-enable */

/**
 * @private
 * @class DropboxBase
 * @classdesc The main Dropbox SDK class. This contains the methods that are
 * shared between Dropbox and DropboxTeam classes. It is marked as private so
 * that it doesn't show up in the docs because it is never used directly.
 * @arg {Object} options
 * @arg {Function} [options.fetch] - fetch library for making requests.
 * @arg {String} [options.accessToken] - An access token for making authenticated
 * requests.
 * @arg {String} [options.clientId] - The client id for your app. Used to create
 * authentication URL.
 * @arg {String} [options.clientSecret] - The client secret for your app.
 * @arg {Number} [options.selectUser] - User that the team access token would like
 * to act as.
 * @arg {String} [options.selectAdmin] - Team admin that the team access token would like
 * to act as.
 * @arg {String} [options.pathRoot] - root pass to access other namespaces
 * Use to access team folders for example
 */

function parseBodyToType(res) {
  const clone = res.clone();
  return new Promise((resolve) => {
    res.json()
      .then(data => resolve(data))
      .catch(() => clone.text().then(data => resolve(data)));
  }).then(data => [res, data]);
}

export class DropboxBase {
  constructor(options) {
    options = options || {};
    this.accessToken = options.accessToken;
    this.clientId = options.clientId;
    this.clientSecret = options.clientSecret;
    this.selectUser = options.selectUser;
    this.selectAdmin = options.selectAdmin;
    this.fetch = options.fetch || fetch;
    this.pathRoot = options.pathRoot;
    if (!options.fetch) { console.warn('Global fetch is deprecated and will be unsupported in a future version. Please pass fetch function as option when instantiating dropbox instance: new Dropbox({fetch})'); } // eslint-disable-line no-console
  }

  /**
   * Set the access token used to authenticate requests to the API.
   * @arg {String} accessToken - An access token
   * @returns {undefined}
   */
  setAccessToken(accessToken) {
    this.accessToken = accessToken;
  }

  /**
   * Get the access token
   * @returns {String} Access token
   */
  getAccessToken() {
    return this.accessToken;
  }

  /**
   * Set the client id, which is used to help gain an access token.
   * @arg {String} clientId - Your apps client id
   * @returns {undefined}
   */
  setClientId(clientId) {
    this.clientId = clientId;
  }

  /**
   * Get the client id
   * @returns {String} Client id
   */
  getClientId() {
    return this.clientId;
  }

  /**
   * Set the client secret
   * @arg {String} clientSecret - Your app's client secret
   * @returns {undefined}
   */
  setClientSecret(clientSecret) {
    this.clientSecret = clientSecret;
  }

  /**
   * Get the client secret
   * @returns {String} Client secret
   */
  getClientSecret() {
    return this.clientSecret;
  }

  /**
   * Get a URL that can be used to authenticate users for the Dropbox API.
   * @arg {String} redirectUri - A URL to redirect the user to after
   * authenticating. This must be added to your app through the admin interface.
   * @arg {String} [state] - State that will be returned in the redirect URL to help
   * prevent cross site scripting attacks.
   * @arg {String} [authType] - auth type, defaults to 'token', other option is 'code'
   * @returns {String} Url to send user to for Dropbox API authentication
   */
  getAuthenticationUrl(redirectUri, state, authType = 'token') {
    const clientId = this.getClientId();
    const baseUrl = 'https://www.dropbox.com/oauth2/authorize';

    if (!clientId) {
      throw new Error('A client id is required. You can set the client id using .setClientId().');
    }
    if (authType !== 'code' && !redirectUri) {
      throw new Error('A redirect uri is required.');
    }
    if (!['code', 'token'].includes(authType)) {
      throw new Error('Authorization type must be code or token');
    }

    let authUrl;
    if (authType === 'code') {
      authUrl = `${baseUrl}?response_type=code&client_id=${clientId}`;
    } else {
      authUrl = `${baseUrl}?response_type=token&client_id=${clientId}`;
    }

    if (redirectUri) {
      authUrl += `&redirect_uri=${redirectUri}`;
    }
    if (state) {
      authUrl += `&state=${state}`;
    }
    return authUrl;
  }

  /**
   * Get an OAuth2 access token from an OAuth2 Code.
   * @arg {String} redirectUri - A URL to redirect the user to after
   * authenticating. This must be added to your app through the admin interface.
   * @arg {String} code - An OAuth2 code.
  */
  getAccessTokenFromCode(redirectUri, code) {
    const clientId = this.getClientId();
    const clientSecret = this.getClientSecret();

    if (!clientId) {
      throw new Error('A client id is required. You can set the client id using .setClientId().');
    }
    if (!clientSecret) {
      throw new Error('A client secret is required. You can set the client id using .setClientSecret().');
    }
    const path = `https://api.dropboxapi.com/oauth2/token?code=${code}&\
grant_type=authorization_code&redirect_uri=${redirectUri}&\
client_id=${clientId}&client_secret=${clientSecret}`;

    const fetchOptions = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    };

    return this.fetch(path, fetchOptions)
      .then(res => parseBodyToType(res))
      .then(([res, data]) => {
        // maintaining existing API for error codes not equal to 200 range
        if (!res.ok) {
          // eslint-disable-next-line no-throw-literal
          throw {
            error: data,
            response: res,
            status: res.status,
          };
        }
        return data.access_token;
      });
  }

  /**
   * Called when the authentication succeed
   * @callback successCallback
   * @param {string} access_token The application's access token
   */

  /**
   * Called when the authentication failed.
   * @callback errorCallback
   */


  /**
   * An authentication process that works with cordova applications.
   * @param {successCallback} successCallback
   * @param {errorCallback} errorCallback
   */
  authenticateWithCordova(successCallback, errorCallback) {
    const redirectUrl = 'https://www.dropbox.com/1/oauth2/redirect_receiver';
    const url = this.getAuthenticationUrl(redirectUrl);

    let removed = false;
    const browser = window.open(url, '_blank');

    function onLoadError(event) {
      if (event.code !== -999) { // Workaround to fix wrong behavior on cordova-plugin-inappbrowser
        // Try to avoid a browser crash on browser.close().
        window.setTimeout(() => { browser.close(); }, 10);
        errorCallback();
      }
    }

    function onLoadStop(event) {
      const errorLabel = '&error=';
      const errorIndex = event.url.indexOf(errorLabel);

      if (errorIndex > -1) {
        // Try to avoid a browser crash on browser.close().
        window.setTimeout(() => { browser.close(); }, 10);
        errorCallback();
      } else {
        const tokenLabel = '#access_token=';
        let tokenIndex = event.url.indexOf(tokenLabel);
        const tokenTypeIndex = event.url.indexOf('&token_type=');
        if (tokenIndex > -1) {
          tokenIndex += tokenLabel.length;
          // Try to avoid a browser crash on browser.close().
          window.setTimeout(() => { browser.close(); }, 10);

          const accessToken = event.url.substring(tokenIndex, tokenTypeIndex);
          successCallback(accessToken);
        }
      }
    }

    function onExit() {
      if (removed) {
        return;
      }
      browser.removeEventListener('loaderror', onLoadError);
      browser.removeEventListener('loadstop', onLoadStop);
      browser.removeEventListener('exit', onExit);
      removed = true;
    }

    browser.addEventListener('loaderror', onLoadError);
    browser.addEventListener('loadstop', onLoadStop);
    browser.addEventListener('exit', onExit);
  }

  request(path, args, auth, host, style) {
    let request = null;
    switch (style) {
      case RPC:
        request = this.getRpcRequest();
        break;
      case DOWNLOAD:
        request = this.getDownloadRequest();
        break;
      case UPLOAD:
        request = this.getUploadRequest();
        break;
      default:
        throw new Error(`Invalid request style: ${style}`);
    }
    const options = {
      selectUser: this.selectUser,
      selectAdmin: this.selectAdmin,
      clientId: this.getClientId(),
      clientSecret: this.getClientSecret(),
      pathRoot: this.pathRoot,
    };
    return request(path, args, auth, host, this.getAccessToken(), options);
  }

  setRpcRequest(newRpcRequest) {
    this.rpcRequest = newRpcRequest;
  }

  getRpcRequest() {
    if (this.rpcRequest === undefined) {
      this.rpcRequest = rpcRequest(this.fetch);
    }
    return this.rpcRequest;
  }

  setDownloadRequest(newDownloadRequest) {
    this.downloadRequest = newDownloadRequest;
  }

  getDownloadRequest() {
    if (this.downloadRequest === undefined) {
      this.downloadRequest = downloadRequest(this.fetch);
    }
    return this.downloadRequest;
  }

  setUploadRequest(newUploadRequest) {
    this.uploadRequest = newUploadRequest;
  }

  getUploadRequest() {
    if (this.uploadRequest === undefined) {
      this.uploadRequest = uploadRequest(this.fetch);
    }
    return this.uploadRequest;
  }

}
