// Copyright (c) 2014, 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:glob/glob.dart';
import 'package:path/path.dart' as p;

import 'hitmap.dart';
import 'resolver.dart';

@Deprecated('Migrate to FileHitMapsFormatter')
abstract class Formatter {
  /// Returns the formatted coverage data.
  Future<String> format(Map<String, Map<int, int>> hitmap);
}

/// Converts the given hitmap to lcov format and appends the result to
/// env.output.
///
/// Returns a [Future] that completes as soon as all map entries have been
/// emitted.
@Deprecated('Migrate to FileHitMapsFormatter.formatLcov')
class LcovFormatter implements Formatter {
  /// Creates a LCOV formatter.
  ///
  /// If [reportOn] is provided, coverage report output is limited to files
  /// prefixed with one of the paths included. If [basePath] is provided, paths
  /// are reported relative to that path.
  LcovFormatter(this.resolver, {this.reportOn, this.basePath});

  final Resolver resolver;
  final String? basePath;
  final List<String>? reportOn;

  @override
  Future<String> format(Map<String, Map<int, int>> hitmap) {
    return Future.value(hitmap
        .map((key, value) => MapEntry(key, HitMap(value)))
        .formatLcov(resolver, basePath: basePath, reportOn: reportOn));
  }
}

/// Converts the given hitmap to a pretty-print format and appends the result
/// to env.output.
///
/// Returns a [Future] that completes as soon as all map entries have been
/// emitted.
@Deprecated('Migrate to FileHitMapsFormatter.prettyPrint')
class PrettyPrintFormatter implements Formatter {
  /// Creates a pretty-print formatter.
  ///
  /// If [reportOn] is provided, coverage report output is limited to files
  /// prefixed with one of the paths included.
  PrettyPrintFormatter(this.resolver, this.loader,
      {this.reportOn, this.reportFuncs = false});

  final Resolver resolver;
  final Loader loader;
  final List<String>? reportOn;
  final bool reportFuncs;

  @override
  Future<String> format(Map<String, Map<int, int>> hitmap) {
    return hitmap.map((key, value) => MapEntry(key, HitMap(value))).prettyPrint(
        resolver, loader,
        reportOn: reportOn, reportFuncs: reportFuncs);
  }
}

extension FileHitMapsFormatter on Map<String, HitMap> {
  /// Converts the given hitmap to lcov format.
  ///
  /// If [reportOn] is provided, coverage report output is limited to files
  /// prefixed with one of the paths included. If [basePath] is provided, paths
  /// are reported relative to that path.
  String formatLcov(
    Resolver resolver, {
    String? basePath,
    List<String>? reportOn,
    Set<Glob>? ignoreGlobs,
  }) {
    final pathFilter = _getPathFilter(
      reportOn: reportOn,
      ignoreGlobs: ignoreGlobs,
    );
    final buf = StringBuffer();
    for (final entry in entries) {
      final v = entry.value;
      final lineHits = v.lineHits;
      final funcHits = v.funcHits;
      final funcNames = v.funcNames;
      final branchHits = v.branchHits;
      var source = resolver.resolve(entry.key);
      if (source == null) {
        continue;
      }

      if (!pathFilter(source)) {
        continue;
      }

      if (basePath != null) {
        source = p.relative(source, from: basePath);
      }

      buf.write('SF:$source\n');
      if (funcHits != null && funcNames != null) {
        for (final k in funcNames.keys.toList()..sort()) {
          buf.write('FN:$k,${funcNames[k]}\n');
        }
        for (final k in funcHits.keys.toList()..sort()) {
          if (funcHits[k]! != 0) {
            buf.write('FNDA:${funcHits[k]},${funcNames[k]}\n');
          }
        }
        buf.write('FNF:${funcNames.length}\n');
        buf.write('FNH:${funcHits.values.where((v) => v > 0).length}\n');
      }
      for (final k in lineHits.keys.toList()..sort()) {
        buf.write('DA:$k,${lineHits[k]}\n');
      }
      buf.write('LF:${lineHits.length}\n');
      buf.write('LH:${lineHits.values.where((v) => v > 0).length}\n');
      if (branchHits != null) {
        for (final k in branchHits.keys.toList()..sort()) {
          buf.write('BRDA:$k,0,0,${branchHits[k]}\n');
        }
      }
      buf.write('end_of_record\n');
    }

    return buf.toString();
  }

  /// Converts the given hitmap to a pretty-print format.
  ///
  /// If [reportOn] is provided, coverage report output is limited to files
  /// prefixed with one of the paths included. If [reportFuncs] is provided,
  /// only function coverage information will be shown.
  Future<String> prettyPrint(
    Resolver resolver,
    Loader loader, {
    List<String>? reportOn,
    Set<Glob>? ignoreGlobs,
    bool reportFuncs = false,
    bool reportBranches = false,
  }) async {
    final pathFilter = _getPathFilter(
      reportOn: reportOn,
      ignoreGlobs: ignoreGlobs,
    );
    final buf = StringBuffer();
    for (final entry in entries) {
      final source = resolver.resolve(entry.key);
      if (source == null) {
        continue;
      }

      if (!pathFilter(source)) {
        continue;
      }

      final lines = await loader.load(source);
      if (lines == null) {
        continue;
      }

      final v = entry.value;
      if (reportFuncs && v.funcHits == null) {
        throw StateError(
          'Function coverage formatting was requested, but the hit map is '
          'missing function coverage information. Did you run '
          'collect_coverage with the --function-coverage flag?',
        );
      }
      if (reportBranches && v.branchHits == null) {
        throw StateError(
            'Branch coverage formatting was requested, but the hit map is '
            'missing branch coverage information. Did you run '
            'collect_coverage with the --branch-coverage flag?');
      }

      final hits = reportFuncs
          ? v.funcHits!
          : reportBranches
              ? v.branchHits!
              : v.lineHits;
      buf.writeln(source);
      for (var line = 1; line <= lines.length; line++) {
        var prefix = _prefix;
        if (hits.containsKey(line)) {
          prefix = hits[line].toString().padLeft(_prefix.length);
        }
        buf.writeln('$prefix|${lines[line - 1]}');
      }
    }

    return buf.toString();
  }
}

const _prefix = '       ';

typedef _PathFilter = bool Function(String path);

_PathFilter _getPathFilter({List<String>? reportOn, Set<Glob>? ignoreGlobs}) {
  if (reportOn == null && ignoreGlobs == null) return (String path) => true;

  final absolutePaths = reportOn?.map(p.canonicalize).toList();

  return (String path) {
    final canonicalizedPath = p.canonicalize(path);

    if (absolutePaths != null &&
        !absolutePaths.any(canonicalizedPath.startsWith)) {
      return false;
    }
    if (ignoreGlobs != null &&
        ignoreGlobs.any((glob) => glob.matches(canonicalizedPath))) {
      return false;
    }

    return true;
  };
}
