# Copyright 2021 The Emscripten Authors.  All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License.  Both these licenses can be
# found in the LICENSE file.

import copy
import difflib
import os
import re
from typing import Any

from . import diagnostics
from .utils import exit_with_error, path_from_root

# Subset of settings that take a memory size (i.e. 1Gb, 64kb etc)
MEM_SIZE_SETTINGS = {
    'GLOBAL_BASE',
    'STACK_SIZE',
    'TOTAL_STACK',
    'INITIAL_HEAP',
    'INITIAL_MEMORY',
    'MEMORY_GROWTH_LINEAR_STEP',
    'MEMORY_GROWTH_GEOMETRIC_CAP',
    'GL_MAX_TEMP_BUFFER_SIZE',
    'MAXIMUM_MEMORY',
    'DEFAULT_PTHREAD_STACK_SIZE',
    'ASYNCIFY_STACK_SIZE',
}

PORTS_SETTINGS = {
    # All port-related settings are valid at compile time
    'USE_SDL',
    'USE_LIBPNG',
    'USE_BULLET',
    'USE_ZLIB',
    'USE_BZIP2',
    'USE_VORBIS',
    'USE_COCOS2D',
    'USE_ICU',
    'USE_MODPLUG',
    'USE_SDL_MIXER',
    'USE_SDL_IMAGE',
    'USE_SDL_TTF',
    'USE_SDL_NET',
    'USE_SDL_GFX',
    'USE_LIBJPEG',
    'USE_OGG',
    'USE_REGAL',
    'USE_BOOST_HEADERS',
    'USE_HARFBUZZ',
    'USE_MPG123',
    'USE_GIFLIB',
    'USE_FREETYPE',
    'SDL2_MIXER_FORMATS',
    'SDL2_IMAGE_FORMATS',
    'USE_SQLITE3',
}

# Subset of settings that apply only when generating JS
JS_ONLY_SETTINGS = {
    'DEFAULT_LIBRARY_FUNCS_TO_INCLUDE',
    'INCLUDE_FULL_LIBRARY',
    'BUILD_AS_WORKER',
    'STRICT_JS',
    'SMALL_XHR_CHUNKS',
    'MODULARIZE',
    'EXPORT_ES6',
    'EXPORT_NAME',
    'DYNAMIC_EXECUTION',
    'PTHREAD_POOL_SIZE',
    'PTHREAD_POOL_SIZE_STRICT',
    'PTHREAD_POOL_DELAY_LOAD',
    'DEFAULT_PTHREAD_STACK_SIZE',
}

# Subset of settings that apply at compile time.
# (Keep in sync with [compile] comments in settings.js)
COMPILE_TIME_SETTINGS = {
    'MEMORY64',
    'INLINING_LIMIT',
    'DISABLE_EXCEPTION_CATCHING',
    'DISABLE_EXCEPTION_THROWING',
    'WASM_LEGACY_EXCEPTIONS',
    'MAIN_MODULE',
    'SIDE_MODULE',
    'RELOCATABLE',
    'LINKABLE',
    'STRICT',
    'EMSCRIPTEN_TRACING',
    'PTHREADS',
    'USE_PTHREADS', # legacy name of PTHREADS setting
    'SHARED_MEMORY',
    'SUPPORT_LONGJMP',
    'WASM_OBJECT_FILES',
    'WASM_WORKERS',

    # Internal settings used during compilation
    'EXCEPTION_CATCHING_ALLOWED',
    'WASM_EXCEPTIONS',
    'LTO',
    'OPT_LEVEL',
    'DEBUG_LEVEL',
}.union(PORTS_SETTINGS)

