// 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 'package:source_maps/parser.dart';

import 'hitmap.dart';

/// Returns a Dart based hit-map containing coverage report for the provided
/// Chrome [preciseCoverage].
///
/// [sourceProvider] returns the source content for the Chrome scriptId, or null
/// if not available.
///
/// [sourceMapProvider] returns the associated source map content for the Chrome
/// scriptId, or null if not available.
///
/// [sourceUriProvider] returns the uri for the provided sourceUrl and
/// associated scriptId, or null if not available.
///
/// Chrome coverage information for which the corresponding source map or source
/// content is null will be ignored.
Future<Map<String, dynamic>> parseChromeCoverage(
  List<Map<String, dynamic>> preciseCoverage,
  Future<String?> Function(String scriptId) sourceProvider,
  Future<String?> Function(String scriptId) sourceMapProvider,
  Future<Uri?> Function(String sourceUrl, String scriptId) sourceUriProvider,
) async {
  final coverageReport = <Uri, Map<int, bool>>{};
  for (var entry in preciseCoverage) {
    final scriptId = entry['scriptId'] as String;

    final mapResponse = await sourceMapProvider(scriptId);
    if (mapResponse == null) continue;

    SingleMapping mapping;
    try {
      mapping = parse(mapResponse) as SingleMapping;
    } on FormatException {
      continue;
      // ignore: avoid_catching_errors
    } on ArgumentError {
      continue;
    }

    final compiledSource = await sourceProvider(scriptId);
    if (compiledSource == null) continue;

    final coverageInfo = _coverageInfoFor(entry);
    final offsetCoverage = _offsetCoverage(coverageInfo, compiledSource.length);
    final coveredPositions = _coveredPositions(compiledSource, offsetCoverage);

    for (var lineEntry in mapping.lines) {
      for (var columnEntry in lineEntry.entries) {
        final sourceUrlId = columnEntry.sourceUrlId;
        if (sourceUrlId == null) continue;
        final sourceUrl = mapping.urls[sourceUrlId];

        // Ignore coverage information for the SDK.
        if (sourceUrl.startsWith('org-dartlang-sdk:')) continue;

        final uri = await sourceUriProvider(sourceUrl, scriptId);
        if (uri == null) continue;
        final coverage = coverageReport.putIfAbsent(uri, () => <int, bool>{});

        final sourceLine = columnEntry.sourceLine!;
        final current = coverage[sourceLine + 1] ?? false;
        coverage[sourceLine + 1] = current ||
            coveredPositions.contains(
                _Position(lineEntry.line + 1, columnEntry.column + 1));
      }
    }
  }

  final coverageHitMaps = <Uri, HitMap>{};
  coverageReport.forEach((uri, coverage) {
    final hitMap = HitMap();
    for (var line in coverage.keys.toList()..sort()) {
      hitMap.lineHits[line] = coverage[line]! ? 1 : 0;
    }
    coverageHitMaps[uri] = hitMap;
  });

  final allCoverage = <Map<String, dynamic>>[];
  coverageHitMaps.forEach((uri, hitMap) {
    allCoverage.add(hitmapToJson(hitMap, uri));
  });
  return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage};
}

/// Returns all covered positions in a provided source.
Set<_Position> _coveredPositions(
    String compiledSource, List<bool> offsetCoverage) {
  final positions = <_Position>{};
  // Line is 1 based.
  var line = 1;
  // Column is 1 based.
  var column = 0;
  for (var offset = 0; offset < compiledSource.length; offset++) {
    if (compiledSource[offset] == '\n') {
      line++;
      column = 0;
    } else {
      column++;
    }
    if (offsetCoverage[offset]) positions.add(_Position(line, column));
  }
  return positions;
}

/// Returns coverage information for a Chrome entry.
List<_CoverageInfo> _coverageInfoFor(Map<String, dynamic> entry) {
  final result = <_CoverageInfo>[];
  for (var functions
      in (entry['functions'] as List).cast<Map<String, dynamic>>()) {
    for (var range
        in (functions['ranges'] as List).cast<Map<String, dynamic>>()) {
      result.add(_CoverageInfo(
        range['startOffset'] as int,
        range['endOffset'] as int,
        (range['count'] as int) > 0,
      ));
    }
  }
  return result;
}

/// Returns the coverage information for each offset.
List<bool> _offsetCoverage(List<_CoverageInfo> coverageInfo, int sourceLength) {
  final offsetCoverage = List.filled(sourceLength, false);

  // Sort coverage information by their size.
  // Coverage information takes granularity as precedence.
  coverageInfo.sort((a, b) =>
      (b.endOffset - b.startOffset).compareTo(a.endOffset - a.startOffset));

  for (var range in coverageInfo) {
    for (var i = range.startOffset; i < range.endOffset; i++) {
      offsetCoverage[i] = range.isCovered;
    }
  }

  return offsetCoverage;
}

class _CoverageInfo {
  _CoverageInfo(this.startOffset, this.endOffset, this.isCovered);

  /// 0 based byte offset.
  final int startOffset;

  /// 0 based byte offset.
  final int endOffset;

  final bool isCovered;
}

/// A covered position in a source file where [line] and [column] are 1 based.
class _Position {
  _Position(this.line, this.column);

  final int line;
  final int column;

  @override
  int get hashCode => Object.hash(line, column);

  @override
  bool operator ==(Object o) =>
      o is _Position && o.line == line && o.column == column;
}
