// Copyright 2014 The Flutter 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 '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/logger.dart';

/// A service for creating and parsing [Depfile]s.
class DepfileService {
  DepfileService({
    required Logger logger,
    required FileSystem fileSystem,
  }) : _logger = logger,
       _fileSystem = fileSystem;

  final Logger _logger;
  final FileSystem _fileSystem;
  static final RegExp _separatorExpr = RegExp(r'([^\\]) ');
  static final RegExp _escapeExpr = RegExp(r'\\(.)');

  /// Given an [depfile] File, write the depfile contents.
  ///
  /// If both [inputs] and [outputs] are empty, ensures the file does not
  /// exist. This can be overridden with the [writeEmpty] parameter when
  /// both static and runtime dependencies exist and it is not desired
  /// to force a rerun due to no depfile.
  void writeToFile(Depfile depfile, File output, {bool writeEmpty = false}) {
    if (depfile.inputs.isEmpty && depfile.outputs.isEmpty && !writeEmpty) {
      ErrorHandlingFileSystem.deleteIfExists(output);
      return;
    }
    final StringBuffer buffer = StringBuffer();
    _writeFilesToBuffer(depfile.outputs, buffer);
    buffer.write(': ');
    _writeFilesToBuffer(depfile.inputs, buffer);
    output.writeAsStringSync(buffer.toString());
  }

  /// Parse the depfile contents from [file].
  ///
  /// If the syntax is invalid, returns an empty [Depfile].
  Depfile parse(File file) {
    final String contents = file.readAsStringSync();
    final List<String> colonSeparated = contents.split(': ');
    if (colonSeparated.length != 2) {
      _logger.printError('Invalid depfile: ${file.path}');
      return const Depfile(<File>[], <File>[]);
    }
    final List<File> inputs = _processList(colonSeparated[1].trim());
    final List<File> outputs = _processList(colonSeparated[0].trim());
    return Depfile(inputs, outputs);
  }


  /// Parse the output of dart2js's used dependencies.
  ///
  /// The [file] contains a list of newline separated file URIs. The output
  /// file must be manually specified.
  Depfile parseDart2js(File file, File output) {
    final List<File> inputs = <File>[];
    for (final String rawUri in file.readAsLinesSync()) {
      if (rawUri.trim().isEmpty) {
        continue;
      }
      final Uri? fileUri = Uri.tryParse(rawUri);
      if (fileUri == null) {
        continue;
      }
      if (fileUri.scheme != 'file') {
        continue;
      }
      inputs.add(_fileSystem.file(fileUri));
    }
    return Depfile(inputs, <File>[output]);
  }

  void _writeFilesToBuffer(List<File> files, StringBuffer buffer) {
    for (final File outputFile in files) {
      if (_fileSystem.path.style.separator == r'\') {
        // backslashes and spaces in a depfile have to be escaped if the
        // platform separator is a backslash.
        final String path = outputFile.path
          .replaceAll(r'\', r'\\')
          .replaceAll(r' ', r'\ ');
        buffer.write(' $path');
      } else {
        final String path = outputFile.path
          .replaceAll(r' ', r'\ ');
        buffer.write(' $path');
      }
    }
  }

  List<File> _processList(String rawText) {
    return rawText
    // Put every file on right-hand side on the separate line
        .replaceAllMapped(_separatorExpr, (Match match) => '${match.group(1)}\n')
        .split('\n')
    // Expand escape sequences, so that '\ ', for example,ß becomes ' '
        .map<String>((String path) => path.replaceAllMapped(_escapeExpr, (Match match) => match.group(1)!).trim())
        .where((String path) => path.isNotEmpty)
    // The tool doesn't write duplicates to these lists. This call is an attempt to
    // be resilient to the outputs of other tools which write or user edits to depfiles.
        .toSet()
        .map(_fileSystem.file)
        .toList();
  }
}

/// A class for representing depfile formats.
class Depfile {
  /// Create a [Depfile] from a list of [input] files and [output] files.
  const Depfile(this.inputs, this.outputs);

  /// The input files for this depfile.
  final List<File> inputs;

  /// The output files for this depfile.
  final List<File> outputs;
}