// Copyright 2020 The Dart Authors. 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 'dart:io';
import 'dart:typed_data';

import 'package:path/path.dart' as p;

import 'dartdevc_bootstrap_amd.dart';
import 'frontend_server_client.dart';
import 'shared.dart';

/// Wraps a [FrontendServerClient] with opinionated web specific functionality,
/// and provides some typical defaults.
///
/// Loads into memory the [CompileResult]s from each [compile] call, and
/// provides access to the up to date sources and source maps.
///
/// Also has the ability to create a bootstrap file for the current entrypoint.
class DartDevcFrontendServerClient implements FrontendServerClient {
  final FrontendServerClient _frontendServerClient;

  final _assets = <String, Uint8List>{};
  final String _entrypoint;

  /// The last compile, or `null` once it has been accepted or rejected.
  CompileResult? _lastResult;

  /// The bootstrap js contents, provided in [_assets] at
  /// the path `${_entrypointModule}.js`.
  ///
  /// This is `null` if the module format is not supported for bootstrapping.
  final String? _bootstrapJs;

  /// The generated main module js contents, provided in [_assets] at
  /// the path `${_entrypointModule}.bootstrap.js`.
  ///
  /// This is `null` if the module format is not supported for bootstrapping.
  final String? _mainModuleJs;

  DartDevcFrontendServerClient._(
      this._frontendServerClient, this._entrypoint, String moduleFormat)
      : _bootstrapJs = moduleFormat == 'amd'
            ? generateAmdBootstrapScript(
                requireUrl: 'require.js',
                mapperUrl: 'dart_stack_trace_mapper.js',
                entrypoint: _entrypoint)
            : null,
        _mainModuleJs = moduleFormat == 'amd'
            ? generateAmdMainModule(entrypoint: _entrypoint)
            : null {
    _resetAssets();
  }

  static Future<DartDevcFrontendServerClient> start(
    String entrypoint,
    String outputDillPath, {
    String dartdevcModuleFormat = 'amd',
    bool debug = false,
    bool enableHttpUris = false,
    List<String> fileSystemRoots = const [], // For `fileSystemScheme` uris,
    String fileSystemScheme =
        'org-dartlang-root', // Custom scheme for virtual `fileSystemRoots`.
    String? frontendServerPath, // Defaults to the snapshot in the sdk.
    String packagesJson = '.dart_tool/package_config.json',
    String? platformKernel, // Defaults to the dartdevc platform from the sdk.
    String? sdkRoot, // Defaults to the current SDK root.
    bool verbose = false,
    bool printIncrementalDependencies = true,
  }) async {
    var feServer = await FrontendServerClient.start(
      entrypoint,
      outputDillPath,
      platformKernel ?? _dartdevcPlatformKernel,
      dartdevcModuleFormat: dartdevcModuleFormat,
      debug: debug,
      enableHttpUris: enableHttpUris,
      fileSystemRoots: fileSystemRoots,
      fileSystemScheme: fileSystemScheme,
      frontendServerPath: frontendServerPath,
      packagesJson: packagesJson,
      sdkRoot: sdkRoot,
      target: 'dartdevc',
      verbose: verbose,
    );
    return DartDevcFrontendServerClient._(
        feServer, Uri.parse(entrypoint).path, dartdevcModuleFormat);
  }

  /// Returns the current bytes for the asset at [path].
  ///
  /// The [path] should be exactly as it appears in the
  /// [CompileResult.jsManifestOutput] file.
  ///
  /// **Note**: Assets are not updated until `accept` is called after a
  /// successful compile. They are not updated if `reject` is called.
  ///
  /// Returns `null` if no asset exists at [path].
  ///
  /// In addition to any DDC compiled assets, this serves
  Uint8List? assetBytes(String path) => _assets[path];

  /// The contents of a JS file capable of bootstrapping the current app.
  ///
  /// TODO: implement
  String bootstrapJs() => throw UnimplementedError();

  /// Updates [_assets] for [result].
  void _updateAssets(CompileResult result) {
    if (result.dillOutput == null) {
      return;
    }
    final manifest =
        jsonDecode(File(result.jsManifestOutput!).readAsStringSync())
            as Map<String, dynamic>;
    final sourceBytes = File(result.jsSourcesOutput!).readAsBytesSync();
    final sourceMapBytes = File(result.jsSourceMapsOutput!).readAsBytesSync();

    for (var entry in manifest.entries) {
      var metadata = entry.value as Map<String, dynamic>;
      var sourceOffsets = metadata['code'] as List;
      _assets[entry.key] =
          sourceBytes.sublist(sourceOffsets[0] as int, sourceOffsets[1] as int);
      var sourceMapOffsets = metadata['sourcemap'] as List;
      _assets['${entry.key}.map'] = sourceMapBytes.sublist(
          sourceMapOffsets[0] as int, sourceMapOffsets[1] as int);
    }
  }

  @override
  Future<CompileResult> compile([List<Uri>? invalidatedUris]) async {
    return _lastResult = await _frontendServerClient.compile(invalidatedUris);
  }

  @override
  Future<CompileResult> compileExpression({
    required String expression,
    required List<String> definitions,
    required bool isStatic,
    required String klass,
    required String libraryUri,
    required List<String> typeDefinitions,
  }) =>
      throw UnsupportedError(
          'Use `compileExpressionToJs` for dartdevc based clients');

  @override
  Future<CompileResult> compileExpressionToJs({
    required String expression,
    required int column,
    required Map<String, String> jsFrameValues,
    required Map<String, String> jsModules,
    required String libraryUri,
    required int line,
    required String moduleName,
  }) =>
      _frontendServerClient.compileExpressionToJs(
          expression: expression,
          column: column,
          jsFrameValues: jsFrameValues,
          jsModules: jsModules,
          libraryUri: libraryUri,
          line: line,
          moduleName: moduleName);

  @override
  void accept() {
    _frontendServerClient.accept();
    if (_lastResult != null) _updateAssets(_lastResult!);
    _lastResult = null;
  }

  @override
  Future<void> reject() async {
    await _frontendServerClient.reject();
    _lastResult = null;
  }

  @override
  void reset() {
    _frontendServerClient.reset();
    _resetAssets();
  }

  @override
  bool kill({ProcessSignal processSignal = ProcessSignal.sigterm}) =>
      _frontendServerClient.kill();

  @override
  Future<int> shutdown() => _frontendServerClient.shutdown();

  /// Clears any previously compiled assets and adds the bootstrap modules as
  /// assets if available.
  void _resetAssets() {
    _assets.clear();
    var bootstrapJs = _bootstrapJs;
    if (bootstrapJs != null) {
      _assets['$_entrypoint.js'] = Uint8List.fromList(utf8.encode(bootstrapJs));
    }
    var mainModuleJs = _mainModuleJs;
    if (mainModuleJs != null) {
      _assets['$_entrypoint.bootstrap.js'] =
          Uint8List.fromList(utf8.encode(mainModuleJs));
    }
  }
}

final _dartdevcPlatformKernel =
    p.toUri(p.join(sdkDir, 'lib', '_internal', 'ddc_sdk.dill')).toString();
