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

import 'package:profiling/src/elf_utils.dart';

// TODO(vegorov): update this to support Android ARM64 both for standalone
// binaries and Flutter applications. Prototype code for that is available
// in https://dart-review.googlesource.com/c/sdk/+/239661.
void main(List<String> args) async {
  args = args.toList();
  bool dryRun = args.remove('--dry-run');
  if (args.length != 3) {
    print(
        'Usage: pkg/vm/tool/set_uprobe.dart <probe-name> <symbol> <AOT snapshot SO file>');
    exit(-1);
  }

  final [probeName, symbol, sharedObject] = args;

  final uprobeAddress =
      await _computeProbesVirtualAddress(sharedObject, symbol, silent: dryRun);
  final loadingBias = loadingBiasOf(sharedObject);

  final uprobeFileOffset = (uprobeAddress + loadingBias).toRadixString(16);

  final soPath = p.canonicalize(p.absolute(sharedObject));

  // TODO(vegorov) ARM64 support
  final threadRegister = "r14";
  final resultRegister = "ax";

  final uprobeFormat = symbol == 'AllocationProbePoint'
      ? 'addr=%$resultRegister:s64 top=+${await _getThreadTopOffset()}(%$threadRegister):s64 cid=-1(%$resultRegister):b20@12/32'
      : '';

  final probe = 'p:$probeName $soPath:0x$uprobeFileOffset $uprobeFormat';
  print(probe);

  if (!dryRun) {
    File('/sys/kernel/tracing/uprobe_events').writeAsStringSync(probe);
  }
}

Future<int> _computeProbesVirtualAddress(
    String sharedObject, String targetSymbol,
    {required bool silent}) async {
  int offset = 0;
  if (targetSymbol == 'AllocationProbePoint') {
    offset = await _determineAllocProbeOffset(sharedObject);
  }

  final targetRe = RegExp('\\b$targetSymbol\\b');
  final matches = <String, int>{
    for (final (:addr, :name) in textSymbolsOf(sharedObject))
      if (targetRe.hasMatch(name)) name: addr,
  };

  if (matches.isEmpty) {
    throw 'Symbol $targetSymbol not found in $sharedObject';
  }

  if (matches.length != 1) {
    throw 'Multiple symbols match: ${matches.keys}';
  }

  final entry = matches.entries.single;
  if (!silent) {
    print('placing uprobe on ${entry.key} at '
        '0x${entry.value.toRadixString(16)}+$offset');
  }
  return entry.value + offset;
}

// `AllocationProbePoint` stub should have a probe placed at a place where
// stack frame is properly setup so that unwinding succeeds. The stub itself
// contains a dummy test immediate instruction which encodes the offset at
// which the probe should be placed.
Future<int> _determineAllocProbeOffset(String sharedObject) async {
  // Dump SO file to get the address of the interesting symbol.
  final disassembly = await _exec('llvm-objdump', [
    '--disassemble-symbols=stub AllocationProbePoint',
    '-Mintel',
    sharedObject,
  ]);

  // We are looking for `test al, imm` or `tst x0, #imm` where `imm` is a
  // hexadecimal immediate encoding offset to the probe point within the stub.
  final pattern = RegExp(
      r'^\s+[a-f0-9]+:(( [a-f0-9]{2})+| [a-f0-9]{8})\s+(test|tst)\s+(al|x0),\s+#?0x(?<offset>[0-9a-f]+)\s*$',
      multiLine: true);

  final match = pattern.firstMatch(disassembly);
  if (match == null) {
    print(disassembly);
    throw StateError(
        'failed to find test-immediate instruction encoding the probe offset');
  }

  return int.parse(match.namedGroup('offset')!, radix: 16);
}

Future<String> _getThreadTopOffset() async {
  // TODO(vegorov) ARM64 support
  final sdkSrc = Platform.script.resolve('../../../..').toFilePath();
  await _exec(
      'ninja', ['-C', 'out/ReleaseX64', '-j1000', '-l64', 'offsets_extractor'],
      workingDirectory: sdkSrc);
  final offsets =
      await _exec(p.join(sdkSrc, 'out/ReleaseX64/offsets_extractor'), []);
  var jsonOffsets = json.decode(offsets)['offsets'] as List;
  for (var offset in jsonOffsets) {
    if (offset
        case {
          'class': 'Thread',
          'name': 'top_offset',
          'value': final String value
        } when int.tryParse(value) != null) {
      return value;
    }
  }
  throw 'Did not find expect json entry in offsets_extractor output.';
}

Future<String> _exec(String executable, List<String> args,
    {String? workingDirectory}) async {
  final result =
      await Process.run(executable, args, workingDirectory: workingDirectory);
  if (result.exitCode != 0) {
    throw StateError('''
Failed to run $executable ${args.join(' ')}
stdout:
${result.stdout}

stderr:

${result.stderr}
''');
  }
  return result.stdout as String;
}
