// Copyright (c) 2019, 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.

// See the Mustachio README at tool/mustachio/README.md for high-level
// documentation.

/// @docImport 'package:source_span/source_span.dart';
library;

import 'dart:collection';
import 'dart:convert' show htmlEscape;

import 'package:analyzer/file_system/file_system.dart';
import 'package:meta/meta.dart';

import 'parser.dart';

// TODO(devoncarew): See is we can make this synchronous.
/// The signature of a partial resolver function.
typedef PartialResolver = Future<File> Function(String uri);

/// The signature of a generated render function.
typedef RenderFunction<T> = String Function(T context, Template template);

/// A parsed Mustache template.
///
/// This container includes Templates for all partials parsed in the template
/// file.
class Template {
  /// The parsed Mustache syntax tree.
  final List<MustachioNode> ast;

  /// A mapping of partial keys to partial [File]s.
  ///
  /// This may appear redundant with [partialTemplates], the mapping of partial
  /// files to partial templates. The reason for two mappings is that
  /// [partialTemplates] functions as a cache, so that each [File] containing a
  /// partial template is only parsed once, and also so that partial templates
  /// can reference themselves, or otherwise recurse. The cache is passed down
  /// from template to partial. Mapping a partial key to a partial [File],
  /// however, cannot be shared between templates and partials because they
  /// typically contain relative file paths. Each partial file path is given
  /// relative to the template or partial making the reference.
  // TODO(srawlins): Resolving partials from the Parser rather than
  // [Template.parse] would allow the [File] to be stored on the [Partial] node,
  // removing the need for this mapping.
  final Map<String, File> partials;

  /// A mapping of partial [File]s to parsed Mustache templates.
  final Map<File, Template> partialTemplates;

  Template._(
      {required this.ast,
      required this.partials,
      required this.partialTemplates});

  /// Parses [file] as a Mustache template, returning a [Template].
  ///
  /// A [partialResolver] can be passed if custom partial resolution is required
  /// (for example, to load any partial from a single directory, and to append a
  /// particular suffix).
  ///
  /// By default, a partial key is resolved as a file path which is either
  /// absolute, or relative to the directory containing the template which
  /// references the partial.
  ///
  /// For example, if the Mustache template at `/foo/template.html` includes a
  /// Partial node, `{{>partials/p1.html}}`, then the partial file path is
  /// resolved as `/foo/partials/p1.html`. If this template includes a Partial
  /// node, `{{>p2.html}}`, then this partial file path is resolved (relative to
  /// the directory containing `p1.html`, not relative to the top-level
  /// template), as `/foo/partials/p2.html`.
  static Future<Template> parse(File file,
      {PartialResolver? partialResolver,
      @internal Map<File, Template>? partialTemplates}) async {
    partialTemplates ??= <File, Template>{};
    if (partialResolver == null) {
      var pathContext = file.provider.pathContext;
      // By default, resolve partials as absolute file paths, or as relative
      // file paths, relative to the template directory from which they are
      // referenced.
      partialResolver = (String path) async {
        var partialPath = pathContext.isAbsolute(path)
            ? path
            : pathContext.join(file.parent.path, path);
        var partialFile =
            file.provider.getFile(pathContext.normalize(partialPath));
        return partialFile;
      };
    }

    // If we fail to read [file], and an exception is thrown, one of two
    // things happen:
    // 1) In the case of a reference from a partial, the exception is caught
    //    below, when parsing partials, and rethrown with information about the
    //    template with the reference.
    // 2) In the case of a reference from a top-level template, user code has
    //    called [Template.parse], and the user is responsible for handling the
    //    exception.
    var ast = MustachioParser(file.readAsStringSync(), file.path).parse();
    var nodeQueue = Queue.of(ast);
    var partials = <String, File>{};

    // Walk the Mustache syntax tree, looking for Partial nodes.
    while (nodeQueue.isNotEmpty) {
      switch (nodeQueue.removeFirst()) {
        case Text():
          break;
        case Variable():
          break;
        case Section(:var children):
          nodeQueue.addAll(children);
        case Partial(:var span, :var key):
          if (!partials.containsKey(key)) {
            partials[key] = await partialResolver(key);
          }
          var partialFile = partials[key]!;
          if (!partialTemplates.containsKey(partialFile)) {
            try {
              var partialTemplate = await Template.parse(partialFile,
                  partialResolver: partialResolver,
                  partialTemplates: {...partialTemplates});
              partialTemplates[partialFile] = partialTemplate;
            } on FileSystemException catch (e) {
              throw MustachioResolutionException(span.message(
                  'FileSystemException (${e.message}) when reading partial:'));
            }
          }
      }
    }

    return Template._(
        ast: ast, partials: partials, partialTemplates: partialTemplates);
  }
}

