// Copyright 2017 The Chromium Authors. 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:async';
import 'dart:convert';

import 'package:usage/uuid/uuid.dart';

import 'artifacts.dart';
import 'base/common.dart';
import 'base/context.dart';
import 'base/io.dart';
import 'base/process_manager.dart';
import 'globals.dart';

KernelCompiler get kernelCompiler => context[KernelCompiler];

typedef void CompilerMessageConsumer(String message);

class CompilerOutput {
  final String outputFilename;
  final int errorCount;

  const CompilerOutput(this.outputFilename, this.errorCount);
}

class _StdoutHandler {
  _StdoutHandler({this.consumer: printError}) {
    reset();
  }

  final CompilerMessageConsumer consumer;
  String boundaryKey;
  Completer<CompilerOutput> compilerOutput;

  void handler(String string) {
    const String kResultPrefix = 'result ';
    if (boundaryKey == null) {
      if (string.startsWith(kResultPrefix))
        boundaryKey = string.substring(kResultPrefix.length);
    } else if (string.startsWith(boundaryKey)) {
      if (string.length <= boundaryKey.length) {
        compilerOutput.complete(null);
        return;
      }
      final int spaceDelimiter = string.lastIndexOf(' ');
      compilerOutput.complete(
        new CompilerOutput(
          string.substring(boundaryKey.length + 1, spaceDelimiter),
          int.parse(string.substring(spaceDelimiter + 1).trim())));
    }
    else
      consumer('compiler message: $string');
  }

  // This is needed to get ready to process next compilation result output,
  // with its own boundary key and new completer.
  void reset() {
    boundaryKey = null;
    compilerOutput = new Completer<CompilerOutput>();
  }
}

class KernelCompiler {
  const KernelCompiler();

  Future<CompilerOutput> compile({
    String sdkRoot,
    String mainPath,
    String outputFilePath,
    String depFilePath,
    bool linkPlatformKernelIn: false,
    bool aot: false,
    List<String> entryPointsJsonFiles,
    bool trackWidgetCreation: false,
    List<String> extraFrontEndOptions,
    String incrementalCompilerByteStorePath,
    String packagesPath,
    List<String> fileSystemRoots,
    String fileSystemScheme,
  }) async {
    final String frontendServer = artifacts.getArtifactPath(
      Artifact.frontendServerSnapshotForEngineDartSdk
    );

    // This is a URI, not a file path, so the forward slash is correct even on Windows.
    if (!sdkRoot.endsWith('/'))
      sdkRoot = '$sdkRoot/';
    final String engineDartPath = artifacts.getArtifactPath(Artifact.engineDartBinary);
    if (!processManager.canRun(engineDartPath)) {
      throwToolExit('Unable to find Dart binary at $engineDartPath');
    }
    final List<String> command = <String>[
      engineDartPath,
      frontendServer,
      '--sdk-root',
      sdkRoot,
      '--strong',
      '--target=flutter',
    ];
    if (trackWidgetCreation)
      command.add('--track-widget-creation');
    if (!linkPlatformKernelIn)
      command.add('--no-link-platform');
    if (aot) {
      command.add('--aot');
      command.add('--tfa');
    }
    if (entryPointsJsonFiles != null) {
      for (String entryPointsJson in entryPointsJsonFiles) {
        command.addAll(<String>['--entry-points', entryPointsJson]);
      }
    }
    if (incrementalCompilerByteStorePath != null) {
      command.add('--incremental');
    }
    if (packagesPath != null) {
      command.addAll(<String>['--packages', packagesPath]);
    }
    if (outputFilePath != null) {
      command.addAll(<String>['--output-dill', outputFilePath]);
    }
    if (depFilePath != null && (fileSystemRoots == null || fileSystemRoots.isEmpty)) {
      command.addAll(<String>['--depfile', depFilePath]);
    }
    if (fileSystemRoots != null) {
      for (String root in fileSystemRoots) {
        command.addAll(<String>['--filesystem-root', root]);
      }
    }
    if (fileSystemScheme != null) {
      command.addAll(<String>['--filesystem-scheme', fileSystemScheme]);
    }

    if (extraFrontEndOptions != null)
      command.addAll(extraFrontEndOptions);
    command.add(mainPath);
    printTrace(command.join(' '));
    final Process server = await processManager
        .start(command)
        .catchError((dynamic error, StackTrace stack) {
      printError('Failed to start frontend server $error, $stack');
    });

    final _StdoutHandler stdoutHandler = new _StdoutHandler();

    server.stderr
      .transform(utf8.decoder)
      .listen((String s) { printError('compiler message: $s'); });
    server.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen(stdoutHandler.handler);
    final int exitCode = await server.exitCode;
    return exitCode == 0 ? stdoutHandler.compilerOutput.future : null;
  }
}

