import 'dart:developer' as developer;
import 'dart:io' as io;
import 'dart:typed_data';

import 'package:analyzer/file_system/overlay_file_system.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/driver_event.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/analysis/results.dart';
import 'package:heap_snapshot/analysis.dart';
import 'package:heap_snapshot/format.dart';
import 'package:linter/src/rules.dart';
import 'package:vm_service/vm_service.dart';

void main() async {
  var resourceProvider = OverlayResourceProvider(
    PhysicalResourceProvider.INSTANCE,
  );

  registerLintRules();

  var byteStore = NullByteStore();

  var packageRootPath = '/Users/scheglov/dart/admin-portal';
  var libPath = '$packageRootPath/lib';

  // var builder = PackageConfigFileBuilder()
  //   ..add(
  //     name: 'test',
  //     rootPath: packageRootPath,
  //   );
  //
  // resourceProvider.setOverlay(
  //   '$packageRootPath/.dart_tool/package_config.json',
  //   content: builder.toContent(
  //     toUriStr: (path) {
  //       var pathContext = resourceProvider.pathContext;
  //       return pathContext.toUri(path).toString();
  //     },
  //   ),
  //   modificationStamp: 0,
  // );

  var modelPath = '$libPath/data/models/group_model.dart';
  var modelCode = resourceProvider.getFile(modelPath).readAsStringSync();

  var collection = AnalysisContextCollectionImpl(
    resourceProvider: resourceProvider,
    // includedPaths: [libPath],
    includedPaths: [
      '/Users/scheglov/dart/admin-portal/lib/data/models/group_model.dart',
      '/Users/scheglov/dart/admin-portal/lib/utils/formatting.dart',
      '/Users/scheglov/dart/admin-portal/lib/redux/company/company_selectors.dart',
    ],
    byteStore: byteStore,
    performanceLog: PerformanceLog(io.stdout),
    drainStreams: false,
  );

  var analysisContext = collection.contextFor(modelPath);
  for (var path in analysisContext.contextRoot.analyzedFiles()) {
    if (path.endsWith('.dart')) {
      analysisContext.driver.addFile(path);
    }
  }

  analysisContext.driver.scheduler.events.listen((event) {
    switch (event) {
      case AnalyzeFile analyzeFile:
        print('[events][analyzeFile][file: ${analyzeFile.file}]');
      case ResolvedUnitResultImpl unitResult:
        print('[events][resolvedUnit][file: ${unitResult.file}]');
        print('  [events][errors: ${unitResult.errors}]');
      default:
        print('[events][event: $event]');
    }
  });

  await collection.scheduler.waitForIdle();
  await pumpEventQueue();
  print('\n' * 2);
  print('[S] Now idle');
  print('-' * 64);

  {
    var heapBytes = _getHeapSnapshot();
    _analyzeSnapshot(heapBytes);
  }
  print('\n' * 2);
  print('[S] Printed heap analysis');
  print('-' * 64);

  resourceProvider.setOverlay(
    modelPath,
    content: modelCode.replaceAll(
      'bool get hasCurrency =>',
      'bool get hasCurrency2 =>',
    ),
    modificationStamp: 1,
  );
  analysisContext.changeFile(modelPath);
  print('[S] hasCurrency -> hasCurrency2');
  print('\n' * 2);
  await collection.scheduler.waitForIdle();
  await pumpEventQueue();
  print('\n' * 2);
  print('[S] Now idle');
  print('-' * 64);

  resourceProvider.setOverlay(
    modelPath,
    content: modelCode.replaceAll(
      'bool get hasCurrency2 =>',
      'bool get hasCurrency =>',
    ),
    modificationStamp: 2,
  );
  analysisContext.changeFile(modelPath);
  print('[S] hasCurrency2 -> hasCurrency');
  print('\n' * 2);
  await collection.scheduler.waitForIdle();
  await pumpEventQueue();
  print('\n' * 2);
  print('[S] Now idle');
  print('-' * 64);

  resourceProvider.setOverlay(
    modelPath,
    content: modelCode.replaceAll(
      'bool get hasCurrency =>',
      'bool get hasCurrency2 =>',
    ),
    modificationStamp: 3,
  );
  analysisContext.changeFile(modelPath);
  print('[S] hasCurrency -> hasCurrency2');
  print('\n' * 2);
  await collection.scheduler.waitForIdle();
  await pumpEventQueue();
  print('\n' * 2);
  print('[S] Now idle');
  print('-' * 64);

  print('[S] Disposing...');
  await collection.dispose();
}

final Stopwatch timer = Stopwatch();

Future pumpEventQueue([int times = 5000]) {
  if (times == 0) return Future.value();
  return Future.delayed(Duration.zero, () => pumpEventQueue(times - 1));
}

void _analyzeSnapshot(Uint8List bytes) {
  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);
  }

  // {
  //   print('\n\n');
  //   print('Tokens');
  //   print('Instances of: _GrowableList');
  //   final objectList = analysis.filter(analysis.reachableObjects, (object) {
  //     return object.klass.libraryUri == Uri.parse('dart:core') &&
  //         object.klass.name == '_GrowableList';
  //     // return analysis.variableLengthOf(object) == 0;
  //   });
  //
  //   // final objectList = analysis.filterByClassPatterns(
  //   //   analysis.reachableObjects,
  //   //   ['_GrowableList'],
  //   // );
  //   final stats = analysis.generateObjectStats(objectList);
  //   print(formatHeapStats(stats, maxLines: 20));
  //   print('');
  //
  //   const maxEntries = 10;
  //   final paths = analysis.retainingPathsOf(objectList, 10);
  //   for (int i = 0; i < paths.length; ++i) {
  //     if (maxEntries != -1 && i >= maxEntries) break;
  //     final path = paths[i];
  //     print('There are ${path.count} retaining paths of');
  //     print(formatRetainingPath(analysis.graph, paths[i]));
  //     print('');
  //   }
  // }

  // timer.reset();
  //
  // allResults.add(
  //   _doUniqueUriStr(analysis),
  // );
  //
  // allResults.add(
  //   _doInterfaceType(analysis),
  // );
  //
  // allResults.add(
  //   _doLinkedData(analysis),
  // );
  //
  // print('[+${timer.elapsedMilliseconds} ms] Compute benchmark results');
  // print('');
  //
  // return allResults;
}

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);
  }
}

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('');
    }
  }
}

// class _ObjectSetMeasure {
//   final int count;
//   final int size;
//
//   _ObjectSetMeasure({required this.count, required this.size});
// }
