// 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:developer' as developer;
import 'dart:io' as io;
import 'dart:typed_data';

import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/visitor2.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart';
import 'package:analyzer_utilities/package_root.dart';
import 'package:args/args.dart';
import 'package:heap_snapshot/analysis.dart';
import 'package:heap_snapshot/format.dart';
import 'package:path/path.dart';
import 'package:vm_service/vm_service.dart';

import 'result.dart';

void main(List<String> arguments) async {
  var argParser = ArgParser()..addFlag('write-file');
  var argResults = argParser.parse(arguments);

  var byteStore = MemoryByteStore();

  print('First pass, fill ByteStore');
  await _withNewAnalysisContext<void>(
    byteStore: byteStore,
    (collection) async {
      print('  Analysis contexts: ${collection.contexts.length}');

      timer.start();
      await _analyzeFiles(collection);
      print('  [+${timer.elapsedMilliseconds} ms] Analyze');

      timer.reset();
      await _getAvailableLibraries(collection);
      print('  [+${timer.elapsedMilliseconds} ms] Get available libraries');
      print('');
    },
  );

  timer.reset();
  print('Second pass, read elements');
  var heapBytes = await _withNewAnalysisContext(
    byteStore: byteStore,
    (collection) async {
      print('  Analysis contexts: ${collection.contexts.length}');

      await _analyzeFiles(collection);
      print('  [+${timer.elapsedMilliseconds} ms] Analyze');

      timer.reset();
      await _getAvailableLibraries(collection);
      print('  [+${timer.elapsedMilliseconds} ms] Get available libraries');
      print('');

      return _getHeapSnapshot();
    },
  );

  var allResults = _analyzeSnapshot(heapBytes);
  _printResults(allResults);

  if (argResults['write-file'] == true) {
    _writeResultFile(allResults);
  }
}

const String includedPath = '/Users/scheglov/dart/flutter_elements/packages';

final Stopwatch timer = Stopwatch();

String get _resultFilePath {
  return posix.join(
    packageRoot,
    'analyzer',
    'tool',
    'benchmark',
    'heap',
    'flutter_elements.xml',
  );
}

/// Analyzes all included files.
///
/// Throws if there is a compile-time error.
Future<void> _analyzeFiles(
  AnalysisContextCollectionImpl collection,
) async {
  for (var analysisContext in collection.contexts) {
    var analyzedFiles = analysisContext.contextRoot.analyzedFiles().toList();
    for (var filePath in analyzedFiles) {
      if (filePath.endsWith('.dart')) {
        var analysisSession = analysisContext.currentSession;
        await analysisSession.getUnitElement(filePath);

        // Check that there are no compile-time errors.
        // We want to be sure that we get elements models.
        var errorsResult = await analysisSession.getErrors(filePath);
        if (errorsResult is ErrorsResult) {
          var errors = errorsResult.errors
              .where((element) =>
                  element.errorCode.type == ErrorType.COMPILE_TIME_ERROR)
              .toList();
          if (errors.isNotEmpty) {
            throw StateError('Errors in $filePath\n$errors');
          }
        }
      }
    }
  }
}

BenchmarkResultCompound _analyzeSnapshot(Uint8List bytes) {
  var allResults = BenchmarkResultCompound(
    name: 'flutter_elements',
  );

  timer.reset();
  var graph = HeapSnapshotGraph.fromChunks(
      [bytes.buffer.asByteData(bytes.offsetInBytes, bytes.length)]);
  print('[+${timer.elapsedMilliseconds} ms] Create HeapSnapshotGraph');

  var analysis = Analysis(graph);

  // Computing reachable objects takes some time.
  timer.reset();
  analysis.reachableObjects;
  print('[+${timer.elapsedMilliseconds} ms] Compute reachable objects');
  print('');
  {
    var measure = analysis.measureObjects(analysis.reachableObjects);
    allResults.add(
      BenchmarkResultCompound(name: 'reachableObjects', children: [
        BenchmarkResultCount(
          name: 'count',
          value: measure.count,
        ),
        BenchmarkResultBytes(
          name: 'size',
          value: measure.size,
        ),
      ]),
    );
  }

  // It is interesting to see all reachable objects.
  {
    print('Reachable objects');
    var objects = analysis.reachableObjects;
    analysis.printObjectStats(objects, maxLines: 100);
  }

  timer.reset();

  allResults.add(
    _doUniqueUriStr(analysis),
  );

  allResults.add(
    _doInterfaceType(analysis),
  );

  allResults.add(
    _doLinkedData(analysis),
  );

  print('[+${timer.elapsedMilliseconds} ms] Compute benchmark results');
  print('');

  return allResults;
}

