// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

/* global window */
const { ipcRenderer: ipc } = require('electron');
const sharp = require('sharp');
const pify = require('pify');
const { readFile } = require('fs');
const config = require('url').parse(window.location.toString(), true).query;
const { noop, uniqBy } = require('lodash');
const pMap = require('p-map');

// It is important to call this as early as possible
const { SignalContext } = require('../ts/windows/context');

const {
  deriveStickerPackKey,
  encryptAttachment,
  getRandomBytes,
} = require('../ts/Crypto');
const Bytes = require('../ts/Bytes');
const { SignalService: Proto } = require('../ts/protobuf');
const { getEnvironment } = require('../ts/environment');
const { createSetting } = require('../ts/util/preload');

const STICKER_SIZE = 512;
const MIN_STICKER_DIMENSION = 10;
const MAX_STICKER_DIMENSION = STICKER_SIZE;
const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024;
const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;

window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
window.getEnvironment = getEnvironment;
window.getVersion = () => config.version;
window.PQueue = require('p-queue').default;
window.Backbone = require('backbone');

window.localeMessages = ipc.sendSync('locale-data');

require('../ts/SignalProtocolStore');

SignalContext.log.info('sticker-creator starting up...');

const Signal = require('../js/modules/signal');

window.Signal = Signal.setup({});
window.textsecure = require('../ts/textsecure').default;

const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
const {
  getAnimatedPngDataIfExists,
} = require('../ts/util/getAnimatedPngDataIfExists');

const WebAPI = initializeWebAPI({
  url: config.serverUrl,
  storageUrl: config.storageUrl,
  updatesUrl: config.updatesUrl,
  directoryUrl: config.directoryUrl,
  directoryEnclaveId: config.directoryEnclaveId,
  directoryTrustAnchor: config.directoryTrustAnchor,
  directoryV2Url: config.directoryV2Url,
  directoryV2PublicKey: config.directoryV2PublicKey,
  directoryV2CodeHash: config.directoryV2CodeHash,
  cdnUrlObject: {
    0: config.cdnUrl0,
    2: config.cdnUrl2,
  },
  certificateAuthority: config.certificateAuthority,
  contentProxyUrl: config.contentProxyUrl,
  proxyUrl: config.proxyUrl,
  version: config.version,
});

function processStickerError(message, i18nKey) {
  const result = new Error(message);
  result.errorMessageI18nKey = i18nKey;
  return result;
}

window.processStickerImage = async path => {
  const imgBuffer = await pify(readFile)(path);
  const sharpImg = sharp(imgBuffer);
  const meta = await sharpImg.metadata();

  const { width, height } = meta;
  if (!width || !height) {
    throw processStickerError(
      'Sticker height or width were falsy',
      'StickerCreator--Toasts--errorProcessing'
    );
  }

  let contentType;
  let processedBuffer;

  // [Sharp doesn't support APNG][0], so we do something simpler: validate the file size
  //   and dimensions without resizing, cropping, or converting. In a perfect world, we'd
  //   resize and convert any animated image (GIF, animated WebP) to APNG.
  // [0]: https://github.com/lovell/sharp/issues/2375
  const animatedPngDataIfExists = getAnimatedPngDataIfExists(imgBuffer);
  if (animatedPngDataIfExists) {
    if (imgBuffer.byteLength > MAX_ANIMATED_STICKER_BYTE_LENGTH) {
      throw processStickerError(
        'Sticker file was too large',
        'StickerCreator--Toasts--tooLarge'
      );
    }
    if (width !== height) {
      throw processStickerError(
        'Sticker must be square',
        'StickerCreator--Toasts--APNG--notSquare'
      );
    }
    if (width > MAX_STICKER_DIMENSION) {
      throw processStickerError(
        'Sticker dimensions are too large',
        'StickerCreator--Toasts--APNG--dimensionsTooLarge'
      );
    }
    if (width < MIN_STICKER_DIMENSION) {
      throw processStickerError(
        'Sticker dimensions are too small',
        'StickerCreator--Toasts--APNG--dimensionsTooSmall'
      );
    }
    if (animatedPngDataIfExists.numPlays !== Infinity) {
      throw processStickerError(
        'Animated stickers must loop forever',
        'StickerCreator--Toasts--mustLoopForever'
      );
    }
    contentType = 'image/png';
    processedBuffer = imgBuffer;
  } else {
    contentType = 'image/webp';
    processedBuffer = await sharpImg
      .resize({
        width: STICKER_SIZE,
        height: STICKER_SIZE,
        fit: 'contain',
        background: { r: 0, g: 0, b: 0, alpha: 0 },
      })
      .webp()
      .toBuffer();
    if (processedBuffer.byteLength > MAX_WEBP_STICKER_BYTE_LENGTH) {
      throw processStickerError(
        'Sticker file was too large',
        'StickerCreator--Toasts--tooLarge'
      );
    }
  }

  return {
    path,
    buffer: processedBuffer,
    src: `data:${contentType};base64,${processedBuffer.toString('base64')}`,
    meta,
  };
};

