// Copyright (c) 2023, 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:io';

Never usage() {
  print('Usage: ${Platform.executable} ${Platform.script} <test-file/folder>');
  exit(1);
}

/// This script updates LINE_* and OFFSET_* constants in the given test or
/// directory.
void main(List<String> args) {
  if (args.length != 1) {
    usage();
  }

  final inputFolder = Directory(args[0]);
  if (inputFolder.existsSync()) {
    return processFolder(inputFolder);
  }
  final inputFile = File(args[0]);
  if (inputFile.existsSync() && args[0].endsWith('.dart')) {
    return processFile(inputFile);
  }
  usage();
}

void processFolder(Directory inputFolder) {
  for (var file in inputFolder.listSync()) {
    if (file is File && file.path.endsWith('.dart')) {
      processFile(file);
    }
  }
}

void processFile(File inputFile) {
  final rawContent = inputFile.readAsStringSync();
  final content = rawContent.trim().split('\n');

  const autogeneratedStart = '// AUTOGENERATED START';
  const autogeneratedEnd = '// AUTOGENERATED END';

  final lineConstantPattern = RegExp(r'^const( int)? LINE_\w+ = \d+;$');
  final offsetConstantPattern = RegExp(r'^const( int)? OFFSET_\w+ = \d+;$');

  final prefix = content
      .takeWhile(
        (line) =>
            !lineConstantPattern.hasMatch(line) &&
            !offsetConstantPattern.hasMatch(line) &&
            autogeneratedStart != line,
      )
      .toList();

  final suffix = content
      .skip(prefix.length)
      .skipWhile(
        (line) =>
            line.startsWith('//') ||
            lineConstantPattern.hasMatch(line) ||
            offsetConstantPattern.hasMatch(line),
      )
      .toList();

  final lineCommentPattern =
      RegExp(r' // (LINE_\w+)\.?$|/\*\s*(LINE_\w+)\s*\*/');
  final offsetCommentPattern =
      RegExp(r'// (OFFSET_\w+)\.?$|(/\*\s*(OFFSET_\w+)\s*\*/\s*)');
  final firstNonSpace = RegExp(r'(\s*)\S');
  final lineMapping = <String, int>{};
  final offsetMapping = <String, int>{};
  var suffixTextLengthSoFar = 0;
  for (var i = 0; i < suffix.length; i++) {
    final line = suffix[i];
    final m = lineCommentPattern.firstMatch(line);
    if (m != null) {
      lineMapping[(m[1] ?? m[2])!] = i;
    }
    for (var match in offsetCommentPattern.allMatches(line)) {
      if (match[1] != null) {
        // E.g. '// OFFSET_FOO'.
        // Points to the offset on the start of the next line.
        offsetMapping[match[1]!] = suffixTextLengthSoFar + line.length + i + 1;
      } else if (match[3] != null) {
        // E.g. '/* OFFSET_INLINE */'.
        // Points to the offset on the start of the next "word".
        if (match.end == line.length && suffix.length > i + 1) {
          // At end of line with another line. Go to the next "word"
          // on the next line.
          final nextLine = suffix[i + 1];
          offsetMapping[match[3]!] = suffixTextLengthSoFar +
              line.length +
              (firstNonSpace.firstMatch(nextLine)?.end ?? 0) +
              i;
        } else {
          offsetMapping[match[3]!] = suffixTextLengthSoFar + match.end + i;
        }
      }
    }
    suffixTextLengthSoFar += line.length;
  }

  if (lineMapping.isEmpty && offsetMapping.isEmpty) return;

  final fileUri = Uri.base.resolveUri(inputFile.uri);
  final fileUriString = fileUri.toString();
  final sdkRootString =
      Uri.base.resolveUri(Platform.script).resolve('../../../').toString();

  var testFileString = '<test.dart>';
  if (sdkRootString.length < fileUriString.length &&
      fileUriString.substring(0, sdkRootString.length) == sdkRootString) {
    testFileString = fileUriString.substring(sdkRootString.length);
  }

  final header = [
    ...prefix,
    autogeneratedStart,
    '//',
    '// Update these constants by running:',
    '//',
    '// dart pkg/vm_service/test/update_line_numbers.dart $testFileString',
    '//',
  ];

  // Mapping currently contains 0 based indices into suffix.
  // Convert them into 1 based line numbers taking into account that we will
  // generate a header + one line for each LINE_* constant plus
  // autogeneratedEnd marker.
  lineMapping.updateAll(
    (_, value) =>
        1 +
        header.length +
        lineMapping.length +
        offsetMapping.length +
        1 +
        value,
  );

  final offsetFixedLengthInt = 4;

  if (offsetMapping.isNotEmpty) {
    final newHeaderMockLength = [
      ...header,
      for (var entry in lineMapping.entries)
        'const ${entry.key} = ${entry.value};',
      for (var entry in offsetMapping.entries)
        'const ${entry.key} = ${entry.value.toString().padLeft(offsetFixedLengthInt, '0')};',
      autogeneratedEnd,
    ].join('\n').length;

    // Mapping currently contains 0 based char-indices into suffix.
    // Convert them to take into account that we will
    // generate a header + one line for each constant plus
    // autogeneratedEnd marker.
    offsetMapping.updateAll(
      (_, value) => newHeaderMockLength + 1 + value,
    );
  }

  final newContent = [
    ...header,
    for (var entry in lineMapping.entries)
      'const ${entry.key} = ${entry.value};',
    for (var entry in offsetMapping.entries)
      'const ${entry.key} = ${entry.value.toString().padLeft(offsetFixedLengthInt, '0')};',
    autogeneratedEnd,
    ...suffix,
    '',
  ].join('\n');
  if (newContent != rawContent) {
    inputFile.writeAsString(newContent);
    print('Updated: ${inputFile.path}');
  } else {
    print('Unchanged: ${inputFile.path}');
  }
}