BenchmarkResult _doInterfaceType(Analysis analysis) {
  var objects = analysis.filterByClass(
    analysis.reachableObjects,
    libraryUri: Uri.parse('package:analyzer/src/dart/element/type.dart'),
    name: 'InterfaceTypeImpl',
  );

  var measure = analysis.measureObjects(objects);
  return BenchmarkResultCompound(name: 'InterfaceTypeImpl', children: [
    BenchmarkResultCount(
      name: 'count',
      value: measure.count,
    ),
    BenchmarkResultBytes(
      name: 'size(shallow)',
      value: measure.size,
    ),
  ]);
}

BenchmarkResult _doLinkedData(Analysis analysis) {
  var readerUri = Uri.parse(
    'package:analyzer/src/summary2/bundle_reader.dart',
  );

  var classSet = analysis.classByPredicate((e) {
    return e.libraryUri == readerUri && e.name.endsWith('LinkedData');
  });

  var objects = analysis.filterByClassId(analysis.reachableObjects, classSet);

  var measure = analysis.measureObjects(objects);
  return BenchmarkResultCompound(name: 'LinkedData', children: [
    BenchmarkResultCount(
      name: 'count',
      value: measure.count,
    ),
  ]);
}

BenchmarkResult _doUniqueUriStr(Analysis analysis) {
  var uriList = analysis.filterByClass(analysis.reachableObjects,
      libraryUri: Uri.parse('dart:core'), name: '_SimpleUri');

  var uriStringList = analysis.findReferences(uriList, [':_uri']);
  var uniqueUriStrSet = <String>{};
  var duplicateUriStrList = <String>[];
  for (var objectId in uriStringList) {
    var object = analysis.graph.objects[objectId];
    var uriStr = object.data as String;
    if (!uniqueUriStrSet.add(uriStr)) {
      duplicateUriStrList.add(uriStr);
    }
  }

  var uriListMeasure = analysis.measureObjects(uriList);
  return BenchmarkResultCompound(name: '_SimpleUri', children: [
    BenchmarkResultCount(
      name: 'count',
      value: uriListMeasure.count,
    ),
    BenchmarkResultBytes(
      name: 'size(shallow)',
      value: uriListMeasure.size,
    ),
    BenchmarkResultCount(
      name: 'duplicateCount',
      value: duplicateUriStrList.length,
    ),
  ]);
}

/// Loads all libraries available in the analysis contexts, and deserializes
/// every element in them.
Future<void> _getAvailableLibraries(
  AnalysisContextCollectionImpl collection,
) async {
  for (var analysisContext in collection.contexts) {
    var analysisDriver = analysisContext.driver;
    await analysisDriver.discoverAvailableFiles();
    var knownFiles = analysisDriver.fsState.knownFiles.toList();
    for (var file in knownFiles) {
      // Skip libraries with known invalid types.
      // if (const {'dart:html', 'dart:ui_web', 'dart:_interceptors'}
      //     .contains(file.uriStr)) {
      //   continue;
      // }
      var result = await analysisDriver.getLibraryByUri(file.uriStr);
      if (result is LibraryElementResult) {
        result.element2.accept2(_AllElementVisitor());
      }
    }
  }
}