/// The base class for a generated Mustache renderer.
abstract class RendererBase<T extends Object?> {
  /// The context object which this renderer can render.
  final T context;

  /// The renderer of the parent context, if any, otherwise `null`.
  final RendererBase<Object?>? parent;

  /// The current template being rendered.
  ///
  /// When rendering a partial, the [context] object, and hence the
  /// [RendererBase] does not change, only this current template.
  Template _template;

  /// The output buffer into which [context] is rendered, using a template.
  final StringSink sink;

  final Set<String> _invisibleGetters;

  RendererBase(
    this.context,
    this.parent,
    this._template,
    this.sink, {
    Set<String> invisibleGetters = const {},
  }) : _invisibleGetters = invisibleGetters;

  Template get template => _template;

  void write(String text) => sink.write(text);

  String get contextChainString =>
      parent == null ? '$T' : '${parent!.contextChainString} > $T';

  /// Returns the [Property] on this renderer named [key].
  ///
  /// If no property named [key] exists for this renderer, `null` is returned.
  Property<T>? getProperty(String key);

  /// Resolves [node] into one or more field accesses, returning the result as
  /// a String.
  ///
  /// [node]'s `key` may have multiple dot-separate names, and it may not be a
  /// valid property of _this_ context type, in which the [parent] renderer is
  /// referenced.
  String getFields(Variable node) {
    var names = node.key;
    if (names.length == 1 && names.single == '.') {
      return context.toString();
    }
    var firstName = names.first;
    try {
      var property = getProperty(firstName);
      var remainingNames = [...names.skip(1)];
      if (property != null) {
        try {
          return property.renderVariable(context, property, remainingNames);
        } on PartialMustachioResolutionException catch (e) {
          // The exception thrown by [Property.renderVariable] does not have all
          // of the names required for a decent error. We throw a new error
          // here.
          throw MustachioResolutionException(node.keySpan.message(
              "Failed to resolve '${e.name}' on ${e.contextType} while "
              'resolving $remainingNames as a property chain on any types in '
              'the context chain: $contextChainString, after first resolving '
              "'$firstName' to a property on $T"));
        }
      }
    } on _MustachioResolutionExceptionWithoutSpan catch (e) {
      throw MustachioResolutionException(node.keySpan.message(e.message));
    }

    final parent = this.parent;
    if (parent != null) {
      return parent.getFields(node);
    } else {
      throw MustachioResolutionException(node.keySpan.message(
          "Failed to resolve '${names.first}' as a property on any types in "
          'the context chain: $contextChainString'));
    }
  }

  /// Renders a block of Mustache template, the [ast], into [sink].
  void renderBlock(List<MustachioNode> ast) {
    for (var node in ast) {
      switch (node) {
        case Text():
          write(node.content);
        case Variable():
          var content = getFields(node);
          if (node.escape) {
            write(htmlEscape.convert(content));
          } else {
            write(content);
          }
        case Section():
          section(node);
        case Partial():
          partial(node);
      }
    }
  }

  void section(Section node) {
    var key = node.key.first;
    var property = getProperty(key);
    if (property == null) {
      final parent = this.parent;
      if (parent == null) {
        throw MustachioResolutionException(node.keySpan.message(
            "Failed to resolve '$key' as a property on any types in the "
            'current context'));
      } else {
        return parent.section(node);
      }
    }

    var getBool = property.getBool;
    if (getBool != null) {
      var boolResult = getBool(context);
      if ((boolResult && !node.invert) || (!boolResult && node.invert)) {
        renderBlock(node.children);
      }
      return;
    }

    var renderIterable = property.renderIterable;
    if (renderIterable != null) {
      var renderedIterable = renderIterable(context, this, node.children, sink);
      if (node.invert && renderedIterable.isEmpty) {
        // An inverted section is rendered with the current context.
        renderBlock(node.children);
      } else if (!node.invert && renderedIterable.isNotEmpty) {
        renderedIterable.toList(growable: false);
      }
      // Otherwise, render nothing.

      return;
    }

    // If this section is not a conditional or repeated section, it is a value
    // section, regardless of type.
    if (node.invert && property.isNullValue(context)) {
      renderBlock(node.children);
    } else if (!node.invert && !property.isNullValue(context)) {
      property.renderValue!(context, this, node.children, sink);
    }
  }

