import { fileURLToPath } from 'url';

import type { NormalizedOutputOptions, RenderedChunk } from 'rollup';
import { hasOwnProperty, isObject, merge } from 'smob';

import type { Options } from './type';
import { WorkerPool } from './worker-pool';

export default function terser(input: Options = {}) {
  const { maxWorkers, ...options } = input;

  let workerPool: WorkerPool | null | undefined;
  let numOfChunks = 0;
  let numOfWorkersUsed = 0;

  // Chained minify tasks; used only when a shared nameCache is provided,
  // since terser requires sequential minification for consistent mangling.
  let nameCacheChain: Promise<unknown> = Promise.resolve();

  return {
    name: 'terser',

    async renderChunk(code: string, chunk: RenderedChunk, outputOptions: NormalizedOutputOptions) {
      if (!workerPool) {
        workerPool = new WorkerPool({
          filePath: fileURLToPath(import.meta.url),
          maxWorkers
        });
      }

      const pool = workerPool;

      numOfChunks += 1;

      const defaultOptions: Options = {
        sourceMap: outputOptions.sourcemap === true || typeof outputOptions.sourcemap === 'string'
      };

      if (outputOptions.format === 'es') {
        defaultOptions.module = true;
      }

      if (outputOptions.format === 'cjs') {
        defaultOptions.toplevel = true;
      }

      const runTask = async () => {
        const {
          code: result,
          nameCache,
          sourceMap
        } = await pool.addAsync({
          code,
          options: merge({}, options || {}, defaultOptions)
        });

        if (options.nameCache && nameCache) {
          let vars: Record<string, any> = {
            props: {}
          };

          if (hasOwnProperty(options.nameCache, 'vars') && isObject(options.nameCache.vars)) {
            vars = merge({}, options.nameCache.vars || {}, vars);
          }

          if (hasOwnProperty(nameCache, 'vars') && isObject(nameCache.vars)) {
            vars = merge({}, nameCache.vars, vars);
          }

          // eslint-disable-next-line no-param-reassign
          options.nameCache.vars = vars;

          let props: Record<string, any> = {};

          if (hasOwnProperty(options.nameCache, 'props') && isObject(options.nameCache.props)) {
            // eslint-disable-next-line prefer-destructuring
            props = options.nameCache.props;
          }

          if (hasOwnProperty(nameCache, 'props') && isObject(nameCache.props)) {
            props = merge({}, nameCache.props, props);
          }

          // eslint-disable-next-line no-param-reassign
          options.nameCache.props = props;
        }

        return { result, sourceMap };
      };

      try {
        let output;
        if (options.nameCache) {
          // The options snapshot and its serialization must only happen once
          // the previous chunk's nameCache has been merged back.
          const task = nameCacheChain.then(runTask);
          nameCacheChain = task.catch(() => {});
          output = await task;
        } else {
          output = await runTask();
        }

        if ((!!defaultOptions.sourceMap || !!options.sourceMap) && isObject(output.sourceMap)) {
          return {
            code: output.result,
            map: output.sourceMap
          };
        }
        return output.result;
      } catch (e) {
        return Promise.reject(e);
      } finally {
        numOfChunks -= 1;
        if (numOfChunks === 0) {
          numOfWorkersUsed = workerPool.numWorkers;
          workerPool.close();
          workerPool = null;
        }
      }
    },

    get numOfWorkersUsed() {
      return numOfWorkersUsed;
    }
  };
}