# Unlike `LEGACY_SETTINGS`, deprecated settings can still be used
# both on the command line and in the emscripten codebase.
#
# At some point in the future, once folks have stopped using these
# settings we can move them to `LEGACY_SETTINGS`.
#
# All settings here should be tagged as `[deprecated]` in settings.js
DEPRECATED_SETTINGS = {
    'RUNTIME_LINKED_LIBS': 'you can simply list the libraries directly on the commandline now',
    'CLOSURE_WARNINGS': 'use -Wclosure/-Wno-closure instead',
    'LEGALIZE_JS_FFI': 'to disable JS type legalization use `-sWASM_BIGINT` or `-sSTANDALONE_WASM`',
    'ASYNCIFY_EXPORTS': 'please use JSPI_EXPORTS instead',
    'LINKABLE': 'under consideration for removal (https://github.com/emscripten-core/emscripten/issues/25262)',
    'RELOCATABLE': ' under consideration for removal (https://github.com/emscripten-core/emscripten/issues/25262)',
}

# Settings that don't need to be externalized when serializing to json because they
# are not used by the JS compiler.
INTERNAL_SETTINGS = {
    'SIDE_MODULE_IMPORTS',
}

# List of incompatible settings, of the form (SETTINGS_A, SETTING_B, OPTIONAL_REASON_FOR_INCOMPAT)
INCOMPATIBLE_SETTINGS = [
    ('MINIMAL_RUNTIME', 'RELOCATABLE', None),
    ('WASM2JS', 'MAIN_MODULE', 'wasm2js does not support dynamic linking'),
    ('WASM2JS', 'SIDE_MODULE', 'wasm2js does not support dynamic linking'),
    ('MODULARIZE', 'NO_DECLARE_ASM_MODULE_EXPORTS', None),
    ('EVAL_CTORS', 'WASM2JS', None),
    ('EVAL_CTORS', 'RELOCATABLE', 'movable segments'),
    # In Asyncify exports can be called more than once, and this seems to not
    # work properly yet (see test_emscripten_scan_registers).
    ('EVAL_CTORS', 'ASYNCIFY', None),
    ('PTHREADS_PROFILING', 'NO_ASSERTIONS', 'only works with ASSERTIONS enabled'),
    ('SOURCE_PHASE_IMPORTS', 'NO_EXPORT_ES6', None),
    ('STANDALONE_WASM', 'MINIMAL_RUNTIME', None),
    ('STRICT_JS', 'MODULARIZE', None),
    ('STRICT_JS', 'EXPORT_ES6', None),
    ('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION', 'MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION', 'they are mutually exclusive'),
    ('MINIMAL_RUNTIME_STREAMING_WASM_COMPILATION', 'SINGLE_FILE', None),
    ('MINIMAL_RUNTIME_STREAMING_WASM_INSTANTIATION', 'SINGLE_FILE', None),
    ('SEPARATE_DWARF', 'WASM2JS', 'as there is no wasm file'),
    ('GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS', 'NO_GL_SUPPORT_SIMPLE_ENABLE_EXTENSIONS', None),
    ('MODULARIZE', 'NODEJS_CATCH_REJECTION', None),
    ('MODULARIZE', 'NODEJS_CATCH_EXIT', None),
    ('LEGACY_VM_SUPPORT', 'MEMORY64', None),
    ('CROSS_ORIGIN', 'NO_DYNAMIC_EXECUTION', None),
    ('CROSS_ORIGIN', 'NO_PTHREADS', None),
]

EXPERIMENTAL_SETTINGS = {
    'SPLIT_MODULE': '-sSPLIT_MODULE is experimental and subject to change',
    'WASM_JS_TYPES': '-sWASM_JS_TYPES is only supported under a flag in certain browsers',
    'SOURCE_PHASE_IMPORTS': '-sSOURCE_PHASE_IMPORTS is experimental and not yet supported in browsers',
    'JS_BASE64_API': '-sJS_BASE64_API is experimental and not yet supported in browsers',
    'GROWABLE_ARRAYBUFFERS': '-sGROWABLE_ARRAYBUFFERS is experimental and not yet supported in browsers',
    'SUPPORT_BIG_ENDIAN': '-sSUPPORT_BIG_ENDIAN is experimental, not all features are fully supported.',
    'WASM_ESM_INTEGRATION': '-sWASM_ESM_INTEGRATION is still experimental and not yet supported in browsers',
}

