// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:async/async.dart';
import 'package:dwds/src/debugging/metadata/module_metadata.dart';
import 'package:dwds/src/readers/asset_reader.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;

/// A provider of metadata in which data is collected through DDC outputs.
class MetadataProvider {
  final AssetReader _assetReader;
  final _logger = Logger('MetadataProvider');
  final String entrypoint;
  final Set<String> _libraries = {};
  final Map<String, String> _scriptToModule = {};
  final Map<String, String> _moduleToSourceMap = {};
  final Map<String, String> _modulePathToModule = {};
  final Map<String, String> _moduleToModulePath = {};
  final Map<String, Set<String>> _moduleToLibraries = {};
  final Map<String, List<String>> _scripts = {};
  final _metadataMemoizer = AsyncMemoizer();

  /// Implicitly imported libraries in any DDC component.
  ///
  /// Currently dart_sdk module does not come with the metadata.
  /// To allow evaluation of expressions that use libraries and
  /// types from the SDK (such as a dart Type object), add the
  /// metadata for dart_sdk manually.
  ///
  /// TODO: Generate sdk module metadata to be consumed by debugger.
  /// Issue: https://github.com/dart-lang/sdk/issues/45477
  List<String> get sdkLibraries => const [
    'dart:_runtime',
    'dart:_debugger',
    'dart:_foreign_helper',
    'dart:_interceptors',
    'dart:_internal',
    'dart:_isolate_helper',
    'dart:_js_helper',
    'dart:_js_primitives',
    'dart:_metadata',
    'dart:_native_typed_data',
    'dart:_rti',
    'dart:async',
    'dart:collection',
    'dart:convert',
    'dart:core',
    'dart:developer',
    'dart:io',
    'dart:isolate',
    'dart:js',
    'dart:js_util',
    'dart:math',
    'dart:typed_data',
    'dart:indexed_db',
    'dart:html',
    'dart:html_common',
    'dart:svg',
    'dart:web_audio',
    'dart:web_gl',
    'dart:ui',
  ];

  MetadataProvider(this.entrypoint, this._assetReader);

  /// A sound null safety mode for the whole app.
  ///
  /// All libraries have to agree on null safety mode.
  @Deprecated('Only sound null safety is supported as of Dart 3.0')
  Future<bool> get soundNullSafety async {
    await _initialize();
    return true;
  }

  /// A list of all libraries in the Dart application.
  ///
  /// Example:
  ///
  ///  [
  ///     dart:web_gl,
  ///     dart:math,
  ///     org-dartlang-app:///web/main.dart
  ///  ]
  ///
  Future<List<String>> get libraries async {
    await _initialize();
    return _libraries.toList();
  }

  /// A map of library uri to dart scripts.
  ///
  /// Example:
  ///
  /// {
  ///   org-dartlang-app:///web/main.dart :
  ///   { web/main.dart  }
  /// }
  ///
  Future<Map<String, List<String>>> get scripts async {
    await _initialize();
    return _scripts;
  }

  /// A map of script to containing module.
  ///
  /// Example:
  ///
  /// {
  ///   org-dartlang-app:///web/main.dart :
  ///   web/main
  /// }
  Future<Map<String, String>> get scriptToModule async {
    await _initialize();
    return _scriptToModule;
  }

  /// A map of module name to source map path.
  ///
  /// Example:
  ///
  /// {
  ///   org-dartlang-app:///web/main.dart :
  ///   web/main.ddc.js.map
  /// }
  Future<Map<String, String>> get moduleToSourceMap async {
    await _initialize();
    return _moduleToSourceMap;
  }

  /// A map of module path to module name.
  ///
  /// Example:
  ///
  /// {
  ///   web/main.ddc.js :
  ///   web/main
  /// }
  Future<Map<String, String>> get modulePathToModule async {
    await _initialize();
    return _modulePathToModule;
  }

  /// A map of module to module path.
  ///
  /// Example:
  ///
  /// {
  ///   web/main
  ///   web/main.ddc.js :
  /// }
  Future<Map<String, String>> get moduleToModulePath async {
    await _initialize();
    return _moduleToModulePath;
  }

  /// A list of module ids.
  ///
  /// Example:
  ///
  /// [
  ///   web/main,
  ///   web/foo/bar
  /// ]
  Future<List<String>> get modules async {
    await _initialize();
    return _moduleToModulePath.keys.toList();
  }

  /// Compute metadata information after reading the metadata contents and
  /// return a map from module names to their [ModuleMetadata].
  Future<Map<String, ModuleMetadata>> _processMetadata() async {
    final modules = <String, ModuleMetadata>{};
    // The merged metadata resides next to the entrypoint.
    // Assume that <name>.bootstrap.js has <name>.ddc_merged_metadata
    if (entrypoint.endsWith('.bootstrap.js')) {
      _logger.info('Loading debug metadata...');
      final serverPath = entrypoint.replaceAll(
        '.bootstrap.js',
        '.ddc_merged_metadata',
      );
      final merged = await _assetReader.metadataContents(serverPath);
      if (merged != null) {
        for (final contents in merged.split('\n')) {
          try {
            if (contents.isEmpty ||
                contents.startsWith('// intentionally empty:')) {
              continue;
            }
            final moduleJson = json.decode(contents);
            final metadata = ModuleMetadata.fromJson(
              moduleJson as Map<String, dynamic>,
            );
            final moduleName = metadata.name;
            modules[moduleName] = metadata;
            _logger.fine('Loaded debug metadata for module: $moduleName');
          } catch (e) {
            _logger.warning('Failed to read metadata: $e');
            rethrow;
          }
        }
      }
    }
    return modules;
  }

