// Copyright 2021 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:async';

import 'package:source_span/source_span.dart';

import 'deprecation.dart';
import 'logger.dart';

/// An interface that exposes information about the current Sass evaluation.
///
/// This allows us to expose zone-scoped information without having to create a
/// new zone variable for each piece of information.
abstract interface class EvaluationContext {
  /// The current evaluation context.
  ///
  /// Throws [StateError] if there isn't a Sass stylesheet currently being
  /// evaluated.
  static EvaluationContext get current =>
      currentOrNull ??
      (throw StateError("No Sass stylesheet is currently being evaluated."));

  /// The current evaluation context, or `null` if none exists.
  static EvaluationContext? get currentOrNull =>
      switch (Zone.current[#_evaluationContext]) {
        EvaluationContext context => context,
        _ => null,
      };

  /// The current evaluation context, or null if there isn't a Sass stylesheet
  /// currently being evaluated.
  static EvaluationContext? get _currentOrNull {
    if (Zone.current[#_evaluationContext] case EvaluationContext context) {
      return context;
    } else {
      return null;
    }
  }

  /// Returns the span for the currently executing callable.
  ///
  /// For normal exception reporting, this should be avoided in favor of
  /// throwing [SassScriptException]s. It should only be used when calling APIs
  /// that require spans.
  ///
  /// Throws a [StateError] if there isn't a callable being invoked.
  FileSpan get currentCallableSpan;

  /// Prints a warning message associated with the current `@import` or function
  /// call.
  ///
  /// If [deprecation] is non-null, the warning is emitted as a deprecation
  /// warning of that type.
  void warn(String message, [Deprecation? deprecation]);
}

/// Prints a warning message associated with the current `@import` or function
/// call.
///
/// If [deprecation] is `true`, the warning is emitted as a deprecation warning.
///
/// This may only be called within a custom function or importer callback.
/// {@category Compile}
void warn(String message, {bool deprecation = false}) =>
    switch (EvaluationContext.currentOrNull) {
      var context? => context.warn(
          message,
          deprecation ? Deprecation.userAuthored : null,
        ),
      _ when deprecation => (const Logger.stderr()).warnForDeprecation(
          Deprecation.userAuthored,
          message,
        ),
      _ => (const Logger.stderr()).warn(message),
    };

/// Prints a deprecation warning with [message] of type [deprecation].
void warnForDeprecation(String message, Deprecation deprecation) =>
    switch (EvaluationContext.currentOrNull) {
      var context? => context.warn(message, deprecation),
      _ => (const Logger.stderr()).warnForDeprecation(deprecation, message),
    };

/// Prints a deprecation warning with [message] of type [deprecation],
/// using stderr if there is no [EvaluationContext.current].
void warnForDeprecationFromApi(String message, Deprecation deprecation) {
  if (EvaluationContext._currentOrNull case var context?) {
    context.warn(message, deprecation);
  } else {
    Logger.stderr().warnForDeprecation(deprecation, message);
  }
}

/// Runs [callback] with [context] as [EvaluationContext.current].
///
/// This is zone-based, so if [callback] is asynchronous [warn] is set for the
/// duration of that callback.
T withEvaluationContext<T>(EvaluationContext context, T callback()) =>
    runZoned(callback, zoneValues: {#_evaluationContext: context});
