// 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 'dart:io';

import 'package:dwds/src/readers/asset_reader.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;

/// A reader for Dart sources and related source maps provided by the Frontend
/// Server.
class FrontendServerAssetReader implements AssetReader {
  final _logger = Logger('FrontendServerAssetReader');
  final File _mapOriginal;
  final File _mapIncremental;
  final File _jsonOriginal;
  final File _jsonIncremental;
  final String _packageRoot;
  final Future<PackageConfig> _packageConfig;
  final String _basePath;

  /// Map of Dart module server path to source map contents.
  final _mapContents = <String, String>{};

  bool _haveReadOriginals = false;

  /// Creates a [FrontendServerAssetReader].
  ///
  /// [outputPath] is the file path to the Frontend Server kernel file e.g.
  ///
  ///   /some/path/main.dart.dill
  ///
  /// Corresponding `.json` and `.map` files will be read relative to
  /// [outputPath].
  ///
  /// [_packageRoot] is the path to the directory that contains a
  /// `.dart_tool/package_config.json` file for the application.
  FrontendServerAssetReader({
    required String outputPath,
    required String packageRoot,
    String? basePath,
  })  : _packageRoot = packageRoot,
        _basePath = basePath ?? '',
        _mapOriginal = File('$outputPath.map'),
        _mapIncremental = File('$outputPath.incremental.map'),
        _jsonOriginal = File('$outputPath.json'),
        _jsonIncremental = File('$outputPath.incremental.json'),
        _packageConfig = loadPackageConfig(
          File(
            p.absolute(p.join(packageRoot, '.dart_tool/package_config.json')),
          ),
        );

  @override
  String get basePath => _basePath;

  @override
  Future<String?> dartSourceContents(String serverPath) async {
    if (serverPath.endsWith('.dart')) {
      final packageConfig = await _packageConfig;

      Uri? fileUri;
      if (serverPath.startsWith('packages/')) {
        final packagePath = serverPath.replaceFirst('packages/', 'package:');
        fileUri = packageConfig.resolve(Uri.parse(packagePath));
      } else {
        fileUri = p.toUri(p.join(_packageRoot, serverPath));
      }
      if (fileUri != null) {
        final source = File(fileUri.toFilePath());
        if (source.existsSync()) return source.readAsString();
      }
    }
    _logger.severe('Cannot find source contents for $serverPath');
    return null;
  }

  @override
  Future<String?> sourceMapContents(String serverPath) async {
    if (serverPath.endsWith('lib.js.map')) {
      if (!serverPath.startsWith('/')) serverPath = '/$serverPath';
      // Strip the .map, sources are looked up by their js path.
      serverPath = p.withoutExtension(serverPath);
      if (_mapContents.containsKey(serverPath)) {
        return _mapContents[serverPath];
      }
    }
    _logger.severe('Cannot find source map contents for $serverPath');
    return null;
  }

  /// Updates the internal caches by reading the Frontend Server output files.
  ///
  /// Will only read the incremental files on additional calls.
  void updateCaches() {
    if (!_haveReadOriginals) {
      _updateCaches(_mapOriginal, _jsonOriginal);
      _haveReadOriginals = true;
    } else {
      _updateCaches(_mapIncremental, _jsonIncremental);
    }
  }

  void _updateCaches(File map, File json) {
    if (!(map.existsSync() && json.existsSync())) {
      throw StateError('$map and $json do not exist.');
    }
    final sourceContents = map.readAsBytesSync();
    final sourceInfo =
        jsonDecode(json.readAsStringSync()) as Map<String, dynamic>;
    for (var key in sourceInfo.keys) {
      final info = sourceInfo[key];
      _mapContents[key] = utf8.decode(
        sourceContents
            .getRange(
              info['sourcemap'][0] as int,
              info['sourcemap'][1] as int,
            )
            .toList(),
      );
    }
  }

  @override
  Future<String> metadataContents(String serverPath) {
    // TODO(grouma) - Implement the merged metadata reader.
    throw UnimplementedError();
  }

  @override
  Future<void> close() async {}
}
