// Copyright 2016 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 'package:charcode/charcode.dart';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:term_glyph/term_glyph.dart' as term_glyph;

import 'util/nullable.dart';
import 'utils.dart';
import 'value.dart';

/// An exception thrown by Sass.
///
/// {@category Compile}
@sealed
class SassException extends SourceSpanException {
  /// The Sass stack trace at the point this exception was thrown.
  ///
  /// This includes [span].
  Trace get trace => Trace([frameForSpan(span, "root stylesheet")]);

  FileSpan get span => super.span as FileSpan;

  /// The set of canonical stylesheet URLs that were loaded in the course of the
  /// compilation, before it failed.
  final Set<Uri> loadedUrls;

  SassException(super.message, FileSpan super.span, [Iterable<Uri>? loadedUrls])
      : loadedUrls =
            loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls);

  /// Converts this to a [MultiSpanSassException] with the additional [span] and
  /// [label].
  ///
  /// @nodoc
  @internal
  MultiSpanSassException withAdditionalSpan(FileSpan span, String label) =>
      MultiSpanSassException(message, this.span, "", {span: label}, loadedUrls);

  /// Returns a copy of this as a [SassRuntimeException] with [trace] as its
  /// Sass stack trace.
  ///
  /// @nodoc
  @internal
  SassRuntimeException withTrace(Trace trace) =>
      SassRuntimeException(message, span, trace, loadedUrls);

  /// Returns a copy of this with [loadedUrls] set to the given value.
  @internal
  SassException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      SassException(message, span, loadedUrls);

  String toString({Object? color}) {
    var buffer = StringBuffer()
      ..writeln("Error: $message")
      ..write(span.highlight(color: color));

    for (var frame in trace.toString().split("\n")) {
      if (frame.isEmpty) continue;
      buffer.writeln();
      buffer.write("  $frame");
    }
    return buffer.toString();
  }

  /// Returns the contents of a CSS stylesheet that will display this error
  /// message above the current page.
  String toCssString() {
    // Don't render the error message in Unicode for the inline comment, since
    // we can't be sure the user's default encoding is UTF-8.
    var wasAscii = term_glyph.ascii;
    term_glyph.ascii = true;
    var commentMessage = toString(color: false)
        // Replace comment-closing sequences in the error message with
        // visually-similar sequences that won't actually close the comment.
        .replaceAll("*/", "*∕")
        // If the original text contains CRLF newlines, replace them with LF
        // newlines to match the rest of the document.
        .replaceAll("\r\n", "\n");
    term_glyph.ascii = wasAscii;

    // For the string comment, render all non-US-ASCII characters as escape
    // sequences so that they'll show up even if the HTTP headers are set
    // incorrectly.
    var stringMessage = StringBuffer();
    for (var rune in SassString(toString(color: false)).toString().runes) {
      if (rune > 0x7F) {
        stringMessage
          ..writeCharCode($backslash)
          ..write(rune.toRadixString(16))
          ..writeCharCode($space);
      } else {
        stringMessage.writeCharCode(rune);
      }
    }

    return """
/* ${commentMessage.split("\n").join("\n * ")} */

body::before {
  font-family: "Source Code Pro", "SF Mono", Monaco, Inconsolata, "Fira Mono",
      "Droid Sans Mono", monospace, monospace;
  white-space: pre;
  display: block;
  padding: 1em;
  margin-bottom: 1em;
  border-bottom: 2px solid black;
  content: $stringMessage;
}""";
  }
}

/// A [SassException] that's also a [MultiSourceSpanException].
class MultiSpanSassException extends SassException
    implements MultiSourceSpanException {
  final String primaryLabel;
  final Map<FileSpan, String> secondarySpans;

  MultiSpanSassException(
    String message,
    FileSpan span,
    this.primaryLabel,
    Map<FileSpan, String> secondarySpans, [
    Iterable<Uri>? loadedUrls,
  ])  : secondarySpans = Map.unmodifiable(secondarySpans),
        super(message, span, loadedUrls);

  MultiSpanSassException withAdditionalSpan(FileSpan span, String label) =>
      MultiSpanSassException(
          message,
          this.span,
          primaryLabel,
          {
            ...secondarySpans,
            span: label,
          },
          loadedUrls);

  MultiSpanSassRuntimeException withTrace(Trace trace) =>
      MultiSpanSassRuntimeException(
        message,
        span,
        primaryLabel,
        secondarySpans,
        trace,
        loadedUrls,
      );

  MultiSpanSassException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      MultiSpanSassException(
        message,
        span,
        primaryLabel,
        secondarySpans,
        loadedUrls,
      );

  String toString({Object? color, String? secondaryColor}) {
    var useColor = false;
    String? primaryColor;
    if (color is String) {
      useColor = true;
      primaryColor = color;
    } else if (color == true) {
      useColor = true;
    }

    var buffer = StringBuffer("Error: $message\n");

    span
        .highlightMultiple(
          primaryLabel,
          secondarySpans,
          color: useColor,
          primaryColor: primaryColor,
          secondaryColor: secondaryColor,
        )
        .andThen(buffer.write);

    for (var frame in trace.toString().split("\n")) {
      if (frame.isEmpty) continue;
      buffer.writeln();
      buffer.write("  $frame");
    }
    return buffer.toString();
  }
}