  /// Process all metadata, including SDK metadata, and compute caches once.
  Future<void> _initialize() async {
    await _metadataMemoizer.runOnce(() async {
      final metadata = await _processMetadata();
      _addSdkMetadata();
      metadata.values.forEach(_addMetadata);
    });
  }

  /// Given a map of hot reloaded modules mapped to their respective libraries,
  /// determines deleted and invalidated libraries and modules, invalidates them
  /// in any caches, and recomputes the necessary information.
  ///
  /// Returns a [ModifiedModuleReport] that can be used to invalidate other
  /// caches after a hot reload.
  Future<ModifiedModuleReport> reinitializeAfterHotReload(
    Map<String, List> reloadedModulesToLibraries,
  ) async {
    final modules = await _processMetadata();
    final invalidatedLibraries = <String>{};
    void invalidateLibrary(String libraryImportUri) {
      invalidatedLibraries.add(libraryImportUri);
      _libraries.remove(libraryImportUri);
      _scriptToModule.remove(libraryImportUri);
      _scripts[libraryImportUri]?.forEach(_scriptToModule.remove);
      _scripts.remove(libraryImportUri);
    }

    final deletedModules = <String>{};
    for (final module in _moduleToLibraries.keys) {
      final deletedModule = !modules.containsKey(module);
      final invalidatedModule = reloadedModulesToLibraries.containsKey(module);
      assert(!(deletedModule && invalidatedModule));
      // If the module was either deleted or reloaded, invalidate all previous
      // information both about the module and its libraries.
      if (deletedModule || invalidatedModule) {
        _modulePathToModule.remove(module);
        _moduleToLibraries[module]?.forEach(invalidateLibrary);
        _moduleToModulePath.remove(module);
        _moduleToSourceMap.remove(module);
      }
      if (deletedModule) deletedModules.add(module);
    }
    final reloadedModules = <String>{};
    final reloadedLibraries = <String>{};
    for (final module in reloadedModulesToLibraries.keys) {
      reloadedModules.add(module);
      reloadedLibraries.addAll(
        reloadedModulesToLibraries[module]!.cast<String>(),
      );
      _addMetadata(modules[module]!);
    }
    // The libraries that are removed from the program are those that we
    // invalidated but were never added again.
    final deletedLibraries =
        invalidatedLibraries
            .where((library) => !_libraries.contains(library))
            .toSet();
    return ModifiedModuleReport(
      deletedModules: deletedModules,
      deletedLibraries: deletedLibraries,
      reloadedModules: reloadedModules,
      reloadedLibraries: reloadedLibraries,
    );
  }

  void _addMetadata(ModuleMetadata metadata) {
    final modulePath = stripLeadingSlashes(metadata.moduleUri);
    final sourceMapPath = stripLeadingSlashes(metadata.sourceMapUri);
    final moduleName = metadata.name;

    _moduleToSourceMap[moduleName] = sourceMapPath;
    _modulePathToModule[modulePath] = moduleName;
    _moduleToModulePath[moduleName] = modulePath;

    final moduleLibraries = <String>{};
    for (final library in metadata.libraries.values) {
      if (library.importUri.startsWith('file:/')) {
        throw AbsoluteImportUriException(library.importUri);
      }
      moduleLibraries.add(library.importUri);
      _libraries.add(library.importUri);
      _scripts[library.importUri] = [];

      _scriptToModule[library.importUri] = moduleName;
      for (final path in library.partUris) {
        // Parts in metadata are relative to the library Uri directory.
        final partPath = p.url.join(p.dirname(library.importUri), path);
        _scripts[library.importUri]!.add(partPath);
        _scriptToModule[partPath] = moduleName;
      }
    }
    _moduleToLibraries[moduleName] = moduleLibraries;
  }

  void _addSdkMetadata() {
    final moduleName = 'dart_sdk';

    for (final lib in sdkLibraries) {
      _libraries.add(lib);
      _scripts[lib] = [];
      // TODO(srujzs): It feels weird that we add this mapping to only this map
      // and not any of the other module maps. We should maybe handle this
      // differently. This will become relevant if we ever support hot reload
      // for the Dart SDK.
      _scriptToModule[lib] = moduleName;
    }
  }
}

class AbsoluteImportUriException implements Exception {
  final String importUri;
  AbsoluteImportUriException(this.importUri);

  @override
  String toString() => "AbsoluteImportUriError: '$importUri'";
}

/// Computed after a hot reload using
/// [MetadataProvider.reinitializeAfterHotReload], represents the modules and
/// libraries in the program that were deleted, reloaded, and therefore,
/// modified.
///
/// Used to recompute caches throughout DWDS.
class ModifiedModuleReport {
  /// Module names that are no longer in the program.
  final Set<String> deletedModules;

  /// Library uris that are no longer in the program.
  final Set<String> deletedLibraries;

  /// Module names that were loaded during the hot reload.
  final Set<String> reloadedModules;

  /// Library uris that were loaded during the hot reload.
  final Set<String> reloadedLibraries;

  /// Module names that were either removed or modified, including additions.
  final Set<String> modifiedModules;

  /// Library uris that were either removed or modified, including additions.
  final Set<String> modifiedLibraries;
  ModifiedModuleReport({
    required this.deletedModules,
    required this.deletedLibraries,
    required this.reloadedModules,
    required this.reloadedLibraries,
  }) : modifiedModules = deletedModules.union(reloadedModules),
       modifiedLibraries = deletedLibraries.union(reloadedLibraries);
}