/// Wrapper around incremental frontend server compiler, that communicates with
/// server via stdin/stdout.
///
/// The wrapper is intended to stay resident in memory as user changes, reloads,
/// restarts the Flutter app.
class ResidentCompiler {
  ResidentCompiler(this._sdkRoot, {bool trackWidgetCreation: false,
      String packagesPath, List<String> fileSystemRoots, String fileSystemScheme ,
      CompilerMessageConsumer compilerMessageConsumer: printError})
    : assert(_sdkRoot != null),
      _trackWidgetCreation = trackWidgetCreation,
      _packagesPath = packagesPath,
      _fileSystemRoots = fileSystemRoots,
      _fileSystemScheme = fileSystemScheme,
      stdoutHandler = new _StdoutHandler(consumer: compilerMessageConsumer) {
    // This is a URI, not a file path, so the forward slash is correct even on Windows.
    if (!_sdkRoot.endsWith('/'))
      _sdkRoot = '$_sdkRoot/';
  }

  final bool _trackWidgetCreation;
  final String _packagesPath;
  final List<String> _fileSystemRoots;
  final String _fileSystemScheme;
  String _sdkRoot;
  Process _server;
  final _StdoutHandler stdoutHandler;

  /// If invoked for the first time, it compiles Dart script identified by
  /// [mainPath], [invalidatedFiles] list is ignored.
  /// On successive runs [invalidatedFiles] indicates which files need to be
  /// recompiled. If [mainPath] is [null], previously used [mainPath] entry
  /// point that is used for recompilation.
  /// Binary file name is returned if compilation was successful, otherwise
  /// null is returned.
  Future<CompilerOutput> recompile(String mainPath, List<String> invalidatedFiles,
      {String outputPath, String packagesFilePath}) async {
    stdoutHandler.reset();

    // First time recompile is called we actually have to compile the app from
    // scratch ignoring list of invalidated files.
    if (_server == null)
      return _compile(_mapFilename(mainPath), outputPath, _mapFilename(packagesFilePath));

    final String inputKey = new Uuid().generateV4();
    _server.stdin.writeln('recompile ${mainPath != null ? _mapFilename(mainPath) + " ": ""}$inputKey');
    for (String fileUri in invalidatedFiles) {
      _server.stdin.writeln(_mapFileUri(fileUri));
    }
    _server.stdin.writeln(inputKey);

    return stdoutHandler.compilerOutput.future;
  }

  Future<CompilerOutput> _compile(String scriptFilename, String outputPath,
      String packagesFilePath) async {
    final String frontendServer = artifacts.getArtifactPath(
      Artifact.frontendServerSnapshotForEngineDartSdk
    );
    final List<String> command = <String>[
      artifacts.getArtifactPath(Artifact.engineDartBinary),
      frontendServer,
      '--sdk-root',
      _sdkRoot,
      '--incremental',
      '--strong',
      '--target=flutter',
    ];
    if (outputPath != null) {
      command.addAll(<String>['--output-dill', outputPath]);
    }
    if (packagesFilePath != null) {
      command.addAll(<String>['--packages', packagesFilePath]);
    }
    if (_trackWidgetCreation) {
      command.add('--track-widget-creation');
    }
    if (_packagesPath != null) {
      command.addAll(<String>['--packages', _packagesPath]);
    }
    if (_fileSystemRoots != null) {
      for (String root in _fileSystemRoots) {
        command.addAll(<String>['--filesystem-root', root]);
      }
    }
    if (_fileSystemScheme != null) {
      command.addAll(<String>['--filesystem-scheme', _fileSystemScheme]);
    }
    printTrace(command.join(' '));
    _server = await processManager.start(command);
    _server.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen(
        stdoutHandler.handler,
        onDone: () {
          // when outputFilename future is not completed, but stdout is closed
          // process has died unexpectedly.
          if (!stdoutHandler.compilerOutput.isCompleted) {
            stdoutHandler.compilerOutput.complete(null);
          }
        });

    _server.stderr
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen((String s) { printError('compiler message: $s'); });

    _server.stdin.writeln('compile $scriptFilename');

    return stdoutHandler.compilerOutput.future;
  }


  /// Should be invoked when results of compilation are accepted by the client.
  ///
  /// Either [accept] or [reject] should be called after every [recompile] call.
  void accept() {
    _server.stdin.writeln('accept');
  }

  /// Should be invoked when results of compilation are rejected by the client.
  ///
  /// Either [accept] or [reject] should be called after every [recompile] call.
  void reject() {
    _server.stdin.writeln('reject');
  }

  /// Should be invoked when frontend server compiler should forget what was
  /// accepted previously so that next call to [recompile] produces complete
  /// kernel file.
  void reset() {
    _server.stdin.writeln('reset');
  }

  String _mapFilename(String filename) {
    if (_fileSystemRoots != null) {
      for (String root in _fileSystemRoots) {
        if (filename.startsWith(root)) {
          return new Uri(
              scheme: _fileSystemScheme, path: filename.substring(root.length))
              .toString();
        }
      }
    }
    return filename;
  }

  String _mapFileUri(String fileUri) {
    if (_fileSystemRoots != null) {
      final String filename = Uri.parse(fileUri).toFilePath();
      for (String root in _fileSystemRoots) {
        if (filename.startsWith(root)) {
          return new Uri(
              scheme: _fileSystemScheme, path: filename.substring(root.length))
              .toString();
        }
      }
    }
    return fileUri;
  }

  Future<dynamic> shutdown() {
    _server.kill();
    return _server.exitCode;
  }
}