// https://github.com/michmich112/sveltekit-adapter-chrome-extension
import staticAdapter from '@sveltejs/adapter-static';
import { load } from 'cheerio';
import {
  createReadStream,
  createWriteStream,
  existsSync,
  readFileSync,
  statSync,
  unlinkSync,
  writeFileSync,
} from 'fs';
import { join } from 'path';
import { pipeline } from 'stream';
import glob from 'tiny-glob';
import { promisify } from 'util';
import zlib from 'zlib';

const pipe = promisify(pipeline);

/** @type {import('.')} */
export default function (options) {
  return {
    name: 'sveltekit-adapter-chrome-extension',

    async adapt(builder) {
      staticAdapter(options).adapt(builder);

      /* extension */
      const pages = options?.pages ?? 'build';
      const assets = options?.assets ?? pages;
      const manifest = options?.manifest ?? 'manifest.json';

      await removeInlineScripts(options, assets, builder.log);

      await removeAppManifest(assets, builder.config.kit.appDir, builder.log);
      await removeAppManifest('.', assets, builder.log);

      // operation required since generated app manifest will overwrite the static extension manifest.json
      reWriteExtensionManifest(assets, manifest, builder);
    },
  };
}

/**
 * Hash using djb2
 * @param {import('types/hooks').StrictBody} value
 */
function hash(value) {
  let hash = 5381;
  let i = value.length;

  if (typeof value === 'string') {
    while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
  } else {
    while (i) hash = (hash * 33) ^ value[--i];
  }

  return (hash >>> 0).toString(36);
}

async function removeAppManifest(directory, appDir, log) {
  log('Removing App Manifest');
  const files = await glob(`**/${appDir}/*manifest*.json`, {
    cwd: directory,
    dot: true,
    absolute: true,
    filesOnly: true,
  });

  files.forEach((path) => {
    try {
      unlinkSync(path);
      log.success(`Removed app manifest file at path: ${path}`);
    } catch (err) {
      log.warn(
        `Error removing app manifest file at path: ${path}. You may have to delete it manually before submitting you extension.\nError: ${err}`
      );
    }
  });
}

async function removeInlineScripts(options, directory, log) {
  log('Removing Inline Scripts');
  const files = await glob('**/*.{html}', {
    cwd: directory,
    dot: true,
    aboslute: true,
    filesOnly: true,
  });

  files
    .map((f) => join(directory, f))
    .forEach((file) => {
      log.minor(`file: ${file}`);
      const f = readFileSync(file);
      const $ = load(f.toString());
      const node = $('script').get()[0];

      if (!node) return;
      if (Object.keys(node.attribs).includes('src')) return; // if there is a src, it's not an inline script

      const attribs = Object.keys(node.attribs).reduce(
        (a, c) => a + `${c}="${node.attribs[c]}" `,
        ''
      );
      let innerScript = node.children[0].data;
      const fullTag = $('script').toString();
      //get new filename
      const fn = `${
        options?.paths?.app ? '/' + options.paths.app + '/immutable' : '.'
      }/inline-${hash(innerScript)}.js`;
      if (options?.paths?.app)
        innerScript = innerScript.replaceAll(options.paths.app + '/', '../');
      //remove from orig html file and replace with new script tag
      const newHtml = f
        .toString()
        .replace(
          fullTag,
          `<script ${attribs} src=${JSON.stringify(
            (options?.paths?.base ?? '/') + fn
          )}></script>`
        );
      writeFileSync(file, newHtml);
      log.minor(`Rewrote ${file}`);

      const p = `${directory}${fn}`;
      writeFileSync(p, innerScript);
      log.success(`Inline script extracted and saved at: ${p}`);
    });
}

function reWriteExtensionManifest(directory, manifest, builder) {
  const { log, getStaticDirectory, getClientDirectory, copy } = builder;
  log('Re-writing extension manifest');
  let sourceFilePath;
  if (typeof getStaticDirectory !== 'undefined') {
    sourceFilePath = join(getStaticDirectory(), manifest);
  } else {
    sourceFilePath = join(getClientDirectory(), manifest);
  }
  if (existsSync(sourceFilePath)) {
    log.info('Extension manifest found');
    const res = copy(sourceFilePath, join(directory, 'manifest.json'));
    log.success('Successfully re-wrote extension manifest');
  } else {
    log.error(
      `Extension manifest not found. Make sure you've added your extension manifest in your statics directory with the name ${manifest}`
    );
  }
}

/**
 * @param {string} directory
 */
async function compress(directory) {
  const files = await glob('**/*.{html,js,json,css,svg,xml}', {
    cwd: directory,
    dot: true,
    absolute: true,
    filesOnly: true,
  });

  await Promise.all(
    files.map((file) =>
      Promise.all([compress_file(file, 'gz'), compress_file(file, 'br')])
    )
  );
}

/**
 * @param {string} file
 * @param {'gz' | 'br'} format
 */
async function compress_file(file, format = 'gz') {
  const compress =
    format == 'br'
      ? zlib.createBrotliCompress({
          params: {
            [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
            [zlib.constants.BROTLI_PARAM_QUALITY]:
              zlib.constants.BROTLI_MAX_QUALITY,
            [zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size,
          },
        })
      : zlib.createGzip({ level: zlib.constants.Z_BEST_COMPRESSION });

  const source = createReadStream(file);
  const destination = createWriteStream(`${file}.${format}`);

  await pipe(source, compress, destination);
}