using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Numerics;

namespace Microsoft.Dafny;

static class AttributeBearingDeclaration {

  public static bool IsExtern(this IAttributeBearingDeclaration me, DafnyOptions options) =>
    options.AllowExterns && Attributes.Contains(me.Attributes, "extern");

  public static bool IsExplicitAxiom(this IAttributeBearingDeclaration me) =>
    Attributes.Contains(me.Attributes, "axiom");
}

public class Attributes : TokenNode {
  [ContractInvariantMethod]
  void ObjectInvariant() {
    Contract.Invariant(Name != null);
    Contract.Invariant(cce.NonNullElements(Args));
  }

  public static string AxiomAttributeName = "axiom";
  public static string ConcurrentAttributeName = "concurrent";
  public static string AssumeConcurrentAttributeName = "assume_concurrent";
  public static string ExternAttributeName = "extern";
  public static string VerifyAttributeName = "verify";
  public static string AutoGeneratedAttributeName = "auto_generated";

  public string Name;
  /*Frozen*/
  public readonly List<Expression> Args;

  public readonly Attributes Prev;
  public Attributes(string name, [Captured] List<Expression> args, Attributes prev) {
    Contract.Requires(name != null);
    Contract.Requires(cce.NonNullElements(args));
    Name = name;
    Args = args;
    Prev = prev;
  }

  public override string ToString() {
    string result = Prev?.ToString() + "{:" + Name;
    if (Args == null || !Args.Any()) {
      return result + "}";
    } else {
      var exprs = String.Join(", ", Args.Select(e => e.ToString()));
      return result + " " + exprs + "}";
    }
  }

  public static IEnumerable<Expression> SubExpressions(Attributes attrs) {
    return attrs.AsEnumerable().SelectMany(aa => aa.Args);
  }

  public static bool Contains(Attributes attrs, string nm) {
    Contract.Requires(nm != null);
    return attrs.AsEnumerable().Any(aa => aa.Name == nm);
  }

  /// <summary>
  /// Returns first occurrence of an attribute named <c>nm</c>, or <c>null</c> if there is no such
  /// attribute.
  /// </summary>
  [Pure]
  public static Attributes/*?*/ Find(Attributes attrs, string nm) {
    Contract.Requires(nm != null);
    return attrs.AsEnumerable().FirstOrDefault(attr => attr.Name == nm);
  }

  /// <summary>
  /// Returns true if "nm" is a specified attribute.  If it is, then:
  /// - if the attribute is {:nm true}, then value==true
  /// - if the attribute is {:nm false}, then value==false
  /// - if the attribute is anything else, then value returns as whatever it was passed in as.
  /// This method does NOT use type information of the attribute arguments, so it can safely
  /// be called very early during resolution before types are available and names have been resolved.
  /// </summary>
  [Pure]
  public static bool ContainsBool(Attributes attrs, string nm, ref bool value) {
    Contract.Requires(nm != null);
    var attr = attrs.AsEnumerable().FirstOrDefault(attr => attr.Name == nm);
    if (attr == null) {
      return false;
    }

    if (attr.Args.Count == 1 && attr.Args[0] is LiteralExpr { Value: bool v }) {
      value = v;
    }
    return true;
  }

  /// <summary>
  /// Checks whether a Boolean attribute has been set on the declaration itself,
  /// the enclosing class, or any enclosing module.  Settings closer to the declaration
  /// override those further away.
  /// </summary>
  public static bool ContainsBoolAtAnyLevel(MemberDecl decl, string attribName, bool defaultVal = false) {
    bool setting = true;
    if (Attributes.ContainsBool(decl.Attributes, attribName, ref setting)) {
      return setting;
    }

    if (Attributes.ContainsBool(decl.EnclosingClass.Attributes, attribName, ref setting)) {
      return setting;
    }

    // Check the entire stack of modules
    var mod = decl.EnclosingClass.EnclosingModuleDefinition;
    while (mod != null) {
      if (Attributes.ContainsBool(mod.Attributes, attribName, ref setting)) {
        return setting;
      }
      mod = mod.EnclosingModule;
    }

    return defaultVal;
  }

  /// <summary>
  /// Returns list of expressions if "nm" is a specified attribute:
  /// - if the attribute is {:nm e1,...,en}, then returns (e1,...,en)
  /// Otherwise, returns null.
  /// </summary>
  public static List<Expression> FindExpressions(Attributes attrs, string nm) {
    Contract.Requires(nm != null);
    foreach (var attr in attrs.AsEnumerable()) {
      if (attr.Name == nm) {
        return attr.Args;
      }
    }
    return null;
  }

