"use strict";
const fs = require("fs");
const { fileURLToPath } = require("url");
const { parseURL } = require("whatwg-url");
const dataURLFromRecord = require("data-urls").fromURLRecord;
const packageVersion = require("../../../../package.json").version;
const agentFactory = require("../../living/helpers/agent-factory");
const Request = require("../../living/helpers/http-request");

const IS_BROWSER = Object.prototype.toString.call(process) !== "[object process]";

module.exports = class ResourceLoader {
  constructor({
    strictSSL = true,
    proxy = undefined,
    userAgent = `Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 ` +
                `(KHTML, like Gecko) jsdom/${packageVersion}`
  } = {}) {
    this._strictSSL = strictSSL;
    this._proxy = proxy;
    this._userAgent = userAgent;
  }

  _readDataURL(urlRecord) {
    const dataURL = dataURLFromRecord(urlRecord);
    let timeoutId;
    const promise = new Promise(resolve => {
      timeoutId = setTimeout(resolve, 0, Buffer.from(dataURL.body));
    });
    promise.abort = () => {
      if (timeoutId !== undefined) {
        clearTimeout(timeoutId);
      }
    };
    return promise;
  }

  _readFile(filePath) {
    let readableStream, abort; // Native Promises doesn't have an "abort" method.

    // Creating a promise for two reason:
    //   1. fetch always return a promise.
    //   2. We need to add an abort handler.
    const promise = new Promise((resolve, reject) => {
      readableStream = fs.createReadStream(filePath);
      let data = Buffer.alloc(0);

      abort = reject;

      readableStream.on("error", reject);

      readableStream.on("data", chunk => {
        data = Buffer.concat([data, chunk]);
      });

      readableStream.on("end", () => {
        resolve(data);
      });
    });

    promise.abort = () => {
      readableStream.destroy();
      const error = new Error("request canceled by user");
      error.isAbortError = true;
      abort(error);
    };

    return promise;
  }

  fetch(urlString, { accept, cookieJar, referrer } = {}) {
    const url = parseURL(urlString);

    if (!url) {
      return Promise.reject(new Error(`Tried to fetch invalid URL ${urlString}`));
    }

    switch (url.scheme) {
      case "data": {
        return this._readDataURL(url);
      }

      case "http":
      case "https": {
        const agents = agentFactory(this._proxy, this._strictSSL);
        const headers = {
          "User-Agent": this._userAgent,
          "Accept-Language": "en",
          "Accept-Encoding": "gzip",
          "Accept": accept || "*/*"
        };
        if (referrer && !IS_BROWSER) {
          headers.Referer = referrer;
        }
        const requestClient = new Request(
          urlString,
          { followRedirects: true, cookieJar, agents },
          { headers }
        );
        const promise = new Promise((resolve, reject) => {
          const accumulated = [];
          requestClient.once("response", res => {
            promise.response = res;
            const { statusCode } = res;
            // TODO This deviates from the spec when it comes to
            // loading resources such as images
            if (statusCode < 200 || statusCode > 299) {
              requestClient.abort();
              reject(new Error(`Resource was not loaded. Status: ${statusCode}`));
            }
          });
          requestClient.on("data", chunk => {
            accumulated.push(chunk);
          });
          requestClient.on("end", () => resolve(Buffer.concat(accumulated)));
          requestClient.on("error", reject);
        });
        // The method fromURL in lib/api.js crashes without the following four
        // properties defined on the Promise instance, causing the test suite to halt
        requestClient.on("end", () => {
          promise.href = requestClient.currentURL;
        });
        promise.abort = requestClient.abort.bind(requestClient);
        promise.getHeader = name => headers[name] || requestClient.getHeader(name);
        requestClient.end();
        return promise;
      }

      case "file": {
        try {
          return this._readFile(fileURLToPath(urlString));
        } catch (e) {
          return Promise.reject(e);
        }
      }

      default: {
        return Promise.reject(new Error(`Tried to fetch URL ${urlString} with invalid scheme ${url.scheme}`));
      }
    }
  }
};
