/**
 * @license
 * Copyright 2010 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

// General JS utilities - things that might be useful in any JS project.
// Nothing specific to Emscripten appears here.

import * as url from 'node:url';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as vm from 'node:vm';
import assert from 'node:assert';

export function safeQuote(x) {
  return x.replace(/"/g, '\\"').replace(/'/g, "\\'");
}

export function dump(item) {
  let funcData;
  try {
    if (typeof item == 'object' && item != null && item.funcData) {
      funcData = item.funcData;
      item.funcData = null;
    }
    return '// ' + JSON.stringify(item, null, '  ').replace(/\n/g, '\n// ');
  } catch {
    const ret = [];
    for (const [i, j] of Object.entries(item)) {
      if (typeof j == 'string' || typeof j == 'number') {
        ret.push(`${i}: ${j}`);
      } else {
        ret.push(`${i}: [?]`);
      }
    }
    return ret.join(',\n');
  } finally {
    if (funcData) item.funcData = funcData;
  }
}

let warnings = false;

export function warningOccured() {
  return warnings;
}

let currentFile = [];

export function pushCurrentFile(f) {
  currentFile.push(f);
}

export function popCurrentFile() {
  currentFile.pop();
}

function errorPrefix(lineNo) {
  if (!currentFile.length) return '';
  const filename = currentFile[currentFile.length - 1];
  if (lineNo) {
    return `${filename}:${lineNo}: `;
  } else {
    return `${filename}: `;
  }
}

export function warn(msg, lineNo) {
  warnings = true;
  printErr(`warning: ${errorPrefix(lineNo)}${msg}`);
}

const seenWarnings = new Set();

export function warnOnce(msg) {
  if (!seenWarnings.has(msg)) {
    seenWarnings.add(msg);
    warn(msg);
  }
}

let abortExecution = false;

export function errorOccured() {
  return abortExecution;
}

export function error(msg, lineNo) {
  abortExecution = true;
  process.exitCode = 1;
  printErr(`error: ${errorPrefix(lineNo)}${msg}`);
}

function range(size) {
  return Array.from(Array(size).keys());
}

export function mergeInto(obj, other, options = null) {
  if (options) {
    // check for unintended symbol redefinition
    if (options.noOverride) {
      for (const key of Object.keys(other)) {
        if (obj.hasOwnProperty(key)) {
          error(
            `Symbol re-definition in JavaScript library: ${key}. Do not use noOverride if this is intended`,
          );
          return;
        }
      }
    }

    // check if sig is missing for added functions
    if (options.checkSig) {
      for (const [key, value] of Object.entries(other)) {
        if (typeof value === 'function' && !other.hasOwnProperty(key + '__sig')) {
          error(`__sig is missing for function: ${key}. Do not use checkSig if this is intended`);
          return;
        }
      }
    }
  }

  if (!options || !options.allowMissing) {
    for (const ident of Object.keys(other)) {
      if (isDecorator(ident)) {
        const index = ident.lastIndexOf('__');
        const basename = ident.slice(0, index);
        if (!(basename in obj) && !(basename in other)) {
          error(`Missing library element '${basename}' for library config '${ident}'`);
        }
      }
    }
  }

  for (const key of Object.keys(other)) {
    if (isDecorator(key)) {
      if (key.endsWith('__sig')) {
        if (obj.hasOwnProperty(key)) {
          const oldsig = obj[key];
          const newsig = other[key];
          if (oldsig == newsig) {
            warn(`signature redefinition for: ${key}`);
          } else {
            error(`signature redefinition for: ${key}. (old=${oldsig} vs new=${newsig})`);
          }
        }
      }

      const index = key.lastIndexOf('__');
      const decoratorName = key.slice(index);
      const type = typeof other[key];

      if (decoratorName == '__async') {
        const decorated = key.slice(0, index);
        if (isJsOnlySymbol(decorated)) {
          error(`__async decorator applied to JS symbol: ${decorated}`);
        }
      }

      // Specific type checking for `__deps` which is expected to be an array
      // (not just any old `object`)
      if (decoratorName === '__deps') {
        const deps = other[key];
        if (!Array.isArray(deps)) {
          error(
            `JS library directive ${key}=${deps} is of type '${type}', but it should be an array`,
          );
        }
        for (const dep of deps) {
          if (dep && typeof dep !== 'string' && typeof dep !== 'function') {
            error(
              `__deps entries must be of type 'string' or 'function' not '${typeof dep}': ${key}`,
            );
          }
        }
      } else {
        // General type checking for all other decorators
        const decoratorTypes = {
          __sig: 'string',
          __proxy: 'string',
          __asm: 'boolean',
          __postset: ['string', 'function'],
          __docs: 'string',
          __nothrow: 'boolean',
          __noleakcheck: 'boolean',
          __internal: 'boolean',
          __user: 'boolean',
          __async: ['string', 'boolean'],
          __i53abi: 'boolean',
        };
        const expected = decoratorTypes[decoratorName];
        if (type !== expected && !expected.includes(type)) {
          error(`Decorator (${key}) has wrong type. Expected '${expected}' not '${type}'`);
        }
      }
    }
  }

  return Object.assign(obj, other);
}

// Symbols that start with '$' are not exported to the wasm module.
// They are intended to be called exclusively by JS code.
export function isJsOnlySymbol(symbol) {
  return symbol[0] == '$';
}

export const decoratorSuffixes = [
  '__sig',
  '__proxy',
  '__asm',
  '__deps',
  '__postset',
  '__docs',
  '__nothrow',
  '__noleakcheck',
  '__internal',
  '__user',
  '__async',
  '__i53abi',
];

export function isDecorator(ident) {
  return decoratorSuffixes.some((suffix) => ident.endsWith(suffix));
}

export function readFile(filename) {
  return fs.readFileSync(filename, 'utf8');
}

// Use import.meta.dirname here once we drop support for node v18.
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

export const srcDir = __dirname;

// Returns an absolute path for a file, resolving it relative to this script
// (i.e. relative to the src/ directory).
export function localFile(filename) {
  assert(!path.isAbsolute(filename));
  return path.join(srcDir, filename);
}

// Helper function for JS library files that can be used to read files
// relative to the src/ directory.
function read(filename) {
  if (!path.isAbsolute(filename)) {
    filename = localFile(filename);
  }
  return readFile(filename);
}

export function printErr(...args) {
  console.error(...args);
}

export function debugLog(...args) {
  if (VERBOSE) printErr(...args);
}

class Profiler {
  ids = [];
  lastTime = 0;

  constructor() {
    this.start('overall')
    this.startTime = performance.now();
  }

  log(msg) {
    const depth = this.ids.length;
    const indent = ' '.repeat(depth)
    printErr('[prof] ' + indent + msg);
  }

  start(id) {
    this.log(`-> ${id}`)
    const now = performance.now();
    this.ids.push([id, now]);
  }

  stop(id) {
    const [poppedId, startTime] = this.ids.pop();
    assert(id === poppedId);
    const now = performance.now();
    const duration = now - startTime;
    this.log(`<- ${id} [${duration.toFixed(1)} ms]`)
  }

  terminate() {
    while (this.ids.length) {
      const lastID = this.ids[this.ids.length - 1][0];
      this.stop(lastID);
    }
    // const overall = performance.now() - this.startTime
    // printErr(`overall total: ${overall.toFixed(1)} ms`);
  }
}

class NullProfiler {
  start(_id) {}
  stop(_id) {}
  terminate() {}
}

// Enable JS compiler profiling if EMPROFILE is "2".  This mode reports profile
// data to stderr.
const EMPROFILE = process.env.EMPROFILE == '2';

export const timer = EMPROFILE ? new Profiler() : new NullProfiler();

if (EMPROFILE) {
  process.on('exit', () => timer.terminate());
}

/**
 * Context in which JS library code is evaluated.  This is distinct from the
 * global scope of the compiler itself which avoids exposing all of the compiler
 * internals to user JS library code.
 */
