// Copyright 2017 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;

import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:grinder/grinder.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';

import 'package:sass/src/util/map.dart';
import 'package:sass/src/util/nullable.dart';

/// The files to compile to synchronous versions.
final sources = const {
  'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
  'lib/src/async_compile.dart': 'lib/src/compile.dart',
  'lib/src/async_environment.dart': 'lib/src/environment.dart',
  'lib/src/async_import_cache.dart': 'lib/src/import_cache.dart',
};

/// Classes that are defined in the async version of a file and used as-is in
/// the sync version, and thus should not be copied over.
final _sharedClasses = const ['EvaluateResult'];

/// This is how we support both synchronous and asynchronous compilation modes.
///
/// Both modes are necessary. Synchronous mode is faster, works in sync-only
/// contexts, and allows us to support Node Sass's renderSync() method.
/// Asynchronous mode allows users to write async importers and functions in
/// both Dart and JS.
///
/// The logic for synchronous and asynchronous mode is identical, but the async
/// code needs to await every statement and expression evaluation in case they
/// do asynchronous work. To avoid duplicating logic, we hand-write asynchronous
/// code for the evaluator and the environment and use this task to compile it
/// to a synchronous equivalent.
@Task('Compile async code to synchronous code.')
void synchronize() {
  for (var (source, target) in sources.pairs) {
    File(target).writeAsStringSync(synchronizeFile(source));
    DartFmt.format(target);
  }
}

/// Returns the result of synchronizing [source].
String synchronizeFile(String source) {
  source = p.absolute(source);
  var visitor = _Visitor(File(source).readAsStringSync(), source);

  parseFile(
    path: source,
    featureSet: FeatureSet.latestLanguageVersion(),
  ).unit.accept(visitor);
  return visitor.result;
}

/// The visitor that traverses the asynchronous parse tree and converts it to
/// synchronous code.
///
/// To preserve the original whitespace and comments, this copies text from the
/// original source where possible. It tracks the [_position] at the end of the
/// text that's been written, and writes from that position to the new position
/// whenever text needs to be emitted.
class _Visitor extends RecursiveAstVisitor<void> {
  /// The source of the original asynchronous file.
  final String _source;

  /// The path from which [_source] was loaded.
  final String _path;

  /// The current position in [_source].
  var _position = 0;

  /// The buffer in which the text of the synchronous file is built up.
  final _buffer = StringBuffer();

  /// Returns the [SourceFile] which is being rewritten.
  ///
  /// This is only used for debugging and error reporting.
  SourceFile get _sourceFile =>
      SourceFile.fromString(_source, url: p.toUri(_path));

  /// The synchronous text.
  String get result {
    _buffer.write(_source.substring(_position));
    _position = _source.length;
    return _buffer.toString();
  }

  _Visitor(this._source, this._path) {
    var afterHeader = "\n".allMatches(_source).skip(3).first.end;
    _buffer.writeln(_source.substring(0, afterHeader));
    _buffer.writeln("""
// DO NOT EDIT. This file was generated from ${p.basename(_path)}.
// See tool/grind/synchronize.dart for details.
//
// Checksum: ${sha1.convert(utf8.encode(_source))}
//
// ignore_for_file: unused_import
""");

    if (p.basename(_path) == 'async_evaluate.dart') {
      _buffer.writeln();
      _buffer.writeln("import 'async_evaluate.dart' show EvaluateResult;");
      _buffer.writeln("export 'async_evaluate.dart' show EvaluateResult;");
      _buffer.writeln();
    } else if (p.basename(_path) == 'async_compile.dart') {
      _buffer.writeln();
      _buffer.writeln("export 'async_compile.dart';");
      _buffer.writeln();
    }

    _position = afterHeader;
  }

  void visitAwaitExpression(AwaitExpression node) {
    _skip(node.awaitKeyword);

    // Skip the space after "await" to work around dart-lang/dart_style#226.
    _position++;
    node.expression.accept(this);
  }

  void visitParenthesizedExpression(ParenthesizedExpression node) {
    if (node.expression is AwaitExpression) {
      _skip(node.leftParenthesis);
      node.expression.accept(this);
      _skip(node.rightParenthesis);
    } else {
      node.expression.accept(this);
    }
  }

  void visitBlockFunctionBody(BlockFunctionBody node) {
    _skip(node.keyword);
    node.visitChildren(this);
  }

  void visitClassDeclaration(ClassDeclaration node) {
    if (_sharedClasses.contains(node.name.lexeme)) {
      _skipNode(node);
    } else {
      for (var child in node.sortedCommentAndAnnotations) {
        child.accept(this);
      }
      _rename(node.name);
      node.typeParameters?.accept(this);
      node.extendsClause?.accept(this);
      node.withClause?.accept(this);
      node.implementsClause?.accept(this);
      node.nativeClause?.accept(this);
      node.members.accept(this);
    }
  }

  void visitGenericTypeAlias(GenericTypeAlias node) {
    if (_sharedClasses.contains(node.name.lexeme)) {
      _skipNode(node);
    } else {
      for (var child in node.sortedCommentAndAnnotations) {
        child.accept(this);
      }
      _rename(node.name);
      node.typeParameters?.accept(this);
      node.type.accept(this);
    }
  }

