// 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:dwds/src/debugging/dart_runtime_debugger.dart';
import 'package:dwds/src/debugging/metadata/provider.dart';
import 'package:dwds/src/loaders/strategy.dart';
import 'package:dwds/src/readers/asset_reader.dart';
import 'package:dwds/src/services/expression_compiler.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart';

String removeJsExtension(String path) =>
    path.endsWith('.js') ? p.withoutExtension(path) : path;

String addJsExtension(String path) => '$path.js';

// web/main.ddc.js -> main.ddc.js
// packages/test/test.dart.js -> packages/test/test.dart.js
String stripTopLevelDirectory(String path) =>
    path.startsWith('packages') ? path : path.split('/').skip(1).join('/');

/// JavaScript snippet to determine the base URL of the current path.
const _baseUrlScript = '''
var baseUrl = (function () {
  // Attempt to detect --precompiled mode for tests, and set the base url
  // appropriately, otherwise set it to '/'.
  var pathParts = location.pathname.split("/");
  if (pathParts[0] == "") {
    pathParts.shift();
  }
  if (pathParts.length > 1 && pathParts[1] == "test") {
    return "/" + pathParts.slice(0, 2).join("/") + "/";
  }
  // Attempt to detect base url using <base href> html tag
  // base href should start and end with "/"
  if (typeof document !== 'undefined') {
    var el = document.getElementsByTagName('base');
    if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute
    ("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){
      return el[0].getAttribute("href");
    }
  }
  // return default value
  return "/";
}());
''';

/// A load strategy for the require-js module system.
class RequireStrategy extends LoadStrategy {
  @override
  final ReloadConfiguration reloadConfiguration;

  final String _requireDigestsPath = r'$requireDigestsPath';

  /// Returns a map of module name to corresponding server path (excluding .js)
  /// for the provided Dart application entrypoint.
  ///
  /// For example:
  ///
  ///   web/main -> main.ddc
  ///   packages/path/path -> packages/path/path.ddc
  ///
  final Future<Map<String, String>> Function(MetadataProvider metadataProvider)
      _moduleProvider;

  /// Returns a map of module name to corresponding digest value.
  ///
  /// For example:
  ///
  ///   web/main -> 8363b363f74b41cac955024ab8b94a3f
  ///   packages/path/path -> d348c2a4647e998011fe305f74f22961
  ///
  final Future<Map<String, String>> Function(MetadataProvider metadataProvider)
      _digestsProvider;

  /// Returns the module for the corresponding server path.
  ///
  /// For example:
  ///
  /// /packages/path/path.ddc.js -> packages/path/path
  ///
  final Future<String?> Function(MetadataProvider provider, String sourcePath)
      _moduleForServerPath;

  /// Returns the server path for the provided module.
  ///
  /// For example:
  ///
  ///   web/main -> main.ddc.js
  ///
  final Future<String?> Function(MetadataProvider provider, String module)
      _serverPathForModule;

  /// Returns the source map path for the provided module.
  ///
  /// For example:
  ///
  ///   web/main -> main.ddc.js.map
  ///
  final Future<String?> Function(MetadataProvider provider, String module)
      _sourceMapPathForModule;

  /// Returns the server path for the app uri.
  ///
  /// For example:
  ///
  ///   org-dartlang-app://web/main.dart -> main.dart
  ///
  /// Will return `null` if the provided uri is not
  /// an app URI.
  final String? Function(String appUri) _serverPathForAppUri;

  /// Returns a map from module id to module info.
  ///
  /// For example:
  ///
  ///   web/main -> {main.ddc.full.dill, main.ddc.dill}
  ///
  final Future<Map<String, ModuleInfo>> Function(
    MetadataProvider metadataProvider,
  ) _moduleInfoForProvider;

  @override
  BuildSettings get buildSettings => _buildSettings;
  final BuildSettings _buildSettings;

  RequireStrategy(
    this.reloadConfiguration,
    this._moduleProvider,
    this._digestsProvider,
    this._moduleForServerPath,
    this._serverPathForModule,
    this._sourceMapPathForModule,
    this._serverPathForAppUri,
    this._moduleInfoForProvider,
    AssetReader assetReader,
    this._buildSettings,
  ) : super(assetReader);

  @override
  Handler get handler => (request) async {
        if (request.url.path.endsWith(_requireDigestsPath)) {
          final entrypoint = request.url.queryParameters['entrypoint'];
          if (entrypoint == null) return Response.notFound('${request.url}');
          final metadataProvider =
              metadataProviderFor(request.url.queryParameters['entrypoint']!);
          final digests = await _digestsProvider(metadataProvider);
          return Response.ok(json.encode(digests));
        }
        return Response.notFound('${request.url}');
      };

