text: // @flow
  /* eslint-disable no-for-in */
  /* eslint-disable no-params-reassign */
  /* eslint-disable new-cap */
  
  import invariant from "invariant";
  import { log } from "@ledgerhq/logs";
  import { NotEnoughBalance } from "@ledgerhq/errors";
  import { deserializeError, serializeError } from "@ledgerhq/errors";
  import { reflect } from "../types";
  import type { Core, CoreStatics } from "../types";
  import { setLoadCoreImplementation } from "../access";
  import { setRemapLibcoreErrorsImplementation } from "../errors";
  import { getEnv } from "../../env";
  import network from "../../network";
  
  const crypto = require("crypto");
  const path = require("path");
  const fs = require("fs");
  
  const prefixHex0x = (str) => (str.startsWith("0x") ? str : "0x" + str);
  
  const unprefixHex0x = (str) => (str.startsWith("0x") ? str.slice(2) : str);
  
  const hexToBytes = (str) => Array.from(Buffer.from(unprefixHex0x(str), "hex"));
  
  const bytesToHex = (buf) => Buffer.from(buf).toString("hex");
  
  const bytesArrayToString = (bytesArray = []) =>
    Buffer.from(bytesArray).toString();
  
  const stringToBytesArray = (str) => Array.from(Buffer.from(str));
  
  export default (arg: {
    // the actual @ledgerhq/ledger-core lib or a function that returns it
    lib: any,
    dbPath: string,
    dbPassword?: string,
  }) => {
    let lib;
    const lazyLoad = () => {
      if (lib) return;
      if (typeof arg.lib === "function") {
        lib = arg.lib();
      } else {
        lib = arg.lib;
      }
    };
    const { dbPath } = arg;
    const dbPassword =
      typeof arg.dbPassword === "undefined"
        ? getEnv("LIBCORE_PASSWORD")
        : arg.dbPassword;
  
    const loadCore = (): Promise<Core> => {
      lazyLoad();
  
      // feature detect if the bindings uses hex or array bytes
      const isUsingArrayOfBytes =
        "object" === typeof new lib.NJSDynamicArray().serialize();
  
      log("libcore", "using array of bytes = " + String(isUsingArrayOfBytes));
  
      const wrappers = {
        hex: isUsingArrayOfBytes ? hexToBytes : prefixHex0x,
      };
      const unwrappers = {
        hex: isUsingArrayOfBytes ? bytesToHex : unprefixHex0x,
      };
  
      const MAX_RANDOM = 2684869021;
  
      const lcore = new lib.NJSLedgerCore();
      const stringVersion = lcore.getStringVersion();
      const sqlitePrefix = `v${stringVersion.split(".")[0]}`;
  
      const NJSExecutionContextImpl = {
        execute: (runnable) => {
          try {
            const runFunction = () => runnable.run();
            setImmediate(runFunction);
          } catch (e) {
            log("libcore-Error", String(e));
          }
        },
        delay: (runnable, ms) => setTimeout(() => runnable.run(), ms),
      };
  
      const ThreadContexts = {};
  
      const getSerialExecutionContext = (name) => {
        let currentContext = ThreadContexts[name];
        if (!currentContext) {
          currentContext = new lib.NJSExecutionContext(NJSExecutionContextImpl);
          ThreadContexts[name] = currentContext;
        }
        return currentContext;
      };
  
      const getMainExecutionContext = () => getSerialExecutionContext("main");
  
      const NJSThreadDispatcher = new lib.NJSThreadDispatcher({
        contexts: ThreadContexts,
        getThreadPoolExecutionContext: (name) => getSerialExecutionContext(name),
        getMainExecutionContext,
        getSerialExecutionContext,
        newLock: () => {
          log(
            "libcore-Warn",
            "libcore NJSThreadDispatcher: newLock: Not implemented"
          );
        },
      });
  
      NJSThreadDispatcher.getMainExecutionContext = getMainExecutionContext;
  
      function createHttpConnection(res, libcoreError) {
        if (!res) {
          return null;
        }
        const headersMap = new Map();
        Object.keys(res.headers).forEach((key) => {
          if (typeof res.headers[key] === "string") {
            headersMap.set(key, res.headers[key]);
          }
        });
        const NJSHttpUrlConnectionImpl = {
          getStatusCode: () => Number(res.status),
          getStatusText: () => res.statusText,
          getHeaders: () => headersMap,
          readBody: () => ({
            error: libcoreError,
            data: isUsingArrayOfBytes ? stringToBytesArray(res.data) : res.data,
          }),
        };
        return new lib.NJSHttpUrlConnection(NJSHttpUrlConnectionImpl);
      }
  
      const NJSHttpClient = new lib.NJSHttpClient({
        execute: async (r) => {
          const method = r.getMethod();
          const headersMap = r.getHeaders();
          const url = r.getUrl();
          let data = r.getBody();
          const headers = {};
          headersMap.forEach((v, k) => {
            headers[k] = v;
          });
          let res;
          const param: Object = {
            method: lib.METHODS[method],
            url,
            headers,
            validateStatus: (status) =>
              // FIXME in future, everything should passthrough libcore
              // for now as we need to have the server error we will only pass-in 2xx and 404
              (status >= 200 && status < 300) || status === 404,
            // the default would parse the request, we want to preserve the string
            transformResponse: (data) => data,
          };
          if (isUsingArrayOfBytes) {
            if (Array.isArray(data)) {
              if (data.length === 0) {
                data = null;
              } else {
                // we transform back to a string
                data = bytesArrayToString(data);
              }
            }
          } else if (
            headers["Content-Type"] &&
            headers["Content-Type"] === "application/x-binary"
          ) {
            data = Buffer.from(unprefixHex0x(data), "hex");
          } else {
            if (typeof data === "string" && data) {
              data = Buffer.from(unprefixHex0x(data), "hex").toString();
            }
          }
  
          if (data) {
            param.data = data;
            if (!headers["Content-Type"]) {
              headers["Content-Type"] = "application/json";
            }
          }
          try {
            // $FlowFixMe not sure what's wrong
            res = await network(param);
            const urlConnection = createHttpConnection(res, null);
            r.complete(urlConnection, null);
          } catch (err) {
            const libcoreError = {
              code: lib.ERROR_CODE.HTTP_ERROR,
              message: JSON.stringify(
                serializeError({
                  message: err.message,
                  name: err.name,
                })
              ),
              stack: err.stack,
            };
            const urlConnection = createHttpConnection(res, libcoreError);
            r.complete(urlConnection, libcoreError);
          }
        },
      });
  
      const NJSWebSocketClient = new lib.NJSWebSocketClient({
        connect: (url, connection) => {
          connection.OnConnect();
        },
        send: (connection, data) => {
          connection.OnMessage(data);
        },
        disconnect: (connection) => {
          connection.OnClose();
        },
      });
  
      const NJSLogPrinter = new lib.NJSLogPrinter({
        context: {},
        printError: (message) => log("libcore-Error", message),
        printInfo: (message) => log("libcore-Info", message),
        printDebug: (message) => log("libcore-Debug", message),
        printWarning: (message) => log("libcore-Warning", message),
        printApdu: (message) => log("libcore-Apdu", message),
        printCriticalError: (message) => log("libcore-CriticalError", message),
        getContext: () => new lib.NJSExecutionContext(NJSExecutionContextImpl),
      });
  
      const NJSRandomNumberGenerator = new lib.NJSRandomNumberGenerator({
        getRandomBytes: isUsingArrayOfBytes
          ? (size) => Array.from(Buffer.from(crypto.randomBytes(size), "hex"))
          : (size) => "0x" + crypto.randomBytes(size).toString("hex"),
        getRandomInt: () => Math.random() * MAX_RANDOM,
        getRandomLong: () => Math.random() * MAX_RANDOM * MAX_RANDOM,
      });
  
      const NJSDatabaseBackend = new lib.NJSDatabaseBackend();
      const config = new lib.NJSDynamicObject();
  
      // We handle logs ourself with the logger
      // still overridable by env
      config.putBoolean(
        "ENABLE_INTERNAL_LOGGING",
        !!process.env.LIBCORE_ENABLE_INTERNAL_LOGGING
      );
  
      let walletPoolInstance = null;
  
      const instanciateWalletPool = () => {
        try {
          fs.mkdirSync(dbPath);
        } catch (err) {
          if (err.code !== "EEXIST") {
            throw err;
          }
        }
  
        const NJSPathResolver = new lib.NJSPathResolver({
          resolveLogFilePath: (pathToResolve) => {
            const hash = pathToResolve.replace(/\//g, "__");
            return path.resolve(dbPath, `./log_file_${sqlitePrefix}_${hash}`);
          },
          resolvePreferencesPath: (pathToResolve) => {
            const hash = pathToResolve.replace(/\//g, "__");
            return path.resolve(dbPath, `./preferences_${sqlitePrefix}_${hash}`);
          },
          resolveDatabasePath: (pathToResolve) => {
            const hash = pathToResolve.replace(/\//g, "__");
            return path.resolve(dbPath, `./database_${sqlitePrefix}_${hash}`);
          },
        });
  
        walletPoolInstance = new lib.NJSWalletPool(
          "ledgerlive",
          dbPassword,
          NJSHttpClient,
          NJSWebSocketClient,
          NJSPathResolver,
          NJSLogPrinter,
          NJSThreadDispatcher,
          NJSRandomNumberGenerator,
          NJSDatabaseBackend,
          config
        );
  
        return walletPoolInstance;
      };
  
      const getPoolInstance = () => {
        if (!walletPoolInstance) {
          instanciateWalletPool();
        }
        invariant(walletPoolInstance, "can't initialize walletPoolInstance");
        return walletPoolInstance;
      };
  
      const mappings = {};
      Object.keys(lib).forEach((k) => {
        if (k.startsWith("NJS")) {
          mappings[k.slice(3)] = lib[k];
        }
      });
  
      function wrapResult(id, value) {
        if (!value || !id) return value;
        if (Array.isArray(id)) {
          const [actualId] = id;
          return value.map((a) => wrapResult(actualId, a));
        }
        if (id in unwrappers) {
          return unwrappers[id](value);
        }
        const Clz = mappings[id];
        if (!Clz) {
          return value;
        }
        if (value instanceof Clz) return value;
        return new Clz(value);
      }
  
      function unwrapArg(id, value) {
        if (!value || !id) return value;
        if (Array.isArray(id)) {
          const [actualId] = id;
          return value.map((v) => unwrapArg(actualId, v));
        }
        if (id in wrappers) {
          return wrappers[id](value);
        }
        return value;
      }
  
      reflect((id, spec) => {
        const { methods, statics } = spec;
        let m;
        if (spec.njsUsesPlainObject) {
          // In that case we need to create a class and abstract out the methods
          m = function constructor(data) {
            Object.assign(this, data);
          };
          mappings[id] = m;
        } else {
          m = mappings[id];
          if (!m) {
            return;
          }
        }
  
        if (statics) {
          Object.keys(statics).forEach((method) => {
            const {
              njsInstanciateClass,
              njsBuggyMethodIsNotStatic,
              params,
              returns,
            } = statics[method];
            if (njsInstanciateClass) {
              m[method] = function met(...vargs) {
                if (process.env.VERBOSE) {
                  log("libcore-call", id + "." + method, vargs);
                }
                const args = njsInstanciateClass.map((arg) => {
                  if (typeof arg === "object") {
                    const o = {};
                    for (const k in arg) {
                      const index = arg[k];
                      o[k] = unwrapArg(params && params[index], vargs[index]);
                    }
                    return o;
                  }
                  return arg;
                });
                const value = new m(...args);
                if (process.env.VERBOSE) {
                  log("libcore-result", id + "." + method, { value });
                }
                return value;
              };
            } else if (njsBuggyMethodIsNotStatic) {
              // There is a bug in the node bindings that don't expose the static functions
              m[method] = (...args) => {
                if (process.env.VERBOSE) {
                  log("libcore-call", id + "." + method, args);
                }
                let value;
                if (params) {
                  // it's seems statics method until now doesn't need to be unwrap
                  const hexArgs = unwrapArg(params, args);
                  value = new m(...hexArgs)[method](...hexArgs);
                } else {
                  const constructorArgs =
                    typeof njsBuggyMethodIsNotStatic === "function"
                      ? njsBuggyMethodIsNotStatic(args)
                      : args;
                  value = new m(...constructorArgs)[method](...args);
                }
                if (process.env.VERBOSE) {
                  log("libcore-result", id + "." + method, { value });
                }
                if (returns) {
                  return wrapResult(returns, value);
                }
                return value;
              };
            }
          });
        }
  
        if (methods) {
          Object.keys(methods).forEach((method) => {
            const { njsField, params, returns, nodejsNotAvailable } = methods[
              method
            ];
            if (nodejsNotAvailable) return;
            if (njsField) {
              m.prototype[method] = function met() {
                if (process.env.VERBOSE) {
                  log("libcore-call", id + "#" + method);
                }
                const value = this[njsField];
                if (process.env.VERBOSE) {
                  log("libcore-result", id + "#" + method, { value });
                }
                const Cls =
                  typeof returns === "string" && returns in mappings
                    ? mappings[returns]
                    : null;
                if (Cls && !(value instanceof Cls)) {
                  const inst = new Cls(value);
                  return inst;
                }
                return wrapResult(returns, value);
              };
            } else {
              const f = m.prototype[method];
              if (!f) {
                console.warn(`no such method '${method}' in ${id}`);
                return;
              }
              m.prototype[method] = async function met(...a) {
                if (process.env.VERBOSE) {
                  log("libcore-call", id + "#" + method, a);
                }
                const args = params
                  ? a.map((value, i) => unwrapArg(params[i], value))
                  : a;
                const value = await f.apply(this, args);
                if (process.env.VERBOSE) {
                  log("libcore-result", id + "#" + method, { value });
                }
                return wrapResult(returns, value);
              };
            }
          });
        }
      });
  
      // other NodeJS bindings specific code
  
      const eventBusSubscribe = mappings.EventBus.prototype.subscribe;
      mappings.EventBus.prototype.subscribe = function subscribe(
        executionContext,
        receiver_
      ) {
        const receiver = receiver_;
        return new Promise((resolve, reject) => {
          receiver._resolve = resolve;
          receiver._reject = reject;
          eventBusSubscribe.call(this, executionContext, receiver);
        });
      };
  
      mappings.EventReceiver.newInstance = () => {
        const receiver = new mappings.EventReceiver({
          onEvent: (e) => {
            const code = e.getCode();
            if (
              code === lib.EVENT_CODE.UNDEFINED ||
              code === lib.EVENT_CODE.SYNCHRONIZATION_FAILED
            ) {
              const payload = e.getPayload();
              const message = (
                (payload && payload.getString("EV_SYNC_ERROR_MESSAGE")) ||
                "Sync failed"
              ).replace(" (EC_PRIV_KEY_INVALID_FORMAT)", "");
              try {
                receiver._reject(deserializeError(JSON.parse(message)));
              } catch (_e) {
                receiver._reject(message);
              }
              return;
            }
            if (
              code === lib.EVENT_CODE.SYNCHRONIZATION_SUCCEED ||
              code ===
                lib.EVENT_CODE.SYNCHRONIZATION_SUCCEED_ON_PREVIOUSLY_EMPTY_ACCOUNT
            ) {
              receiver._resolve();
            }
          },
        });
        return receiver;
      };
  
      // $FlowFixMe
      const cs: CoreStatics = mappings;
      // $FlowFixMe
      const core: Core = {
        ...cs,
        flush: () => Promise.resolve(),
        getPoolInstance,
        getThreadDispatcher: () => NJSThreadDispatcher,
      };
  
      return Promise.resolve(core);
    };
  
    function parseError(error: string): Error {
      const m = error.match(/[^{]*({.*}).*/);
      if (m) {
        const json = JSON.parse(m[1]);
        if (json.name) {
          return deserializeError(json);
        }
      }
  
      return new Error(String(error));
    }
  
    const remapLibcoreErrors = (e: mixed): Error => {
      lazyLoad();
      if (typeof e === "string") {
        try {
          return parseError(e);
        } catch (e2) {
          return e2;
        }
      }
  
      if (e && typeof e === "object") {
        if (
          typeof e.code === "number" &&
          e.code === lib.ERROR_CODE.NOT_ENOUGH_FUNDS
        ) {
          return new NotEnoughBalance();
        } else {
          // re-deserialize error if it was a serialized error
          if (e.message === "string") {
            try {
              return parseError(e.message);
            } catch (_e2) {}
          }
        }
      }
  
      if (e instanceof Error) {
        return e;
      }
  
      if (e && typeof e.message === "string") {
        return new Error(e.message);
      }
  
      return new Error(String(e));
    };
  
    setLoadCoreImplementation(loadCore);
    setRemapLibcoreErrorsImplementation(remapLibcoreErrors);
  };
  