  void partial(Partial node) {
    var key = node.key;
    var partialFile = template.partials[key];
    var partialTemplate = template.partialTemplates[partialFile]!;
    var outerTemplate = _template;
    _template = partialTemplate;
    renderBlock(partialTemplate.ast);
    _template = outerTemplate;
  }
}

String renderSimple(Object? context, List<MustachioNode> ast, Template template,
    StringSink sink,
    {required RendererBase<Object> parent, required Set<String> getters}) {
  var renderer = SimpleRenderer(context, parent, template, sink, getters);
  renderer.renderBlock(ast);
  return renderer.sink.toString();
}

class SimpleRenderer extends RendererBase<Object?> {
  SimpleRenderer(
    super.context,
    RendererBase<Object> super.parent,
    super.template,
    super.sink,
    Set<String> invisibleGetters,
  ) : super(invisibleGetters: invisibleGetters);

  @override
  Property<Object>? getProperty(String key) {
    if (_invisibleGetters.contains(key)) {
      throw MustachioResolutionException(_failedKeyVisibilityMessage(key));
    } else {
      return null;
    }
  }

  @override
  String getFields(Variable node) {
    var names = node.key;
    var firstName = node.key.first;
    if (names.length == 1 && firstName == '.') {
      return context.toString();
    } else if (_invisibleGetters.contains(firstName)) {
      throw MustachioResolutionException(
          _failedKeyVisibilityMessage(firstName));
    } else if (parent != null) {
      return parent!.getFields(node);
    } else {
      return 'null';
    }
  }

  String _failedKeyVisibilityMessage(String name) {
    var type = context.runtimeType;
    return '[$name] is a getter on $type, which is not visible to Mustache. '
        'To render [$name] on $type, make it visible to Mustache via the '
        '`visibleTypes` parameter on `@Renderer`; to render [$name] on a '
        'different type up in the context stack, perhaps provide [$name] via '
        'a different name.';
  }
}

/// An individual property of objects of type [T], including functions for
/// rendering various types of Mustache nodes.
@immutable
class Property<T extends Object?> {
  /// Gets the value of this property on the object `context`.
  final Object? Function(T context) getValue;

  final String Function(T, Property<T>, List<String>) renderVariable;

  /// Gets the bool value (true or false, never null) of this property on the
  /// object `context`.
  final bool Function(T context)? getBool;

  final Iterable<void> Function(
      T, RendererBase<T>, List<MustachioNode>, StringSink)? renderIterable;

  final bool Function(T) isNullValue;

  final void Function(T, RendererBase<T>, List<MustachioNode>, StringSink)?
      renderValue;

  Property(
      {required this.getValue,
      required this.renderVariable,
      this.getBool,
      this.renderIterable,
      // TODO(jcollins-g): consider making this required or emitting warnings
      // if called on a non-nullable?
      bool Function(T)? isNullValue,
      this.renderValue})
      : isNullValue = (isNullValue ?? (_) => false);

  String renderSimpleVariable(
      T c, List<String> remainingNames, String typeString) {
    if (remainingNames.isEmpty) {
      return getValue(c).toString();
    } else {
      throw MustachioResolutionException(
          _simpleResolveErrorMessage(remainingNames, typeString));
    }
  }

  static String _simpleResolveErrorMessage(List<String> key, String type) =>
      'Failed to resolve $key property chain on $type using a simple renderer; '
      'expose the properties of $type by adding it to the @Renderer '
      "annotation's 'visibleTypes' list";
}

/// An exception indicating that a renderer failed to resolve a key.
class MustachioResolutionException implements Exception {
  final String message;

  MustachioResolutionException(this.message);

  @override
  String toString() => 'MustachioResolutionException: $message';
}

/// An exception indicating that a renderer failed to resolve a follow-on name
/// in a multi-name key.
class PartialMustachioResolutionException implements Exception {
  final String name;

  final Type contextType;

  PartialMustachioResolutionException(this.name, this.contextType);
}

/// A Mustachio resolution exception which is thrown in a position where the
/// AST node is not known.
///
/// This exception should be caught and "re-thrown" as a
/// [MustachioResolutionException] with a message derived from a [SourceSpan].
class _MustachioResolutionExceptionWithoutSpan implements Exception {
  final String message;

  _MustachioResolutionExceptionWithoutSpan(this.message);
}

extension MapExtensions<T> on Map<String, Property<T>> {
  Property<T> getValue(String name) {
    if (containsKey(name)) {
      return this[name]!;
    } else {
      throw PartialMustachioResolutionException(name, T);
    }
  }
}