Uint8List _getHeapSnapshot() {
  timer.reset();
  var tmpDir = io.Directory.systemTemp.createTempSync('analyzer_heap');
  try {
    var snapshotFile = io.File('${tmpDir.path}/0.heap_snapshot');
    developer.NativeRuntime.writeHeapSnapshotToFile(snapshotFile.path);
    print('[+${timer.elapsedMilliseconds} ms] Write heap snapshot');

    timer.reset();
    var bytes = snapshotFile.readAsBytesSync();
    print(
      '[+${timer.elapsedMilliseconds} ms] '
      'Read heap snapshot, ${bytes.length ~/ (1024 * 1024)} MB',
    );
    return bytes;
  } finally {
    tmpDir.deleteSync(recursive: true);
  }
}

void _printResults(BenchmarkResultCompound allResults) {
  BenchmarkResult? baseResult;
  try {
    var text = io.File(_resultFilePath).readAsStringSync();
    baseResult = BenchmarkResult.fromXmlText(text);
  } catch (e) {
    // ignore
  }

  print('All results');
  print('-' * 32);
  print(allResults.asDisplayText(baseResult));
}

Future<T> _withNewAnalysisContext<T>(
  Future<T> Function(AnalysisContextCollectionImpl collection) f, {
  required ByteStore byteStore,
}) async {
  var resourceProvider = PhysicalResourceProvider.INSTANCE;
  var fileContentCache = FileContentCache(resourceProvider);
  var unlinkedUnitStore = UnlinkedUnitStoreImpl();
  var collection = AnalysisContextCollectionImpl(
    byteStore: byteStore,
    resourceProvider: resourceProvider,
    fileContentCache: fileContentCache,
    includedPaths: [includedPath],
    unlinkedUnitStore: unlinkedUnitStore,
  );
  var result = await f(collection);
  collection.hashCode; // to keep it alive
  return result;
}

void _writeResultFile(BenchmarkResultCompound result) {
  io.File(_resultFilePath).writeAsStringSync(result.asXmlText, flush: true);
}

class _AllElementVisitor extends GeneralizingElementVisitor2<void> {
  @override
  void visitElement(Element2 element) {
    // This triggers lazy reading.
    if (element case Annotatable element) {
      element.metadata2;
    }
    super.visitElement(element);
  }
}

class _ObjectSetMeasure {
  final int count;
  final int size;

  _ObjectSetMeasure({required this.count, required this.size});
}

extension on Analysis {
  IntSet classByPredicate(bool Function(HeapSnapshotClass) predicate) {
    var allClasses = graph.classes;
    var classSet = SpecializedIntSet(allClasses.length);
    for (var class_ in allClasses) {
      if (predicate(class_)) {
        classSet.add(class_.classId);
      }
    }
    return classSet;
  }

  IntSet filterByClass(
    IntSet objectIds, {
    required Uri libraryUri,
    required String name,
  }) {
    var cid = graph.classes.singleWhere((class_) {
      return class_.libraryUri == libraryUri && class_.name == name;
    }).classId;
    return filter(objectIds, (object) => object.classId == cid);
  }

  _ObjectSetMeasure measureObjects(IntSet objectIds) {
    var stats = generateObjectStats(objectIds);
    var totalSize = 0;
    var totalCount = 0;
    for (var class_ in stats.classes) {
      totalCount += stats.counts[class_.classId];
      totalSize += stats.sizes[class_.classId];
    }
    return _ObjectSetMeasure(count: totalCount, size: totalSize);
  }

  void printObjectStats(IntSet objectIds, {int maxLines = 20}) {
    var stats = generateObjectStats(objectIds);
    print(formatHeapStats(stats, maxLines: maxLines));
    print('');
  }

  // ignore: unused_element
  void printRetainers(
    IntSet objectIds, {
    int maxEntries = 3,
  }) {
    var paths = retainingPathsOf(objectIds, 20);
    for (int i = 0; i < paths.length; ++i) {
      if (i >= maxEntries) break;
      var path = paths[i];
      print('There are ${path.count} retaining paths of');
      print(formatRetainingPath(graph, paths[i]));
      print('');
    }
  }
}
