/* global Whisper, window */

/* eslint-disable global-require, no-inner-declarations */

try {
  const electron = require('electron');
  const semver = require('semver');
  const curve = require('curve25519-n');
  const _ = require('lodash');
  const { installGetter, installSetter } = require('./preload_utils');

  const { remote } = electron;
  const { app } = remote;
  const { nativeTheme } = remote.require('electron');

  // Enable calling
  window.CALLING = true;

  window.PROTO_ROOT = 'protos';
  const config = require('url').parse(window.location.toString(), true).query;

  let title = config.name;
  if (config.environment !== 'production') {
    title += ` - ${config.environment}`;
  }
  if (config.appInstance) {
    title += ` - ${config.appInstance}`;
  }

  window.platform = process.platform;
  window.getTitle = () => title;
  window.getEnvironment = () => config.environment;
  window.getAppInstance = () => config.appInstance;
  window.getVersion = () => config.version;
  window.getExpiration = () => config.buildExpiration;
  window.getNodeVersion = () => config.node_version;
  window.getHostName = () => config.hostname;
  window.getServerTrustRoot = () => config.serverTrustRoot;
  window.getServerPublicParams = () => config.serverPublicParams;
  window.isBehindProxy = () => Boolean(config.proxyUrl);

  function setSystemTheme() {
    window.systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
  }

  setSystemTheme();

  window.subscribeToSystemThemeChange = fn => {
    nativeTheme.on('updated', () => {
      setSystemTheme();
      fn();
    });
  };

  window.isBeforeVersion = (toCheck, baseVersion) => {
    try {
      return semver.lt(toCheck, baseVersion);
    } catch (error) {
      window.log.error(
        `isBeforeVersion error: toCheck: ${toCheck}, baseVersion: ${baseVersion}`,
        error && error.stack ? error.stack : error
      );
      return true;
    }
  };

  const ipc = electron.ipcRenderer;
  const localeMessages = ipc.sendSync('locale-data');

  window.setBadgeCount = count => ipc.send('set-badge-count', count);

  // We never do these in our code, so we'll prevent it everywhere
  window.open = () => null;
  // eslint-disable-next-line no-eval, no-multi-assign
  window.eval = global.eval = () => null;

  window.drawAttention = () => {
    window.log.info('draw attention');
    ipc.send('draw-attention');
  };
  window.showWindow = () => {
    window.log.info('show window');
    ipc.send('show-window');
  };

  window.setAutoHideMenuBar = autoHide =>
    ipc.send('set-auto-hide-menu-bar', autoHide);

  window.setMenuBarVisibility = visibility =>
    ipc.send('set-menu-bar-visibility', visibility);

  window.restart = () => {
    window.log.info('restart');
    ipc.send('restart');
  };
  window.shutdown = () => {
    window.log.info('shutdown');
    ipc.send('shutdown');
  };

  window.closeAbout = () => ipc.send('close-about');
  window.readyForUpdates = () => ipc.send('ready-for-updates');

  window.updateTrayIcon = unreadCount =>
    ipc.send('update-tray-icon', unreadCount);

  ipc.on('set-up-as-new-device', () => {
    Whisper.events.trigger('setupAsNewDevice');
  });

  ipc.on('set-up-as-standalone', () => {
    Whisper.events.trigger('setupAsStandalone');
  });

  // Settings-related events

  window.showSettings = () => ipc.send('show-settings');
  window.showPermissionsPopup = () => ipc.send('show-permissions-popup');
  window.showCallingPermissionsPopup = forCamera =>
    ipc.invoke('show-calling-permissions-popup', forCamera);

  ipc.on('show-keyboard-shortcuts', () => {
    window.Events.showKeyboardShortcuts();
  });
  ipc.on('add-dark-overlay', () => {
    window.Events.addDarkOverlay();
  });
  ipc.on('remove-dark-overlay', () => {
    window.Events.removeDarkOverlay();
  });

  installGetter('device-name', 'getDeviceName');

  installGetter('theme-setting', 'getThemeSetting');
  installSetter('theme-setting', 'setThemeSetting');
  installGetter('hide-menu-bar', 'getHideMenuBar');
  installSetter('hide-menu-bar', 'setHideMenuBar');

  installGetter('notification-setting', 'getNotificationSetting');
  installSetter('notification-setting', 'setNotificationSetting');
  installGetter('audio-notification', 'getAudioNotification');
  installSetter('audio-notification', 'setAudioNotification');

  installGetter('spell-check', 'getSpellCheck');
  installSetter('spell-check', 'setSpellCheck');

  installGetter('always-relay-calls', 'getAlwaysRelayCalls');
  installSetter('always-relay-calls', 'setAlwaysRelayCalls');

  installGetter('call-ringtone-notification', 'getCallRingtoneNotification');
  installSetter('call-ringtone-notification', 'setCallRingtoneNotification');

  window.getCallRingtoneNotification = () =>
    new Promise((resolve, reject) => {
      ipc.once(
        'get-success-call-ringtone-notification',
        (_event, error, value) => {
          if (error) {
            return reject(new Error(error));
          }

          return resolve(value);
        }
      );
      ipc.send('get-call-ringtone-notification');
    });

  installGetter('call-system-notification', 'getCallSystemNotification');
  installSetter('call-system-notification', 'setCallSystemNotification');

  window.getCallSystemNotification = () =>
    new Promise((resolve, reject) => {
      ipc.once(
        'get-success-call-system-notification',
        (_event, error, value) => {
          if (error) {
            return reject(new Error(error));
          }

          return resolve(value);
        }
      );
      ipc.send('get-call-system-notification');
    });

  installGetter('incoming-call-notification', 'getIncomingCallNotification');
  installSetter('incoming-call-notification', 'setIncomingCallNotification');

  window.getIncomingCallNotification = () =>
    new Promise((resolve, reject) => {
      ipc.once(
        'get-success-incoming-call-notification',
        (_event, error, value) => {
          if (error) {
            return reject(new Error(error));
          }

          return resolve(value);
        }
      );
      ipc.send('get-incoming-call-notification');
    });

  window.getAlwaysRelayCalls = () =>
    new Promise((resolve, reject) => {
      ipc.once('get-success-always-relay-calls', (_event, error, value) => {
        if (error) {
          return reject(new Error(error));
        }

        return resolve(value);
      });
      ipc.send('get-always-relay-calls');
    });

  window.getMediaPermissions = () =>
    new Promise((resolve, reject) => {
      ipc.once('get-success-media-permissions', (_event, error, value) => {
        if (error) {
          return reject(new Error(error));
        }

        return resolve(value);
      });
      ipc.send('get-media-permissions');
    });

  window.getMediaCameraPermissions = () =>
    new Promise((resolve, reject) => {
      ipc.once(
        'get-success-media-camera-permissions',
        (_event, error, value) => {
          if (error) {
            return reject(new Error(error));
          }

          return resolve(value);
        }
      );
      ipc.send('get-media-camera-permissions');
    });

  window.getBuiltInImages = () =>
    new Promise((resolve, reject) => {
      ipc.once('get-success-built-in-images', (_event, error, value) => {
        if (error) {
          return reject(new Error(error));
        }

        return resolve(value);
      });
      ipc.send('get-built-in-images');
    });

  installGetter('is-primary', 'isPrimary');
  installGetter('sync-request', 'getSyncRequest');
  installGetter('sync-time', 'getLastSyncTime');
  installSetter('sync-time', 'setLastSyncTime');

  ipc.on('delete-all-data', () => {
    const { deleteAllData } = window.Events;
    if (deleteAllData) {
      deleteAllData();
    }
  });

  ipc.on('show-sticker-pack', (_event, info) => {
    const { packId, packKey } = info;
    const { showStickerPack } = window.Events;
    if (showStickerPack) {
      showStickerPack(packId, packKey);
    }
  });

  ipc.on('install-sticker-pack', (_event, info) => {
    const { packId, packKey } = info;
    const { installStickerPack } = window.Events;
    if (installStickerPack) {
      installStickerPack(packId, packKey);
    }
  });

  ipc.on('get-ready-for-shutdown', async () => {
    const { shutdown } = window.Events || {};
    if (!shutdown) {
      window.log.error('preload shutdown handler: shutdown method not found');
      ipc.send('now-ready-for-shutdown');
      return;
    }

    try {
      await shutdown();
      ipc.send('now-ready-for-shutdown');
    } catch (error) {
      ipc.send(
        'now-ready-for-shutdown',
        error && error.stack ? error.stack : error
      );
    }
  });

  window.addSetupMenuItems = () => ipc.send('add-setup-menu-items');
  window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items');

  // We pull these dependencies in now, from here, because they have Node.js dependencies

  require('./js/logging');

  if (config.proxyUrl) {
    window.log.info('Using provided proxy url');
  }

  window.nodeSetImmediate = setImmediate;

  window.textsecure = require('./ts/textsecure').default;

  window.WebAPI = window.textsecure.WebAPI.initialize({
    url: config.serverUrl,
    cdnUrlObject: {
      '0': config.cdnUrl0,
      '2': config.cdnUrl2,
    },
    certificateAuthority: config.certificateAuthority,
    contentProxyUrl: config.contentProxyUrl,
    proxyUrl: config.proxyUrl,
    version: config.version,
  });

  // Linux seems to periodically let the event loop stop, so this is a global workaround
  setInterval(() => {
    window.nodeSetImmediate(() => {});
  }, 1000);

  const { autoOrientImage } = require('./js/modules/auto_orient_image');
  const { imageToBlurHash } = require('./ts/util/imageToBlurHash');

  window.autoOrientImage = autoOrientImage;
  window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
  window.imageToBlurHash = imageToBlurHash;
  window.emojiData = require('emoji-datasource');
  window.filesize = require('filesize');
  window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
  window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
  window.loadImage = require('blueimp-load-image');
  window.getGuid = require('uuid/v4');

  window.isValidGuid = maybeGuid =>
    /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
      maybeGuid
    );
  // https://stackoverflow.com/a/23299989
  window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164);

  window.normalizeUuids = (obj, paths, context) => {
    if (!obj) {
      return;
    }
    paths.forEach(path => {
      const val = _.get(obj, path);
      if (val) {
        if (!window.isValidGuid(val)) {
          window.log.warn(
            `Normalizing invalid uuid: ${val} at path ${path} in context "${context}"`
          );
        }
        _.set(obj, path, val.toLowerCase());
      }
    });
  };

  window.React = require('react');
  window.ReactDOM = require('react-dom');
  window.moment = require('moment');
  window.PQueue = require('p-queue').default;

  const Signal = require('./js/modules/signal');
  const i18n = require('./js/modules/i18n');
  const Attachments = require('./app/attachments');

  const { locale } = config;
  window.i18n = i18n.setup(locale, localeMessages);
  window.moment.updateLocale(locale, {
    relativeTime: {
      s: window.i18n('timestamp_s'),
      m: window.i18n('timestamp_m'),
      h: window.i18n('timestamp_h'),
    },
  });
  window.moment.locale(locale);

  const userDataPath = app.getPath('userData');
  window.baseAttachmentsPath = Attachments.getPath(userDataPath);
  window.baseStickersPath = Attachments.getStickersPath(userDataPath);
  window.baseTempPath = Attachments.getTempPath(userDataPath);
  window.baseDraftPath = Attachments.getDraftPath(userDataPath);
  window.Signal = Signal.setup({
    Attachments,
    userDataPath,
    getRegionCode: () => window.storage.get('regionCode'),
    logger: window.log,
  });

  function wrapWithPromise(fn) {
    return (...args) => Promise.resolve(fn(...args));
  }
  const externalCurve = {
    generateKeyPair: () => {
      const { privKey, pubKey } = curve.generateKeyPair();

      return {
        privKey: window.Signal.Crypto.typedArrayToArrayBuffer(privKey),
        pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(pubKey),
      };
    },
    createKeyPair: incomingKey => {
      const incomingKeyBuffer = Buffer.from(incomingKey);
      const { privKey, pubKey } = curve.createKeyPair(incomingKeyBuffer);

      return {
        privKey: window.Signal.Crypto.typedArrayToArrayBuffer(privKey),
        pubKey: window.Signal.Crypto.typedArrayToArrayBuffer(pubKey),
      };
    },
    calculateAgreement: (pubKey, privKey) => {
      const pubKeyBuffer = Buffer.from(pubKey);
      const privKeyBuffer = Buffer.from(privKey);

      const buffer = curve.calculateAgreement(pubKeyBuffer, privKeyBuffer);

      return window.Signal.Crypto.typedArrayToArrayBuffer(buffer);
    },
    verifySignature: (pubKey, message, signature) => {
      const pubKeyBuffer = Buffer.from(pubKey);
      const messageBuffer = Buffer.from(message);
      const signatureBuffer = Buffer.from(signature);

      const result = curve.verifySignature(
        pubKeyBuffer,
        messageBuffer,
        signatureBuffer
      );

      return result;
    },
    calculateSignature: (privKey, message) => {
      const privKeyBuffer = Buffer.from(privKey);
      const messageBuffer = Buffer.from(message);

      const buffer = curve.calculateSignature(privKeyBuffer, messageBuffer);

      return window.Signal.Crypto.typedArrayToArrayBuffer(buffer);
    },
    validatePubKeyFormat: pubKey => {
      const pubKeyBuffer = Buffer.from(pubKey);

      return curve.validatePubKeyFormat(pubKeyBuffer);
    },
  };
  externalCurve.ECDHE = externalCurve.calculateAgreement;
  externalCurve.Ed25519Sign = externalCurve.calculateSignature;
  externalCurve.Ed25519Verify = externalCurve.verifySignature;
  const externalCurveAsync = {
    generateKeyPair: wrapWithPromise(externalCurve.generateKeyPair),
    createKeyPair: wrapWithPromise(externalCurve.createKeyPair),
    calculateAgreement: wrapWithPromise(externalCurve.calculateAgreement),
    verifySignature: async (...args) => {
      // The async verifySignature function has a different signature than the
      //   sync function
      const verifyFailed = externalCurve.verifySignature(...args);
      if (verifyFailed) {
        throw new Error('Invalid signature');
      }
    },
    calculateSignature: wrapWithPromise(externalCurve.calculateSignature),
    validatePubKeyFormat: wrapWithPromise(externalCurve.validatePubKeyFormat),
    ECDHE: wrapWithPromise(externalCurve.ECDHE),
    Ed25519Sign: wrapWithPromise(externalCurve.Ed25519Sign),
    Ed25519Verify: wrapWithPromise(externalCurve.Ed25519Verify),
  };
  window.libsignal = window.libsignal || {};
  window.libsignal.externalCurve = externalCurve;
  window.libsignal.externalCurveAsync = externalCurveAsync;

  // Pulling these in separately since they access filesystem, electron
  window.Signal.Backup = require('./js/modules/backup');
  window.Signal.Debug = require('./js/modules/debug');
  window.Signal.Logs = require('./js/modules/logs');

  window.addEventListener('contextmenu', e => {
    const editable = e.target.closest(
      'textarea, input, [contenteditable="true"]'
    );
    const link = e.target.closest('a');
    const selection = Boolean(window.getSelection().toString());
    if (!editable && !selection && !link) {
      e.preventDefault();
    }
  });

  if (config.environment === 'test') {
    /* eslint-disable global-require, import/no-extraneous-dependencies */
    window.test = {
      fastGlob: require('fast-glob'),
      normalizePath: require('normalize-path'),
      fse: require('fs-extra'),
      tmp: require('tmp'),
      path: require('path'),
      basePath: __dirname,
      attachmentsPath: window.Signal.Migrations.attachmentsPath,
    };
    /* eslint-enable global-require, import/no-extraneous-dependencies */
  }
} catch (error) {
  /* eslint-disable no-console */
  if (console._log) {
    console._log('preload error!', error.stack);
  } else {
    console.log('preload error!', error.stack);
  }
  /* eslint-enable no-console */

  throw error;
}

window.log.info('preload complete');