/// An exception thrown by Sass while evaluating a stylesheet.
class SassRuntimeException extends SassException {
  final Trace trace;

  MultiSpanSassRuntimeException withAdditionalSpan(
    FileSpan span,
    String label,
  ) =>
      MultiSpanSassRuntimeException(
        message,
        this.span,
        "",
        {span: label},
        trace,
        loadedUrls,
      );

  SassRuntimeException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      SassRuntimeException(message, span, trace, loadedUrls);

  SassRuntimeException(
    String message,
    FileSpan span,
    this.trace, [
    Iterable<Uri>? loadedUrls,
  ]) : super(message, span, loadedUrls);
}

/// A [SassRuntimeException] that's also a [MultiSpanSassException].
class MultiSpanSassRuntimeException extends MultiSpanSassException
    implements SassRuntimeException {
  final Trace trace;

  MultiSpanSassRuntimeException(
    String message,
    FileSpan span,
    String primaryLabel,
    Map<FileSpan, String> secondarySpans,
    this.trace, [
    Iterable<Uri>? loadedUrls,
  ]) : super(message, span, primaryLabel, secondarySpans, loadedUrls);

  MultiSpanSassRuntimeException withAdditionalSpan(
    FileSpan span,
    String label,
  ) =>
      MultiSpanSassRuntimeException(
        message,
        this.span,
        primaryLabel,
        {...secondarySpans, span: label},
        trace,
        loadedUrls,
      );

  MultiSpanSassRuntimeException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      MultiSpanSassRuntimeException(
        message,
        span,
        primaryLabel,
        secondarySpans,
        trace,
        loadedUrls,
      );
}

/// An exception thrown when Sass parsing has failed.
///
/// {@category Parsing}
@sealed
class SassFormatException extends SassException
    implements SourceSpanFormatException {
  String get source => span.file.getText(0);

  int get offset => span.start.offset;

  /// @nodoc
  @internal
  MultiSpanSassFormatException withAdditionalSpan(
    FileSpan span,
    String label,
  ) =>
      MultiSpanSassFormatException(
          message,
          this.span,
          "",
          {
            span: label,
          },
          loadedUrls);

  /// @nodoc
  SassFormatException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      SassFormatException(message, span, loadedUrls);

  SassFormatException(super.message, super.span, [super.loadedUrls]);
}

/// A [SassFormatException] that's also a [MultiSpanFormatException].
///
/// {@category Parsing}
@sealed
class MultiSpanSassFormatException extends MultiSpanSassException
    implements MultiSourceSpanFormatException, SassFormatException {
  String get source => span.file.getText(0);

  int get offset => span.start.offset;

  MultiSpanSassFormatException withAdditionalSpan(
    FileSpan span,
    String label,
  ) =>
      MultiSpanSassFormatException(
          message,
          this.span,
          primaryLabel,
          {
            ...secondarySpans,
            span: label,
          },
          loadedUrls);

  MultiSpanSassFormatException withLoadedUrls(Iterable<Uri> loadedUrls) =>
      MultiSpanSassFormatException(
        message,
        span,
        primaryLabel,
        secondarySpans,
        loadedUrls,
      );

  MultiSpanSassFormatException(
    super.message,
    super.span,
    super.primaryLabel,
    super.secondarySpans, [
    super.loadedUrls,
  ]);
}

/// An exception thrown by SassScript.
///
/// This doesn't extends [SassException] because it doesn't (yet) have a
/// [FileSpan] associated with it. It's caught by Sass's internals and converted
/// to a [SassRuntimeException] with a source span and a stack trace.
class SassScriptException {
  /// The error message.
  final String message;

  /// Creates a [SassScriptException] with the given [message].
  ///
  /// The [argumentName] is the name of the Sass function argument that
  /// triggered this exception. If it's not null, it's automatically included in
  /// [message].
  SassScriptException(String message, [String? argumentName])
      : message = argumentName == null ? message : "\$$argumentName: $message";

  /// Converts this to a [SassException] with the given [span].
  SassException withSpan(FileSpan span) => SassException(message, span);

  String toString() => "$message\n\nBUG: This should include a source span!";
}

/// A [SassScriptException] that contains one or more additional spans to
/// display as points of reference.
class MultiSpanSassScriptException extends SassScriptException {
  /// See [MultiSourceSpanException.primaryLabel].
  final String primaryLabel;

  /// See [MultiSourceSpanException.secondarySpans].
  final Map<FileSpan, String> secondarySpans;

  MultiSpanSassScriptException(
    super.message,
    this.primaryLabel,
    Map<FileSpan, String> secondarySpans,
  ) : secondarySpans = Map.unmodifiable(secondarySpans);

  /// Converts this to a [SassException] with the given primary [span].
  MultiSpanSassException withSpan(FileSpan span) =>
      MultiSpanSassException(message, span, primaryLabel, secondarySpans);
}