# For renamed settings the format is:
# [OLD_NAME, NEW_NAME]
# For removed settings (which now effectively have a fixed value and can no
# longer be changed) the format is:
# [OPTION_NAME, POSSIBLE_VALUES, ERROR_EXPLANATION], where POSSIBLE_VALUES is
# an array of values that will still be silently accepted by the compiler.
# First element in the list is the canonical/fixed value going forward.
# This allows existing build systems to keep specifying one of the supported
# settings, for backwards compatibility.
# When a setting has been removed, and we want to error on all values of it,
# we can set POSSIBLE_VALUES to an impossible value (like "disallowed" for a
# numeric setting, or -1 for a string setting).
LEGACY_SETTINGS = [
    ['BINARYEN', 'WASM'],
    ['TOTAL_STACK', 'STACK_SIZE'],
    ['BINARYEN_ASYNC_COMPILATION', 'WASM_ASYNC_COMPILATION'],
    ['UNALIGNED_MEMORY', [0], 'forced unaligned memory not supported in fastcomp'],
    ['FORCE_ALIGNED_MEMORY', [0], 'forced aligned memory is not supported in fastcomp'],
    ['PGO', [0], 'pgo no longer supported'],
    ['QUANTUM_SIZE', [4], 'altering the QUANTUM_SIZE is not supported'],
    ['FUNCTION_POINTER_ALIGNMENT', [2], 'Starting from Emscripten 1.37.29, no longer available (https://github.com/emscripten-core/emscripten/pull/6091)'],
    # Reserving function pointers is not needed - allowing table growth allows any number of new functions to be added.
    ['RESERVED_FUNCTION_POINTERS', 'ALLOW_TABLE_GROWTH'],
    ['BUILD_AS_SHARED_LIB', [0], 'Starting from Emscripten 1.38.16, no longer available (https://github.com/emscripten-core/emscripten/pull/7433)'],
    ['SAFE_SPLIT_MEMORY', [0], 'Starting from Emscripten 1.38.19, SAFE_SPLIT_MEMORY codegen is no longer available (https://github.com/emscripten-core/emscripten/pull/7465)'],
    ['SPLIT_MEMORY', [0], 'Starting from Emscripten 1.38.19, SPLIT_MEMORY codegen is no longer available (https://github.com/emscripten-core/emscripten/pull/7465)'],
    ['BINARYEN_METHOD', ['native-wasm'], 'Starting from Emscripten 1.38.23, Emscripten now always builds either to Wasm (-sWASM - default), or to JavaScript (-sWASM=0), other methods are not supported (https://github.com/emscripten-core/emscripten/pull/7836)'],
    ['BINARYEN_TRAP_MODE', [-1], 'The wasm backend does not support a trap mode (it always clamps, in effect)'],
    ['PRECISE_I64_MATH', [1, 2], 'Starting from Emscripten 1.38.26, PRECISE_I64_MATH is always enabled (https://github.com/emscripten-core/emscripten/pull/7935)'],
    ['MEMFS_APPEND_TO_TYPED_ARRAYS', [1], 'Starting from Emscripten 1.38.26, MEMFS_APPEND_TO_TYPED_ARRAYS=0 is no longer supported. MEMFS no longer supports using JS arrays for file data (https://github.com/emscripten-core/emscripten/pull/7918)'],
    ['ERROR_ON_MISSING_LIBRARIES', [1], 'missing libraries are always an error now'],
    ['EMITTING_JS', [1], 'The new STANDALONE_WASM flag replaces this (replace EMITTING_JS=0 with STANDALONE_WASM=1)'],
    ['SKIP_STACK_IN_SMALL', [0, 1], 'SKIP_STACK_IN_SMALL is no longer needed as the backend can optimize it directly'],
    ['SAFE_STACK', [0], 'Replace SAFE_STACK=1 with STACK_OVERFLOW_CHECK=2'],
    ['MEMORY_GROWTH_STEP', 'MEMORY_GROWTH_LINEAR_STEP'],
    ['ELIMINATE_DUPLICATE_FUNCTIONS', [0, 1], 'Duplicate function elimination for wasm is handled automatically by binaryen'],
    ['ELIMINATE_DUPLICATE_FUNCTIONS_DUMP_EQUIVALENT_FUNCTIONS', [0], 'Duplicate function elimination for wasm is handled automatically by binaryen'],
    ['ELIMINATE_DUPLICATE_FUNCTIONS_PASSES', [5], 'Duplicate function elimination for wasm is handled automatically by binaryen'],
    ['WASM_OBJECT_FILES', [1], 'For LTO, use -flto or -fto=thin instead. Otherwise, Wasm object files are the default'],
    ['TOTAL_MEMORY', 'INITIAL_MEMORY'],
    ['WASM_MEM_MAX', 'MAXIMUM_MEMORY'],
    ['BINARYEN_MEM_MAX', 'MAXIMUM_MEMORY'],
    ['BINARYEN_PASSES', [''], 'Use BINARYEN_EXTRA_PASSES to add additional passes'],
    ['SWAPPABLE_ASM_MODULE', [0], 'Fully swappable asm modules are no longer supported'],
    ['ASM_JS', [1], 'asm.js output is not supported anymore'],
    ['FINALIZE_ASM_JS', [0, 1], 'asm.js output is not supported anymore'],
    ['ASYNCIFY_WHITELIST', 'ASYNCIFY_ONLY'],
    ['ASYNCIFY_BLACKLIST', 'ASYNCIFY_REMOVE'],
    ['EXCEPTION_CATCHING_WHITELIST', 'EXCEPTION_CATCHING_ALLOWED'],
    ['SEPARATE_ASM', [0], 'Separate asm.js only made sense for fastcomp with asm.js output'],
    ['SEPARATE_ASM_MODULE_NAME', [''], 'Separate asm.js only made sense for fastcomp with asm.js output'],
    ['FAST_UNROLLED_MEMCPY_AND_MEMSET', [0, 1], 'The wasm backend implements memcpy/memset in C'],
    ['DOUBLE_MODE', [0, 1], 'The wasm backend always implements doubles normally'],
    ['PRECISE_F32', [0, 1, 2], 'The wasm backend always implements floats normally'],
    ['ALIASING_FUNCTION_POINTERS', [0, 1], 'The wasm backend always uses a single index space for function pointers, in a single Table'],
    ['AGGRESSIVE_VARIABLE_ELIMINATION', [0, 1], 'Wasm ignores asm.js-specific optimization flags'],
    ['SIMPLIFY_IFS', [1], 'Wasm ignores asm.js-specific optimization flags'],
    ['DEAD_FUNCTIONS', [[]], 'The wasm backend does not support dead function removal'],
    ['WASM_BACKEND', [-1], 'Only the wasm backend is now supported (note that setting it as -s has never been allowed anyhow)'],
    ['EXPORT_BINDINGS', [0, 1], 'No longer needed'],
    ['RUNNING_JS_OPTS', [0], 'Fastcomp cared about running JS which could alter asm.js validation, but not upstream'],
    ['EXPORT_FUNCTION_TABLES', [0], 'No longer needed'],
    ['BINARYEN_SCRIPTS', [''], 'No longer needed'],
    ['WARN_UNALIGNED', [0, 1], 'No longer needed'],
    ['ASM_PRIMITIVE_VARS', [[]], 'No longer needed'],
    ['WORKAROUND_IOS_9_RIGHT_SHIFT_BUG', [0], 'Wasm2JS does not support iPhone 4s, iPad 2, iPad 3, iPad Mini 1, Pod Touch 5 (devices with end-of-life at iOS 9.3.5) and older'],
    ['RUNTIME_FUNCS_TO_IMPORT', [[]], 'No longer needed'],
    ['LIBRARY_DEPS_TO_AUTOEXPORT', [[]], 'No longer needed'],
    ['EMIT_EMSCRIPTEN_METADATA', [0], 'No longer supported'],
    ['SHELL_FILE', [''], 'No longer supported'],
    ['LLD_REPORT_UNDEFINED', [1], 'Disabling is no longer supported'],
    ['MEM_INIT_METHOD', [0], 'No longer supported'],
    ['USE_PTHREADS', [0, 1], 'No longer needed. Use -pthread instead'],
    ['USES_DYNAMIC_ALLOC', [1], 'No longer supported. Use -sMALLOC=none'],
    ['REVERSE_DEPS', ['auto', 'all', 'none'], 'No longer needed'],
    ['RUNTIME_LOGGING', 'RUNTIME_DEBUG'],
    ['MIN_EDGE_VERSION', [0x7FFFFFFF], 'No longer supported'],
    ['MIN_IE_VERSION', [0x7FFFFFFF], 'No longer supported'],
    ['WORKAROUND_OLD_WEBGL_UNIFORM_UPLOAD_IGNORED_OFFSET_BUG', [0], 'No longer supported'],
    ['AUTO_ARCHIVE_INDEXES', [0, 1], 'No longer needed'],
    ['USE_ES6_IMPORT_META', [1], 'Disabling is no longer supported'],
    ['EXTRA_EXPORTED_RUNTIME_METHODS', [[]], 'No longer supported, use EXPORTED_RUNTIME_METHODS'],
    ['SUPPORT_ERRNO', [0], 'No longer supported'],
    ['DEMANGLE_SUPPORT', [0], 'No longer supported'],
    ['MAYBE_WASM2JS', [0], 'No longer supported (use -sWASM=2)'],
    ['HEADLESS', [0], 'No longer supported, use headless browsers or Node.js with JSDOM'],
    ['USE_OFFSET_COVERTER', [0], 'No longer supported, not needed with modern v8 versions'],
    ['ASYNCIFY_LAZY_LOAD_CODE', [0], 'No longer supported'],
    ['USE_WEBGPU', [0], 'No longer supported; replaced by --use-port=emdawnwebgpu, which implements a newer (but incompatible) version of webgpu.h - see tools/ports/emdawnwebgpu.py'],
    ['PROXY_TO_WORKER', [0], 'No longer supported'],
]