window.encryptAndUpload = async (
  manifest,
  stickers,
  cover,
  onProgress = noop
) => {
  const usernameItem = await window.Signal.Data.getItemById('uuid_id');
  const oldUsernameItem = await window.Signal.Data.getItemById('number_id');
  const passwordItem = await window.Signal.Data.getItemById('password');

  if (!oldUsernameItem || !passwordItem) {
    const { message } =
      window.localeMessages['StickerCreator--Authentication--error'];

    ipc.send('show-message-box', {
      type: 'warning',
      message,
    });

    throw new Error(message);
  }

  const { value: username } = usernameItem;
  const { value: oldUsername } = oldUsernameItem;
  const { value: password } = passwordItem;

  const packKey = getRandomBytes(32);
  const encryptionKey = deriveStickerPackKey(packKey);
  const iv = getRandomBytes(16);

  const server = WebAPI.connect({
    username: username || oldUsername,
    password,
    useWebSocket: false,
  });

  const uniqueStickers = uniqBy(
    [...stickers, { imageData: cover }],
    'imageData'
  );

  const manifestProto = new Proto.StickerPack();
  manifestProto.title = manifest.title;
  manifestProto.author = manifest.author;
  manifestProto.stickers = stickers.map(({ emoji }, id) => {
    const s = new Proto.StickerPack.Sticker();
    s.id = id;
    s.emoji = emoji;

    return s;
  });
  const coverSticker = new Proto.StickerPack.Sticker();
  coverSticker.id =
    uniqueStickers.length === stickers.length ? 0 : uniqueStickers.length - 1;
  coverSticker.emoji = '';
  manifestProto.cover = coverSticker;

  const encryptedManifest = await encrypt(
    Proto.StickerPack.encode(manifestProto).finish(),
    encryptionKey,
    iv
  );
  const encryptedStickers = await pMap(
    uniqueStickers,
    ({ imageData }) => encrypt(imageData.buffer, encryptionKey, iv),
    {
      concurrency: 3,
      timeout: 1000 * 60 * 2,
    }
  );

  const packId = await server.putStickers(
    encryptedManifest,
    encryptedStickers,
    onProgress
  );

  const hexKey = Bytes.toHex(packKey);

  ipc.send('install-sticker-pack', packId, hexKey);

  return { packId, key: hexKey };
};

async function encrypt(data, key, iv) {
  const { ciphertext } = await encryptAttachment(data, key, iv);

  return ciphertext;
}

const getThemeSetting = createSetting('themeSetting');

async function resolveTheme() {
  const theme = (await getThemeSetting.getValue()) || 'system';
  if (process.platform === 'darwin' && theme === 'system') {
    const { theme: nativeTheme } = SignalContext.nativeThemeListener;
    return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
  }
  return theme;
}

async function applyTheme() {
  window.document.body.classList.remove('dark-theme');
  window.document.body.classList.remove('light-theme');
  window.document.body.classList.add(`${await resolveTheme()}-theme`);
}

window.addEventListener('DOMContentLoaded', applyTheme);

SignalContext.nativeThemeListener.subscribe(() => applyTheme());

SignalContext.log.info('sticker-creator preload complete...');
