content: import 'dart:convert';
  import 'dart:io';
  
  import 'package:collection/collection.dart';
  import 'package:path/path.dart' as path;
  
  import '../models/api_models.dart';
  import 'cache.dart';
  import 'project.dart';
  
  class Compiler {
    StorageCache cache;
  
    Compiler() : cache = StorageCache();
  
    Future<CompileResponse> compile(CompileRequest request) async {
      var sourceHash = MapEquality().hash(request.sources);
      var hashedResult = await cache.getCachedResult(sourceHash);
  
      if (hashedResult != null) {
        print("[INFO] Cache hit for $sourceHash");
        return CompileResponse(hashedResult, null);
      }
  
      final temp = await Directory.systemTemp.createTemp('jasprpad');
  
      try {
        var fromPath = jasprBasicTemplatePath;
        await copyPath(fromPath, temp.path);
        await fixPathPackages(fromPath, temp.path);
  
        await Directory(path.join(temp.path, 'lib')).create(recursive: true);
  
        for (var key in request.sources.keys) {
          final file = File(path.join(temp.path, 'lib', key));
          await file.writeAsString(request.sources[key]!);
        }
  
        final arguments = <String>[
          'compile',
          'js',
          '--suppress-hints',
          '--terse',
          '--packages=${path.join('.dart_tool', 'package_config.json')}',
          '--enable-asserts',
          '-o',
          'main.js',
          'web/main.dart',
        ];
  
        final result = await Process.run('dart', arguments, workingDirectory: temp.path);
  
        if (result.exitCode != 0) {
          return CompileResponse(null, result.stdout as String);
        } else {
          final mainJs = await File(path.join(temp.path, 'main.js')).readAsString();
          await cache.cacheResult(sourceHash, mainJs);
          return CompileResponse(mainJs, null);
        }
      } catch (e, st) {
        print('Compiler failed: $e\n$st');
        rethrow;
      } finally {
        await temp.delete(recursive: true);
      }
    }
  }
  
  /// Copies all of the files in the [from] directory to [to].
  ///
  /// This is similar to `cp -R <from> <to>`:
  /// * Symlinks are supported.
  /// * Existing files are over-written, if any.
  /// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
  /// * If [from] and [to] are canonically the same, no operation occurs.
  ///
  /// Returns a future that completes when complete.
  Future<void> copyPath(String from, String to) async {
    if (_doNothing(from, to)) {
      return;
    }
    await Directory(to).create(recursive: true);
    await for (final file in Directory(from).list(recursive: true)) {
      final copyTo = path.join(to, path.relative(file.path, from: from));
      if (file is Directory) {
        await Directory(copyTo).create(recursive: true);
      } else if (file is File) {
        await File(file.path).copy(copyTo);
      } else if (file is Link) {
        await Link(copyTo).create(await file.target(), recursive: true);
      }
    }
  }
  
  bool _doNothing(String from, String to) {
    if (path.canonicalize(from) == path.canonicalize(to)) {
      return true;
    }
    if (path.isWithin(from, to)) {
      throw ArgumentError('Cannot copy from $from to $to');
    }
    return false;
  }
  
  Future<void> fixPathPackages(String from, String to) async {
    var packagesFile = File(path.join(to, '.dart_tool', 'package_config.json'));
    var packageJson = jsonDecode(await packagesFile.readAsString());
    bool hasChanged = false;
    for (var package in packageJson['packages']) {
      if (package['name'] == 'jaspr_basic_template') continue;
      var uri = package['rootUri'];
      if (!uri.startsWith('file://') && path.isRelative(uri)) {
        package['rootUri'] = 'file://${path.normalize(path.join(from, '.dart_tool', uri))}';
        hasChanged = true;
      }
    }
    if (hasChanged) {
      await packagesFile.writeAsString(jsonEncode(packageJson));
    }
  }
  