  /// <summary>
  /// Same as FindExpressions, but returns all matches
  /// </summary>
  public static List<List<Expression>> FindAllExpressions(Attributes attrs, string nm) {
    Contract.Requires(nm != null);
    List<List<Expression>> ret = null;
    for (; attrs != null; attrs = attrs.Prev) {
      if (attrs.Name == nm) {
        ret = ret ?? new List<List<Expression>>();   // Avoid allocating the list in the common case where we don't find nm
        ret.Add(attrs.Args);
      }
    }
    return ret;
  }

  /// <summary>
  /// Returns true if "nm" is a specified attribute whose arguments match the "allowed" parameter.
  /// - if "nm" is not found in attrs, return false and leave value unmodified.  Otherwise,
  /// - if "allowed" contains Empty and the Args contains zero elements, return true and leave value unmodified.  Otherwise,
  /// - if "allowed" contains Bool and Args contains one bool literal, return true and set value to the bool literal.  Otherwise,
  /// - if "allowed" contains Int and Args contains one BigInteger literal, return true and set value to the BigInteger literal.  Otherwise,
  /// - if "allowed" contains String and Args contains one string literal, return true and set value to the string literal.  Otherwise,
  /// - if "allowed" contains Expression and Args contains one element, return true and set value to the one element (of type Expression).  Otherwise,
  /// - return false, leave value unmodified, and call reporter with an error string.
  /// </summary>
  public enum MatchingValueOption { Empty, Bool, Int, String, Expression }
  public static bool ContainsMatchingValue(Attributes attrs, string nm, ref object value, IEnumerable<MatchingValueOption> allowed, Action<string> reporter) {
    Contract.Requires(nm != null);
    Contract.Requires(allowed != null);
    Contract.Requires(reporter != null);
    List<Expression> args = FindExpressions(attrs, nm);
    if (args == null) {
      return false;
    } else if (args.Count == 0) {
      if (allowed.Contains(MatchingValueOption.Empty)) {
        return true;
      } else {
        reporter("Attribute " + nm + " requires one argument");
        return false;
      }
    } else if (args.Count == 1) {
      Expression arg = args[0];
      StringLiteralExpr stringLiteral = arg as StringLiteralExpr;
      LiteralExpr literal = arg as LiteralExpr;
      if (literal != null && literal.Value is bool && allowed.Contains(MatchingValueOption.Bool)) {
        value = literal.Value;
        return true;
      } else if (literal != null && literal.Value is BigInteger && allowed.Contains(MatchingValueOption.Int)) {
        value = literal.Value;
        return true;
      } else if (stringLiteral != null && stringLiteral.Value is string && allowed.Contains(MatchingValueOption.String)) {
        value = stringLiteral.Value;
        return true;
      } else if (allowed.Contains(MatchingValueOption.Expression)) {
        value = arg;
        return true;
      } else {
        reporter("Attribute " + nm + " expects an argument in one of the following categories: " + String.Join(", ", allowed));
        return false;
      }
    } else {
      reporter("Attribute " + nm + " cannot have more than one argument");
      return false;
    }
  }

  public override IEnumerable<INode> Children => Args.Concat<Node>(
    Prev == null
      ? Enumerable.Empty<Node>()
      : new List<Node> { Prev });

  public override IEnumerable<INode> PreResolveChildren => Children;
}

public static class AttributesExtensions {
  /// <summary>
  /// By making this an extension method, it can also be invoked for a null receiver.
  /// </summary>
  public static IEnumerable<Attributes> AsEnumerable(this Attributes attr) {
    while (attr != null) {
      yield return attr;
      attr = attr.Prev;
    }
  }
}

public class UserSuppliedAttributes : Attributes {
  public readonly IToken OpenBrace;
  public readonly IToken CloseBrace;
  public bool Recognized;  // set to true to indicate an attribute that is processed by some part of Dafny; this allows it to be colored in the IDE
  public UserSuppliedAttributes(IToken tok, IToken openBrace, IToken closeBrace, List<Expression> args, Attributes prev)
    : base(tok.val, args, prev) {
    Contract.Requires(tok != null);
    Contract.Requires(openBrace != null);
    Contract.Requires(closeBrace != null);
    Contract.Requires(args != null);
    this.tok = tok;
    OpenBrace = openBrace;
    CloseBrace = closeBrace;
  }
}

/// <summary>
/// A class implementing this interface is one that can carry attributes.
/// </summary>
public interface IAttributeBearingDeclaration {
  Attributes Attributes { get; }
}

public static class AttributeBearingDeclarationExtensions {
  public static bool HasUserAttribute(this IAttributeBearingDeclaration decl, string name, out Attributes attribute) {
    if (Attributes.Find(decl.Attributes, name) is UserSuppliedAttributes attr) {
      attribute = attr;
      return true;
    }

    attribute = null;
    return false;
  }
}