text: import { Daemon } from "./daemon";
  import { WalletRPC } from "./wallet-rpc";
  import { SCEE } from "./SCEE-Node";
  import { dialog } from "electron";
  import semver from "semver";
  import axios from "axios";
  import { version } from "../../../package.json";
  const bunyan = require("bunyan");
  
  const WebSocket = require("ws");
  const electron = require("electron");
  const os = require("os");
  const fs = require("fs-extra");
  const path = require("upath");
  const objectAssignDeep = require("object-assign-deep");
  
  const { ipcMain: ipc } = electron;
  
  const LOG_LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"];
  
  export class Backend {
    constructor(mainWindow) {
      this.mainWindow = mainWindow;
      this.daemon = null;
      this.walletd = null;
      this.wss = null;
      this.token = null;
      this.config_dir = null;
      this.wallet_dir = null;
      this.config_file = null;
      this.config_data = {};
      this.scee = new SCEE();
      this.log = null;
    }
  
    init(config) {
      let configDir;
      let legacyLokiConfigDir;
      if (os.platform() === "win32") {
        configDir = "C:\\ProgramData\\oxen";
        legacyLokiConfigDir = "C:\\ProgramData\\loki\\";
        this.wallet_dir = `${os.homedir()}\\Documents\\Oxen`;
      } else {
        configDir = path.join(os.homedir(), ".oxen");
        legacyLokiConfigDir = path.join(os.homedir(), ".loki/");
        this.wallet_dir = path.join(os.homedir(), "Oxen");
      }
  
      // if the user has used loki before, just keep the same stuff
      if (fs.existsSync(legacyLokiConfigDir)) {
        this.config_dir = legacyLokiConfigDir;
      } else {
        // create the new, Oxen location
        this.config_dir = configDir;
        if (!fs.existsSync(configDir)) {
          fs.mkdirpSync(configDir);
        }
      }
  
      if (!fs.existsSync(path.join(this.config_dir, "gui"))) {
        fs.mkdirpSync(path.join(this.config_dir, "gui"));
      }
  
      this.config_file = path.join(this.config_dir, "gui", "config.json");
  
      const daemon = {
        type: "remote",
        p2p_bind_ip: "0.0.0.0",
        p2p_bind_port: 22022,
        rpc_bind_ip: "127.0.0.1",
        rpc_bind_port: 22023,
        zmq_rpc_bind_ip: "127.0.0.1",
        out_peers: -1,
        in_peers: -1,
        limit_rate_up: -1,
        limit_rate_down: -1,
        log_level: 0
      };
  
      const daemons = {
        mainnet: {
          ...daemon,
          remote_host: "imaginary.stream",
          remote_port: 22023
        },
        stagenet: {
          ...daemon,
          type: "local",
          p2p_bind_port: 38153,
          rpc_bind_port: 38154
        },
        testnet: {
          ...daemon,
          type: "local",
          p2p_bind_port: 38156,
          rpc_bind_port: 38157
        }
      };
  
      // Default values
      this.defaults = {
        daemons: objectAssignDeep({}, daemons),
        app: {
          data_dir: this.config_dir,
          wallet_data_dir: this.wallet_dir,
          ws_bind_port: 12313,
          net_type: "mainnet"
        },
        wallet: {
          rpc_bind_port: 22026,
          log_level: 0
        }
      };
  
      this.config_data = {
        // Copy all the properties of defaults
        ...objectAssignDeep({}, this.defaults),
        appearance: {
          theme: "dark"
        }
      };
  
      this.remotes = [
        {
          host: "imaginary.stream",
          port: "22023"
        },
        {
          host: "nodes.hashvault.pro",
          port: "22023"
        },
        {
          host: "explorer.loki.aussie-pools.com",
          port: "18081"
        },
        {
          host: "public.loki.foundation",
          port: "22023"
        }
      ];
  
      this.token = config.token;
  
      this.wss = new WebSocket.Server({
        port: config.port,
        maxPayload: Number.POSITIVE_INFINITY
      });
  
      this.wss.on("connection", ws => {
        ws.on("message", data => this.receive(data));
      });
    }
  
    send(event, data = {}) {
      let message = {
        event,
        data
      };
  
      let encrypted_data = this.scee.encryptString(
        JSON.stringify(message),
        this.token
      );
  
      this.wss.clients.forEach(function each(client) {
        if (client.readyState === WebSocket.OPEN) {
          client.send(encrypted_data);
        }
      });
    }
  
    receive(data) {
      let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token));
  
      // route incoming request to either the daemon, wallet, or here
      switch (decrypted_data.module) {
        case "core":
          this.handle(decrypted_data);
          break;
        case "daemon":
          if (this.daemon) {
            this.daemon.handle(decrypted_data);
          }
          break;
        case "wallet":
          if (this.walletd) {
            this.walletd.handle(decrypted_data);
          }
          break;
      }
    }
  
    handle(data) {
      let params = data.data;
  
      // check if config has changed
      let config_changed = false;
  
      switch (data.method) {
        case "set_language":
          this.send("set_language", { lang: params.lang });
          break;
        case "quick_save_config":
          // save only partial config settings
          Object.keys(params).map(key => {
            this.config_data[key] = Object.assign(
              this.config_data[key],
              params[key]
            );
          });
          fs.writeFile(
            this.config_file,
            JSON.stringify(this.config_data, null, 4),
            "utf8",
            () => {
              this.send("set_app_data", {
                config: params,
                pending_config: params
              });
            }
          );
          break;
        case "save_config_init":
        case "save_config": {
          if (data.method === "save_config") {
            Object.keys(this.config_data).map(i => {
              if (i == "appearance") return;
              Object.keys(this.config_data[i]).map(j => {
                if (this.config_data[i][j] !== params[i][j]) {
                  config_changed = true;
                }
              });
            });
          }
  
          Object.keys(params).map(key => {
            this.config_data[key] = Object.assign(
              this.config_data[key],
              params[key]
            );
          });
  
          const validated = Object.keys(this.defaults)
            .filter(k => k in this.config_data)
            .map(k => [
              k,
              this.validate_values(this.config_data[k], this.defaults[k])
            ])
            .reduce((map, obj) => {
              map[obj[0]] = obj[1];
              return map;
            }, {});
  
          // Validate daemon data
          this.config_data = {
            ...this.config_data,
            ...validated
          };
  
          fs.writeFile(
            this.config_file,
            JSON.stringify(this.config_data, null, 4),
            "utf8",
            () => {
              if (data.method == "save_config_init") {
                this.startup();
              } else {
                this.send("set_app_data", {
                  config: this.config_data,
                  pending_config: this.config_data
                });
                if (config_changed) {
                  this.send("settings_changed_reboot");
                }
              }
            }
          );
          break;
        }
        case "init":
          this.startup();
          break;
  
        case "open_explorer": {
          const { net_type } = this.config_data.app;
  
          let path = null;
          if (params.type === "tx") {
            path = "tx";
          } else if (params.type === "service_node") {
            path = "service_node";
          }
  
          if (path) {
            const baseUrl =
              net_type === "testnet"
                ? "https://testnet.oxen.observer"
                : "https://oxen.observer";
            const url = `${baseUrl}/${path}/`;
            require("electron").shell.openExternal(url + params.id);
          }
          break;
        }
  
        case "open_url":
          require("electron").shell.openExternal(params.url);
          break;
  
        case "save_png": {
          let filename = dialog.showSaveDialog(this.mainWindow, {
            title: "Save " + params.type,
            filters: [{ name: "PNG", extensions: ["png"] }],
            defaultPath: os.homedir()
          });
          if (filename) {
            let base64Data = params.img.replace(/^data:image\/png;base64,/, "");
            let binaryData = Buffer.from(base64Data, "base64").toString("binary");
            fs.writeFile(filename, binaryData, "binary", err => {
              if (err) {
                this.send("show_notification", {
                  type: "negative",
                  i18n: [
                    "notification.errors.errorSavingItem",
                    { item: params.type }
                  ],
                  timeout: 2000
                });
              } else {
                this.send("show_notification", {
                  i18n: [
                    "notification.positive.itemSaved",
                    { item: params.type, filename }
                  ],
                  timeout: 2000
                });
              }
            });
          }
          break;
        }
  
        default:
          break;
      }
    }
    // if the version is a whole minor version out of date (hardfork out of date)
    // set update required to true
    async checkVersion() {
      try {
        const { data } = await axios.get(
          "https://api.github.com/repos/loki-project/loki-electron-gui-wallet/releases/latest"
        );
        // remove the 'v' from front of the version
        const latestVersion = data.tag_name.substring(1);
        // can return "major", "minor", "patch"
        const vSizeDiff = semver.diff(version, latestVersion);
        const updateAvailable = semver.ltr(version, latestVersion);
        const majorOrMinor = vSizeDiff === "major" || vSizeDiff == "minor";
        const updateRequired = updateAvailable && majorOrMinor;
        this.send("set_update_required", updateRequired);
      } catch (e) {
        this.send("set_updated_required", false);
      }
    }
  
    initLogger(logPath) {
      let log = bunyan.createLogger({
        name: "log",
        streams: [
          {
            type: "rotating-file",
            path: path.join(logPath, "electron.log"),
            period: "1d", // daily rotation
            count: 4 // keep 4 days of logs
          }
        ]
      });
  
      LOG_LEVELS.forEach(level => {
        ipc.on(`log-${level}`, (first, ...rest) => {
          log[level](...rest);
        });
      });
  
      this.log = log;
  
      process.on("uncaughtException", error => {
        log.error("Unhandled Error", error);
      });
  
      process.on("unhandledRejection", error => {
        log.error("Unhandled Promise Rejection", error);
      });
    }
  
    startup() {
      this.send("set_app_data", {
        remotes: this.remotes,
        defaults: this.defaults
      });
  
      this.checkVersion();
  
      fs.readFile(this.config_file, "utf8", (err, data) => {
        if (err) {
          this.send("set_app_data", {
            status: {
              code: -1 // Config not found
            },
            config: this.config_data,
            pending_config: this.config_data
          });
          return;
        }
  
        let disk_config_data = JSON.parse(data);
  
        // semi-shallow object merge
        Object.keys(disk_config_data).map(key => {
          if (!this.config_data.hasOwnProperty(key)) {
            this.config_data[key] = {};
          }
          this.config_data[key] = Object.assign(
            this.config_data[key],
            disk_config_data[key]
          );
        });
  
        // here we may want to check if config data is valid, if not also send code -1
        // i.e. check ports are integers and > 1024, check that data dir path exists, etc
        const validated = Object.keys(this.defaults)
          .filter(k => k in this.config_data)
          .map(k => [
            k,
            this.validate_values(this.config_data[k], this.defaults[k])
          ])
          .reduce((map, obj) => {
            map[obj[0]] = obj[1];
            return map;
          }, {});
  
        // Make sure the daemon data is valid
        this.config_data = {
          ...this.config_data,
          ...validated
        };
  
        // save config file back to file, so updated options are stored on disk
        fs.writeFile(
          this.config_file,
          JSON.stringify(this.config_data, null, 4),
          "utf8",
          () => {}
        );
  
        this.send("set_app_data", {
          config: this.config_data,
          pending_config: this.config_data
        });
  
        // Make the wallet dir
        const { wallet_data_dir, data_dir } = this.config_data.app;
        if (!fs.existsSync(wallet_data_dir)) {
          fs.mkdirpSync(wallet_data_dir);
        }
  
        // Check to see if data and wallet directories exist
        const dirs_to_check = [
          {
            path: data_dir,
            error: "notification.errors.dataPathNotFound"
          },
          {
            path: wallet_data_dir,
            error: "notification.errors.walletPathNotFound"
          }
        ];
  
        for (const dir of dirs_to_check) {
          // Check to see if dir exists
          if (!fs.existsSync(dir.path)) {
            this.send("show_notification", {
              type: "negative",
              i18n: dir.error,
              timeout: 2000
            });
  
            // Go back to config
            this.send("set_app_data", {
              status: {
                code: -1 // Return to config screen
              }
            });
            return;
          }
        }
  
        const { net_type } = this.config_data.app;
  
        const dirs = {
          mainnet: this.config_data.app.data_dir,
          stagenet: path.join(this.config_data.app.data_dir, "stagenet"),
          testnet: path.join(this.config_data.app.data_dir, "testnet")
        };
  
        // Make sure we have the directories we need
        const net_dir = dirs[net_type];
        if (!fs.existsSync(net_dir)) {
          fs.mkdirpSync(net_dir);
        }
  
        const log_dir = path.join(net_dir, "logs");
        if (!fs.existsSync(log_dir)) {
          fs.mkdirpSync(log_dir);
        }
  
        this.initLogger(log_dir);
  
        this.daemon = new Daemon(this);
        this.walletd = new WalletRPC(this);
  
        this.send("set_app_data", {
          status: {
            code: 3 // Starting daemon
          }
        });
  
        // Make sure the remote node provided is accessible
        const config_daemon = this.config_data.daemons[net_type];
        this.daemon.checkRemote(config_daemon).then(data => {
          if (data.error) {
            // If we can default to local then we do so, otherwise we tell the user  to re-set the node
            if (config_daemon.type === "local_remote") {
              this.config_data.daemons[net_type].type = "local";
              this.send("set_app_data", {
                config: this.config_data,
                pending_config: this.config_data
              });
              this.send("show_notification", {
                type: "warning",
                textColor: "black",
                i18n: "notification.warnings.usingLocalNode",
                timeout: 2000
              });
            } else {
              this.send("show_notification", {
                type: "negative",
                i18n: "notification.errors.cannotAccessRemoteNode",
                timeout: 2000
              });
  
              // Go back to config
              this.send("set_app_data", {
                status: {
                  code: -1 // Return to config screen
                }
              });
              return;
            }
          }
  
          // If we got a net type back then check if ours match
          if (data.net_type && data.net_type !== net_type) {
            this.send("show_notification", {
              type: "negative",
              i18n: "notification.errors.differentNetType",
              timeout: 2000
            });
  
            // Go back to config
            this.send("set_app_data", {
              status: {
                code: -1 // Return to config screen
              }
            });
            return;
          }
  
          this.daemon
            .checkVersion()
            .then(version => {
              if (version) {
                this.send("set_app_data", {
                  status: {
                    code: 4,
                    message: version
                  }
                });
              } else {
                // daemon not found, probably removed by AV, set to remote node
                this.config_data.daemons[net_type].type = "remote";
                this.send("set_app_data", {
                  status: {
                    code: 5
                  },
                  config: this.config_data,
                  pending_config: this.config_data
                });
              }
  
              this.daemon
                .start(this.config_data)
                .then(() => {
                  this.send("set_app_data", {
                    status: {
                      code: 6 // Starting wallet
                    }
                  });
  
                  this.walletd
                    .start(this.config_data)
                    .then(() => {
                      this.send("set_app_data", {
                        status: {
                          code: 7 // Reading wallet list
                        }
                      });
  
                      this.walletd.listWallets(true);
  
                      this.send("set_app_data", {
                        status: {
                          code: 0 // Ready
                        }
                      });
                      // eslint-disable-next-line
                    })
                    .catch(error => {
                      this.daemon.killProcess();
                      this.send("show_notification", {
                        type: "negative",
                        message: error.message,
                        timeout: 3000
                      });
                      this.send("set_app_data", {
                        status: {
                          code: -1 // Return to config screen
                        }
                      });
                    });
                  // eslint-disable-next-line
                })
                .catch(error => {
                  if (this.config_data.daemons[net_type].type == "remote") {
                    this.send("show_notification", {
                      type: "negative",
                      i18n: "notification.errors.remoteCannotBeReached",
                      timeout: 3000
                    });
                  } else {
                    this.send("show_notification", {
                      type: "negative",
                      message: error.message,
                      timeout: 3000
                    });
                  }
                  this.send("set_app_data", {
                    status: {
                      code: -1 // Return to config screen
                    }
                  });
                });
              // eslint-disable-next-line
            })
            .catch(() => {
              this.send("set_app_data", {
                status: {
                  code: -1 // Return to config screen
                }
              });
            });
        });
      });
    }
  
    quit() {
      return new Promise(resolve => {
        let process = [];
        if (this.daemon) {
          process.push(this.daemon.quit());
        }
        if (this.walletd) {
          process.push(this.walletd.quit());
        }
        if (this.wss) {
          this.wss.close();
        }
  
        Promise.all(process).then(() => {
          resolve();
        });
      });
    }
  
    // Replace any invalid value with default values
    validate_values(values, defaults) {
      const isDictionary = v =>
        typeof v === "object" &&
        v !== null &&
        !(v instanceof Array) &&
        !(v instanceof Date);
      const modified = { ...values };
  
      // Make sure we have valid defaults
      if (!isDictionary(defaults)) return modified;
  
      for (const key in modified) {
        // Only modify if we have a default
        if (!(key in defaults)) continue;
  
        const defaultValue = defaults[key];
        const invalidDefault =
          defaultValue === null ||
          defaultValue === undefined ||
          Number.isNaN(defaultValue);
        if (invalidDefault) continue;
  
        const value = modified[key];
  
        // If we have a object then recurse through it
        if (isDictionary(value)) {
          modified[key] = this.validate_values(value, defaultValue);
        } else {
          // Check if we need to replace the value
          const isValidValue = !(
            value === undefined ||
            value === null ||
            value === "" ||
            Number.isNaN(value)
          );
          if (isValidValue) continue;
  
          // Otherwise set the default value
          modified[key] = defaultValue;
        }
      }
      return modified;
    }
  }
  