export const compileTimeContext = vm.createContext({
  process,
  console,
});

/**
 * A symbols to the macro context.
 * This will makes the symbols available to JS library code at build time.
 */
export function addToCompileTimeContext(object) {
  Object.assign(compileTimeContext, object);
}

const setLikeSettings = [
  'EXPORTED_FUNCTIONS',
  'WASM_EXPORTS',
  'SIDE_MODULE_EXPORTS',
  'INCOMING_MODULE_JS_API',
  'ALL_INCOMING_MODULE_JS_API',
  'EXTRA_INCOMING_JS_API',
  'EXPORTED_RUNTIME_METHODS',
  'WEAK_IMPORTS'
];

export function applySettings(obj) {
  // Certain settings are read in as lists, but we convert them to Set
  // within the compiler, for efficiency.
  for (const key of setLikeSettings) {
    if (typeof obj[key] !== 'undefined') {
      obj[key] = new Set(obj[key]);
    }
  }

  // Make settings available both in the current / global context
  // and also in the macro execution context.
  Object.assign(globalThis, obj);
  addToCompileTimeContext(obj);
}

export function loadSettingsFile(f) {
  timer.start('loadSettingsFile')
  const settings = {};
  vm.runInNewContext(readFile(f), settings, {filename: f});
  applySettings(settings);
  timer.stop('loadSettingsFile')
  return settings;
}

export function loadDefaultSettings() {
  const rtn = loadSettingsFile(localFile('settings.js'));
  Object.assign(rtn, loadSettingsFile(localFile('settings_internal.js')));
  return rtn;
}

export function runInMacroContext(code, options) {
  compileTimeContext['__filename'] = options.filename;
  compileTimeContext['__dirname'] = path.dirname(options.filename);
  return vm.runInContext(code, compileTimeContext, options);
}

addToCompileTimeContext({
  assert,
  decoratorSuffixes,
  error,
  isDecorator,
  isJsOnlySymbol,
  mergeInto,
  read,
  warn,
  warnOnce,
  printErr,
  range,
});