  @override
  String get id => 'require-js';

  @override
  String get moduleFormat => 'amd';

  @override
  String get loadLibrariesModule => 'require.js';

  @override
  String get loadModuleSnippet => 'require';

  @override
  late final DartRuntimeDebugger dartRuntimeDebugger = DartRuntimeDebugger(
    loadStrategy: this,
    useLibraryBundleExpression: false,
  );

  /// Require JS config for ddc.
  ///
  /// Sets the base url to `/` so that all modules can be loaded using absolute
  /// paths which simplifies a lot of scenarios.
  ///
  /// Sets the timeout for loading modules to infinity (0).
  ///
  /// Sets up the custom module paths.
  ///
  /// Adds error handler code for require.js which requests a `.errors` file for
  /// any failed module, and logs it to the console.
  String get _requireJsConfig => '''
$_baseUrlScript;
require.config({
    baseUrl: baseUrl,
    waitSeconds: 0,
    paths: modulePaths
});
const modulesGraph = new Map();
requirejs.onResourceLoad = function (context, map, depArray) {
  const name = map.name;
  const depNameArray = depArray.map((dep) => dep.name);
  if (modulesGraph.has(name)) {
    var previousDeps = modulesGraph.get(name);
    var changed = previousDeps.length != depNameArray.length;
    changed = changed || depNameArray.some(function(depName) {
      return !previousDeps.includes(depName);
    });
    if (changed) {
      console.warn("Dependencies graph change for module '" + name + "' detected. " +
        "Dependencies was [" + previousDeps + "], now [" +  depNameArray.map((depName) => depName) +"]. " +
        "Page can't be hot-reloaded, firing full page reload.");
      window.location.reload();
    }
  } else {
    modulesGraph.set(name, []);
    for (const depName of depNameArray) {
      if (!\$requireLoader.moduleParentsGraph.has(depName)) {
        \$requireLoader.moduleParentsGraph.set(depName, []);
      }
      \$requireLoader.moduleParentsGraph.get(depName).push(name);
      modulesGraph.get(name).push(depName);
    }
  }
};
''';

  @override
  Future<String> bootstrapFor(String entrypoint) async =>
      await _requireLoaderSetup(entrypoint) + _requireJsConfig;

  @override
  String loadClientSnippet(String clientScript) =>
      'window.\$requireLoader.forceLoadModule("$clientScript");\n';

  Future<String> _requireLoaderSetup(String entrypoint) async {
    final metadataProvider = metadataProviderFor(entrypoint);
    final modulePaths = await _moduleProvider(metadataProvider);
    final moduleNames =
        modulePaths.map((key, value) => MapEntry<String, String>(value, key));
    return '''
$_baseUrlScript
let modulePaths = ${const JsonEncoder.withIndent(" ").convert(modulePaths)};
let moduleNames = ${const JsonEncoder.withIndent(" ").convert(moduleNames)};
if(!window.\$requireLoader) {
   window.\$requireLoader = {
     digestsPath: '$_requireDigestsPath?entrypoint=$entrypoint',
     // Used in package:build_runner/src/server/build_updates_client/hot_reload_client.dart
     moduleParentsGraph: new Map(),
     forceLoadModule: function (modulePath, callback, onError) {
       let moduleName = moduleNames[modulePath];
       if (moduleName == null) {
         moduleName = modulePath;
       }
       requirejs.undef(moduleName);
       try{
        requirejs([moduleName], function() {
          if (typeof callback != 'undefined') {
            callback();
          }
        });
       } catch (error) {
        if (typeof onError != 'undefined') {
          onError(error);
        }else{
          throw(error);
        }
       }
     },
     getModuleLibraries: null, // set up by _initializeTools
   };
}
''';
  }

  @override
  Future<String?> moduleForServerPath(String entrypoint, String serverPath) {
    final metadataProvider = metadataProviderFor(entrypoint);
    return _moduleForServerPath(metadataProvider, serverPath);
  }

  @override
  Future<String?> serverPathForModule(String entrypoint, String module) {
    final metadataProvider = metadataProviderFor(entrypoint);
    return _serverPathForModule(metadataProvider, module);
  }

  @override
  Future<String?> sourceMapPathForModule(String entrypoint, String module) {
    final metadataProvider = metadataProviderFor(entrypoint);
    return _sourceMapPathForModule(metadataProvider, module);
  }

  @override
  String? serverPathForAppUri(String appUri) => _serverPathForAppUri(appUri);

  @override
  Future<Map<String, ModuleInfo>> moduleInfoForEntrypoint(String entrypoint) =>
      _moduleInfoForProvider(metadataProviderFor(entrypoint));

  @override
  String? g3RelativePath(String absolutePath) => null;
}