user_settings: dict[str, str] = {}


def default_setting(name, new_default):
  if name not in user_settings:
    setattr(settings, name, new_default)


class SettingsManager:
  attrs: dict[str, Any] = {}
  defaults: dict[str, tuple] = {}
  types: dict[str, Any] = {}
  allowed_settings: set[str] = set()
  legacy_settings: dict[str, tuple] = {}
  alt_names: dict[str, str] = {}
  internal_settings: set[str] = set()

  def __init__(self):
    self.attrs.clear()
    self.legacy_settings.clear()
    self.defaults.clear()
    self.alt_names.clear()
    self.internal_settings.clear()
    self.allowed_settings.clear()

    # Load the JS defaults into python.
    def read_js_settings(filename, attrs):
      with open(filename) as fh:
        settings = fh.read()
      # Use a bunch of regexs to convert the file from JS to python
      # TODO(sbc): This is kind hacky and we should probably convert
      # this file in format that python can read directly (since we
      # no longer read this file from JS at all).
      settings = settings.replace('//', '#')
      settings = re.sub(r'var ([\w\d]+)', r'attrs["\1"]', settings)
      settings = re.sub(r'=\s+false\s*;', '= False', settings)
      settings = re.sub(r'=\s+true\s*;', '= True', settings)
      exec(settings, {'attrs': attrs})

    internal_attrs = {}
    read_js_settings(path_from_root('src/settings.js'), self.attrs)
    read_js_settings(path_from_root('src/settings_internal.js'), internal_attrs)
    self.attrs.update(internal_attrs)
    self.infer_types()

    strict_override = False
    if 'EMCC_STRICT' in os.environ:
      strict_override = int(os.environ.get('EMCC_STRICT'))

    # Special handling for LEGACY_SETTINGS.  See src/setting.js for more
    # details
    for legacy in LEGACY_SETTINGS:
      if len(legacy) == 2:
        name, new_name = legacy
        self.legacy_settings[name] = (None, 'setting renamed to ' + new_name)
        self.alt_names[name] = new_name
        self.alt_names[new_name] = name
        default_value = self.attrs[new_name]
      else:
        name, fixed_values, err = legacy
        self.legacy_settings[name] = (fixed_values, err)
        default_value = fixed_values[0]
      assert name not in self.attrs, 'legacy setting (%s) cannot also be a regular setting' % name
      if not strict_override:
        self.attrs[name] = default_value

    self.internal_settings.update(internal_attrs.keys())
    # Stash a deep copy of all settings in self.defaults.  This allows us to detect which settings
    # have local mods.
    self.defaults.update(copy.deepcopy(self.attrs))

    if strict_override:
      self.attrs['STRICT'] = strict_override

  def infer_types(self):
    for key, value in self.attrs.items():
      self.types[key] = type(value)

  def dict(self):
    return self.attrs

  def external_dict(self, skip_keys={}): # noqa
    external_settings = {}
    for key, value in self.dict().items():
      if value != self.defaults.get(key) and key not in INTERNAL_SETTINGS and key not in skip_keys:
        external_settings[key] = value # noqa: PERF403
    if not self.attrs['STRICT']:
      # When not running in strict mode we also externalize all legacy settings
      # (Since the external tools do process LEGACY_SETTINGS themselves)
      for key in self.legacy_settings:
        external_settings[key] = self.attrs[key]
    return external_settings

  def keys(self):
    return self.attrs.keys()

  def limit_settings(self, allowed):
    self.allowed_settings.clear()
    if allowed:
      self.allowed_settings.update(allowed)

  def __getattr__(self, attr):
    if self.allowed_settings:
      assert attr in self.allowed_settings, f"internal error: attempt to read setting '{attr}' while in limited settings mode"

    if attr in self.attrs:
      return self.attrs[attr]
    else:
      raise AttributeError(f"no such setting: '{attr}'")

  def __setattr__(self, name, value):
    if self.allowed_settings:
      assert name in self.allowed_settings, f"internal error: attempt to write setting '{name}' while in limited settings mode"

    if name == 'STRICT' and value:
      for a in self.legacy_settings:
        self.attrs.pop(a, None)

    if name in self.legacy_settings:
      # TODO(sbc): Rather then special case this we should have STRICT turn on the
      # legacy-settings warning below
      if self.attrs['STRICT']:
        exit_with_error('legacy setting used in strict mode: %s', name)
      fixed_values, error_message = self.legacy_settings[name]
      if fixed_values and value not in fixed_values:
        exit_with_error(f'invalid command line setting `-s{name}={value}`: {error_message}')
      diagnostics.warning('legacy-settings', 'use of legacy setting: %s (%s)', name, error_message)

    if name in self.alt_names:
      alt_name = self.alt_names[name]
      self.attrs[alt_name] = value

    if name not in self.attrs:
      msg = "Attempt to set a non-existent setting: '%s'\n" % name
      valid_keys = set(self.attrs.keys()).difference(self.internal_settings)
      suggestions = difflib.get_close_matches(name, valid_keys)
      suggestions = [s for s in suggestions if s not in self.legacy_settings]
      suggestions = ', '.join(suggestions)
      if suggestions:
        msg += ' - did you mean one of %s?\n' % suggestions
      msg += " - perhaps a typo in emcc's  -sX=Y  notation?\n"
      msg += ' - (see src/settings.js for valid values)'
      exit_with_error(msg)

    self.check_type(name, value)
    self.attrs[name] = value

  def check_type(self, name, value):
    # These settings have a variable type so cannot be easily type checked.
    if name in ('EXECUTABLE', 'SUPPORT_LONGJMP', 'PTHREAD_POOL_SIZE', 'SEPARATE_DWARF', 'LTO', 'MODULARIZE'):
      return
    expected_type = self.types.get(name)
    if not expected_type:
      return
    # Allow integers 1 and 0 for type `bool`
    if expected_type == bool:
      if value in (1, 0):
        value = bool(value)
      if value in ('True', 'False', 'true', 'false'):
        exit_with_error(f'attempt to set `{name}` to `{value}`; use 1/0 to set boolean settings')
    if type(value) is not expected_type:
      exit_with_error(f'setting `{name}` expects `{expected_type.__name__}` but got `{type(value).__name__}`')

  def __getitem__(self, key):
    return self.attrs[key]

  def __setitem__(self, key, value):
    self.attrs[key] = value

  def backup(self):
    return copy.deepcopy(self.attrs)

  def restore(self, previous):
    self.attrs.update(previous)


settings = SettingsManager()
