/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 *
 */
"use strict";

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

const HttpError = require("./HttpError");

const NetworkError = require("./NetworkError");

const http = require("http");

const https = require("https");

const url = require("url");

const zlib = require("zlib");

const ZLIB_OPTIONS = {
  level: 9
};
const NULL_BYTE = 0x00;
const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE]);

class HttpStore {
  constructor(options) {
    const uri = url.parse(options.endpoint);
    const module = uri.protocol === "http:" ? http : https;
    const agentConfig = {
      family: options.family,
      keepAlive: true,
      keepAliveMsecs: options.timeout || 5000,
      maxSockets: 64,
      maxFreeSockets: 64
    };

    if (!uri.hostname || !uri.pathname) {
      throw new TypeError("Invalid endpoint: " + options.endpoint);
    }

    this._module = module;
    this._timeout = options.timeout || 5000;
    this._host = uri.hostname;
    this._path = uri.pathname;
    this._port = +uri.port;
    this._getAgent = new module.Agent(agentConfig);
    this._setAgent = new module.Agent(agentConfig);
  }

  get(key) {
    return new Promise((resolve, reject) => {
      const options = {
        agent: this._getAgent,
        host: this._host,
        method: "GET",
        path: this._path + "/" + key.toString("hex"),
        port: this._port,
        timeout: this._timeout
      };

      const req = this._module.request(options, res => {
        const code = res.statusCode;
        const data = [];

        if (code === 404) {
          res.resume();
          resolve(null);
          return;
        } else if (code !== 200) {
          res.resume();
          reject(new HttpError("HTTP error: " + code, code));
          return;
        }

        const gunzipped = res.pipe(zlib.createGunzip());
        gunzipped.on("data", chunk => {
          data.push(chunk);
        });
        gunzipped.on("error", err => {
          reject(err);
        });
        gunzipped.on("end", () => {
          try {
            const buffer = Buffer.concat(data);

            if (buffer.length > 0 && buffer[0] === NULL_BYTE) {
              resolve(buffer.slice(1));
            } else {
              resolve(JSON.parse(buffer.toString("utf8")));
            }
          } catch (err) {
            reject(err);
          }
        });
        res.on("error", err => gunzipped.emit("error", err));
      });

      req.on("error", err => {
        reject(new NetworkError(err.message, err.code));
      });
      req.end();
    });
  }

  set(key, value) {
    return new Promise((resolve, reject) => {
      const gzip = zlib.createGzip(ZLIB_OPTIONS);
      const options = {
        agent: this._setAgent,
        host: this._host,
        method: "PUT",
        path: this._path + "/" + key.toString("hex"),
        port: this._port,
        timeout: this._timeout
      };

      const req = this._module.request(options, res => {
        res.on("error", err => {
          reject(err);
        });
        res.on("end", () => {
          resolve();
        }); // Consume all the data from the response without processing it.

        res.resume();
      });

      gzip.pipe(req);

      if (value instanceof Buffer) {
        gzip.write(NULL_BYTE_BUFFER);
        gzip.end(value);
      } else {
        gzip.end(JSON.stringify(value) || "null");
      }
    });
  }

  clear() {
    // Not implemented.
  }
}

_defineProperty(HttpStore, "HttpError", HttpError);

_defineProperty(HttpStore, "NetworkError", NetworkError);

module.exports = HttpStore;