  void visitExpressionFunctionBody(ExpressionFunctionBody node) {
    _skip(node.keyword);
    node.visitChildren(this);
  }

  void visitFunctionDeclaration(FunctionDeclaration node) {
    for (var child in node.sortedCommentAndAnnotations) {
      child.accept(this);
    }
    node.returnType?.accept(this);
    _rename(node.name);
    node.functionExpression.accept(this);
  }

  void visitMethodDeclaration(MethodDeclaration node) {
    if (_synchronizeName(node.name.lexeme) != node.name.lexeme) {
      // If the file defines any asynchronous versions of synchronous functions,
      // remove them.
      _skipNode(node);
    } else {
      super.visitMethodDeclaration(node);
    }
  }

  void visitImportDirective(ImportDirective node) {
    _skipNode(node);
    var text = node.toString();
    if (!text.contains("dart:async")) {
      _buffer.write(text.replaceAll("async_", ""));
    }
  }

  void visitMethodInvocation(MethodInvocation node) {
    // Convert async utility methods to their synchronous equivalents.
    if (node
        case MethodInvocation(
          target: null,
          methodName: SimpleIdentifier(name: "mapAsync" || "putIfAbsentAsync"),
        )) {
      _writeTo(node);
      var arguments = node.argumentList.arguments;
      _write(arguments.first);

      _buffer.write(".${_synchronizeName(node.methodName.name)}");
      node.typeArguments.andThen(_write);
      _buffer.write("(");

      _position = arguments[1].beginToken.offset;
      for (var argument in arguments.skip(1)) {
        argument.accept(this);
      }
    } else {
      super.visitMethodInvocation(node);
    }
  }

  void visitSimpleIdentifier(SimpleIdentifier node) {
    _skip(node.token);
    _buffer.write(_synchronizeName(node.name));
  }

  void visitNamedType(NamedType node) {
    if (node.name.lexeme case "Future" || "FutureOr") {
      _skip(node.name);
      if (node.typeArguments case var typeArguments?) {
        _skip(typeArguments.leftBracket);
        typeArguments.arguments.first.accept(this);
        _skip(typeArguments.rightBracket);
      } else {
        _buffer.write("void");
      }
    } else if (node.name.lexeme == "Module") {
      _skipNode(node);
      _buffer.write("Module<Callable>");
    } else if (node.typeArguments == null) {
      _skip(node.name);
      _buffer.write(_synchronizeName(node.name.lexeme));
    } else {
      super.visitNamedType(node);
    }
  }

  /// Writes through [node]'s (synchronized) name.
  ///
  /// Assumes [node] has a name field with type [Token].
  void _rename(Token token) {
    _skip(token);
    _buffer.write(_synchronizeName(token.lexeme));
  }

  /// Writes [_source] to [_buffer] up to the beginning of [token], then puts
  /// [_position] after [token] so it doesn't get written.
  void _skip(Token? token) {
    if (token == null) return;
    if (token.offset < _position) {
      throw _alreadyEmittedException(_spanForToken(token));
    }

    _buffer.write(_source.substring(_position, token.offset));
    _position = token.end;
  }

  /// Writes [_source] to [_buffer] up to the beginning of [node], then puts
  /// [_position] after [node] so it doesn't get written.
  void _skipNode(AstNode node) {
    _writeTo(node);
    _position = node.endToken.end;
  }

  /// Writes [_source] to [_buffer] up to the beginning of [node].
  void _writeTo(AstNode node) {
    if (node.beginToken.offset < _position) {
      throw _alreadyEmittedException(_spanForNode(node));
    }

    _buffer.write(_source.substring(_position, node.beginToken.offset));
    _position = node.beginToken.offset;
  }

  /// Writes the contents of [node] to [_buffer].
  ///
  /// This leaves [_position] at the end of [node].
  void _write(AstNode node) {
    if (node.beginToken.offset < _position) {
      throw _alreadyEmittedException(_spanForNode(node));
    }

    _position = node.beginToken.offset;
    node.accept(this);
    _buffer.write(_source.substring(_position, node.endToken.end));
    _position = node.endToken.end;
  }

  /// Strips an "async" prefix or suffix from [name].
  String _synchronizeName(String name) {
    if (name.toLowerCase().startsWith('async')) {
      return name.substring('async'.length);
    } else if (name.toLowerCase().endsWith('async')) {
      return name.substring(0, name.length - 'async'.length);
    } else {
      return name;
    }
  }

  SourceSpanException _alreadyEmittedException(SourceSpan span) {
    var lines = _buffer.toString().split("\n");
    return SourceSpanException(
      "Node was already emitted. Last 3 lines:\n\n" +
          lines
              .slice(math.max(lines.length - 3, 0))
              .map((line) => "  $line")
              .join("\n") +
          "\n",
      span,
    );
  }

  /// Returns a [FileSpan] that represents [token]'s position in the source
  /// file.
  SourceSpan _spanForToken(Token token) =>
      _sourceFile.span(token.offset, token.end);

  /// Returns a [FileSpan] that represents [token]'s position in the source
  /// file.
  SourceSpan _spanForNode(AstNode node) =>
      _sourceFile.span(node.beginToken.offset, node.endToken.end);
}
