// Copyright (c) 2018, 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.

/// This tool creates interactive navigable binary size reports from
/// JSON file generated by the AOT compiler's --print-instructions-sizes-to
/// flag.
///
/// It used the same visualization framework as Chromium's binary_size tool
/// located in runtime/third_party/binary_size.
library vm_snapshot_analysis.commands.treemap;

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

import 'package:args/command_runner.dart';
import 'package:path/path.dart' as p;
import 'package:vm_snapshot_analysis/treemap.dart';

import 'utils.dart';

class TreemapCommand extends Command<void> {
  @override
  final name = 'treemap';

  @override
  final description = '''
Create interactive treemap from snapshot profiling information.

This tool is used to process snapshot size reports produced by
--print-instructions-sizes-to and --write-v8-snapshot-profile-to flags.

It will create an interactive web-page in the output-directory which can be
viewed in a browser:

\$ google-chrome <output-directory>/index.html''';

  @override
  String get invocation => super
      .invocation
      .replaceAll('[arguments]', '<symbol-sizes.json> <output-directory>');

  TreemapCommand() {
    // [argParser] is automatically created by the parent class.
    argParser
      ..addFlag('force',
          abbr: 'f',
          help: 'Force overwrite output directory even if it already exists')
      ..addOption('format',
          help: 'Specifies granularity of the treemap output',
          allowed: _formatFromString.keys,
          defaultsTo: _formatFromString.keys.first);
  }

  @override
  Future<void> run() async {
    final args = argResults!;

    if (args.rest.length != 2) {
      usageException('Need to specify input JSON and output directory.');
    }

    final input = File(args.rest[0]);
    final outputDir = Directory(args.rest[1]);

    if (!input.existsSync()) {
      usageException('Input file ${input.path} does not exist!');
    }

    if (outputDir.existsSync() && !args['force']) {
      usageException(
          'Output directory ${outputDir.path} already exists, specify --force to ignore.');
    }

    await generateTreeMap(input, outputDir,
        format: _formatFromString[args['format']] ??
            (throw 'Unrecognized format ${args['format']}'));
  }

  // Note: the first key in this map is the default format.
  static const _formatFromString = {
    'object-type': TreemapFormat.objectType,
    'data-and-code': TreemapFormat.dataAndCode,
    'collapsed': TreemapFormat.collapsed,
    'simplified': TreemapFormat.simplified,
  };
}

Future<void> generateTreeMap(File input, Directory outputDir,
    {TreemapFormat format = TreemapFormat.objectType}) async {
  // Load symbols data produced by the AOT compiler and convert it to
  // a tree.
  final inputJson = await loadJsonFromFile(input);
  final tree = treemapFromJson(inputJson, format: format);

  // Create output directory and copy all auxiliary files from binary_size tool.
  await outputDir.create(recursive: true);

  final assetsUri = (await Isolate.resolvePackageUri(
      Uri.parse('package:vm_snapshot_analysis/src/assets')))!;
  final assetsDir = assetsUri.toFilePath();
  final d3SrcDir = p.join(assetsDir, 'd3', 'src');

  final d3OutDir = p.join(outputDir.path, 'd3');
  await Directory(d3OutDir).create(recursive: true);

  for (var file in ['LICENSE', 'd3.js']) {
    await copyFile(d3SrcDir, file, d3OutDir);
  }
  for (var file in ['index.html', 'D3SymbolTreeMap.js']) {
    await copyFile(assetsDir, file, outputDir.path);
  }

  // Serialize symbol size tree as JSON.
  final dataJsPath = p.join(outputDir.path, 'data.js');
  final sink = File(dataJsPath).openWrite();
  sink.write('var tree_data=');
  await sink.addStream(Stream<Object?>.fromIterable([tree])
      .transform(json.encoder.fuse(utf8.encoder)));
  await sink.close();

  // Done.
  print('Generated ${p.toUri(p.absolute(outputDir.path, 'index.html'))}');
}

/// Copy file with the given name from [fromDir] to [toDir].
Future<void> copyFile(String fromDir, String name, String toDir) async {
  await File(p.join(fromDir, name)).copy(p.join(toDir, name));
}
