# -*- coding: utf-8 -*-
r"""
Attributes of Definitions

While a definition like 'cube[$x_$] = $x$^3' gives a way to specify <em>values</em> of a function, <em>attributes</em> allow a way to specify general properties of functions and symbols. This is independent of the parameters they take and the values they produce.

The builtin-attributes having a predefined meaning in \Mathics which are described below.

However in contrast to \Mathematica, you can set any symbol as an attribute.
"""


from mathics.builtin.base import Predefined, Builtin
from mathics.builtin.assignments.internals import get_symbol_list
from mathics.core.atoms import String
from mathics.core.attributes import (
    attributes_bitset_to_list,
    attribute_string_to_number,
    hold_all,
    hold_first,
    listable,
    locked,
    protected,
)
from mathics.core.expression import Expression
from mathics.core.list import ListExpression
from mathics.core.symbols import Symbol, SymbolNull

SymbolClearAttributes = Symbol("ClearAttributes")
SymbolSetAttributes = Symbol("SetAttributes")
SymbolProtected = Symbol("Protected")


class Attributes(Builtin):
    """
    <dl>
      <dt>'Attributes'[$symbol$]
      <dd>returns the attributes of $symbol$.

      <dt>'Attributes'["$string$"]
      <dd>returns the attributes of 'Symbol'["$string$"].

      <dt>'Attributes'[$symbol$] = {$attr1$, $attr2$}
      <dd>sets the attributes of $symbol$, replacing any existing attributes.
    </dl>

    >> Attributes[Plus]
     = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected}

    >> Attributes["Plus"]
     = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected}

    'Attributes' always considers the head of an expression:
    >> Attributes[a + b + c]
     = {Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected}

    You can assign values to 'Attributes' to set attributes:

    >> Attributes[f] = {Flat, Orderless}
     = {Flat, Orderless}
    >> f[b, f[a, c]]
     = f[a, b, c]
    Attributes must be symbols:
    >> Attributes[f] := {a + b}
     : Argument a + b at position 1 is expected to be a symbol.
     = $Failed
    Use 'Symbol' to convert strings to symbols:
    >> Attributes[f] = Symbol["Listable"]
     = Listable
    >> Attributes[f]
     = {Listable}
    """

    attributes = hold_all | listable | protected
    summary_text = "find the attributes of a symbol"

    def apply(self, expr, evaluation):
        "Attributes[expr_]"

        if isinstance(expr, String):
            expr = Symbol(expr.get_string_value())
        name = expr.get_lookup_name()

        attributes = attributes_bitset_to_list(
            evaluation.definitions.get_attributes(name)
        )
        attributes_symbols = [Symbol(attribute) for attribute in attributes]
        return ListExpression(*attributes_symbols)


class SetAttributes(Builtin):
    """
    <dl>
    <dt>'SetAttributes'[$symbol$, $attrib$]
        <dd>adds $attrib$ to the list of $symbol$'s attributes.
    </dl>

    >> SetAttributes[f, Flat]
    >> Attributes[f]
     = {Flat}

    Multiple attributes can be set at the same time using lists:
    >> SetAttributes[{f, g}, {Flat, Orderless}]
    >> Attributes[g]
     = {Flat, Orderless}
    """

    attributes = hold_first | protected

    messages = {
        "unknownattr": f"`1` should be one of {', '.join(attribute_string_to_number.keys())}"
    }
    summary_text = "set attributes for a symbol"

    def apply(self, symbols, attributes, evaluation):
        "SetAttributes[symbols_, attributes_]"

        symbols = get_symbol_list(
            symbols, lambda item: evaluation.message("SetAttributes", "sym", item, 1)
        )
        if symbols is None:
            return
        values = get_symbol_list(
            attributes, lambda item: evaluation.message("SetAttributes", "sym", item, 2)
        )
        if values is None:
            return
        for symbol in symbols:
            if locked & evaluation.definitions.get_attributes(symbol):
                evaluation.message("SetAttributes", "locked", Symbol(symbol))
            else:
                for value in values:
                    try:
                        evaluation.definitions.set_attribute(
                            symbol, attribute_string_to_number[value]
                        )
                    except KeyError:
                        evaluation.message("SetAttributes", "unknowattr", value)
        return SymbolNull


class ClearAttributes(Builtin):
    """
    <dl>
    <dt>'ClearAttributes'[$symbol$, $attrib$]
        <dd>removes $attrib$ from $symbol$'s attributes.
    </dl>

    >> SetAttributes[f, Flat]
    >> Attributes[f]
     = {Flat}
    >> ClearAttributes[f, Flat]
    >> Attributes[f]
     = {}
    Attributes that are not even set are simply ignored:
    >> ClearAttributes[{f}, {Flat}]
    >> Attributes[f]
     = {}
    """

    attributes = hold_first | protected
    summary_text = "clear the attributes of a symbol"

    def apply(self, symbols, attributes, evaluation):
        "ClearAttributes[symbols_, attributes_]"

        symbols = get_symbol_list(
            symbols, lambda item: evaluation.message("ClearAttributes", "sym", item, 1)
        )
        if symbols is None:
            return
        values = get_symbol_list(
            attributes,
            lambda item: evaluation.message("ClearAttributes", "sym", item, 2),
        )
        if values is None:
            return
        for symbol in symbols:
            if locked & evaluation.definitions.get_attributes(symbol):
                evaluation.message("ClearAttributes", "locked", Symbol(symbol))
            else:
                for value in values:
                    evaluation.definitions.clear_attribute(
                        symbol, attribute_string_to_number[value]
                    )
        return SymbolNull


class Protect(Builtin):
    """
    <dl>
      <dt>'Protect'[$s1$, $s2$, ...]
      <dd>sets the attribute 'Protected' for the symbols $si$.

      <dt>'Protect'[$str1$, $str2$, ...]
      <dd>protects all symbols whose names textually match $stri$.
    </dl>

    >> A = {1, 2, 3};
    >> Protect[A]
    >> A[[2]] = 4;
     : Symbol A is Protected.
    >> A
     = {1, 2, 3}
    """

    attributes = hold_all | protected
    messages = {
        "ssym": "`1` is not a symbol or a string.",
    }
    summary_text = "protect a symbol against redefinitions"

    def apply(self, symbols, evaluation):
        "Protect[symbols___]"
        protected = SymbolProtected
        items = []

        if isinstance(symbols, Symbol):
            symbols = [symbols]
        elif isinstance(symbols, String):
            symbols = [symbols]
        elif isinstance(symbols, Expression):
            if symbols.get_head_name() in ("System`Sequence", "System`List"):
                symbols = symbols.elements
            else:
                evaluation.message("Protect", "ssym", symbols)
                return SymbolNull

        for symbol in symbols:
            if isinstance(symbol, Symbol):
                items.append(symbol)
            else:
                pattern = symbol.get_string_value()
                if not pattern or pattern == "":
                    evaluation.message("Protect", "ssym", symbol)
                    continue

                if pattern[0] == "`":
                    pattern = evaluation.definitions.get_current_context() + pattern[1:]
                names = evaluation.definitions.get_matching_names(pattern)
                for defn in names:
                    symbol = Symbol(defn)
                    if not locked & evaluation.definitions.get_attributes(defn):
                        items.append(symbol)

        Expression(SymbolSetAttributes, ListExpression(*items), protected).evaluate(
            evaluation
        )
        return SymbolNull


class Unprotect(Builtin):
    """
    <dl>
      <dt>'Unprotect'[$s1$, $s2$, ...]
      <dd>removes the attribute 'Protected' for the symbols $si$.

      <dt>'Unprotect'[$str$]
      <dd>unprotects symbols whose names textually match $str$.
    </dl>
    """

    attributes = hold_all | protected
    messages = {
        "ssym": "`1` is not a symbol or a string.",
    }
    summary_text = "remove protection against redefinitions"

    def apply(self, symbols, evaluation):
        "Unprotect[symbols___]"
        protected = SymbolProtected
        items = []
        if isinstance(symbols, Symbol):
            symbols = [symbols]
        elif isinstance(symbols, Expression):
            symbols = symbols.elements
        elif isinstance(symbols, String):
            symbols = [symbols]
        else:
            symbols = symbols.get_sequence()

        for symbol in symbols:
            if isinstance(symbol, Symbol):
                items.append(symbol)
            else:
                pattern = symbol.get_string_value()
                if not pattern or pattern == "":
                    evaluation.message("Unprotect", "ssym", symbol)
                    continue

                if pattern[0] == "`":
                    pattern = evaluation.definitions.get_current_context() + pattern[1:]
                names = evaluation.definitions.get_matching_names(pattern)
                for defn in names:
                    symbol = Symbol(defn)
                    if not locked & evaluation.definitions.get_attributes(defn):
                        items.append(symbol)

        Expression(
            SymbolClearAttributes,
            ListExpression(*items),
            protected,
        ).evaluate(evaluation)
        return SymbolNull


class Protected(Predefined):
    """
    <dl>
    <dt>'Protected'
        <dd>is an attribute that prevents values on a symbol from
        being modified.
    </dl>

    Values of 'Protected' symbols cannot be modified:
    >> Attributes[p] = {Protected};
    >> p = 2;
     : Symbol p is Protected.
    >> f[p] ^= 3;
     : Tag p in f[p] is Protected.
    >> Format[p] = "text";
     : Symbol p is Protected.

    However, attributes might still be set:
    >> SetAttributes[p, Flat]
    >> Attributes[p]
     = {Flat, Protected}
    Thus, you can easily remove the attribute 'Protected':
    >> Attributes[p] = {};
    >> p = 2
     = 2
    You can also use 'Protect' or 'Unprotect', resp.
    >> Protect[p]
    >> Attributes[p]
     = {Protected}
    >> Unprotect[p]

    If a symbol is 'Protected' and 'Locked', it can never be changed again:
    >> SetAttributes[p, {Protected, Locked}]
    >> p = 2
     : Symbol p is Protected.
     = 2
    >> Unprotect[p]
     : Symbol p is locked.
    """

    summary_text = "attribute of protected symbols"


class ReadProtected(Predefined):
    """
    <dl>
    <dt>'ReadProtected'
        <dd>is an attribute that prevents values on a symbol from
        being read.
    </dl>

    Values associated with 'ReadProtected' symbols cannot be seen in
    'Definition':
    >> ClearAll[p]
    >> p = 3;
    >> Definition[p]
     = p = 3
    >> SetAttributes[p, ReadProtected]
    >> Definition[p]
     = Attributes[p] = {ReadProtected}
    """

    summary_text = "attribute of symbols with hidden definitions"


class Locked(Predefined):
    """
    <dl>
    <dt>'Locked'
        <dd>is an attribute that prevents attributes on a symbol from
        being modified.
    </dl>

    The attributes of 'Locked' symbols cannot be modified:
    >> Attributes[lock] = {Flat, Locked};
    >> SetAttributes[lock, {}]
     : Symbol lock is locked.
    >> ClearAttributes[lock, Flat]
     : Symbol lock is locked.
    >> Attributes[lock] = {}
     : Symbol lock is locked.
     = {}
    >> Attributes[lock]
     = {Flat, Locked}

    However, their values might be modified (as long as they are not 'Protected' too):
    >> lock = 3
     = 3
    """

    summary_text = "keep all attributes locked (settable but not clearable)"


class Flat(Predefined):
    """
    <dl>
    <dt>'Flat'
        <dd>is an attribute that specifies that nested occurrences of
        a function should be automatically flattened.
    </dl>

    A symbol with the 'Flat' attribute represents an associative
    mathematical operation:
    >> SetAttributes[f, Flat]
    >> f[a, f[b, c]]
     = f[a, b, c]

    'Flat' is taken into account in pattern matching:
    >> f[a, b, c] /. f[a, b] -> d
     = f[d, c]

    #> SetAttributes[{u, v}, Flat]
    #> u[x_] := {x}
    #> u[]
     = u[]
    #> u[a]
     = {a}
    #> u[a, b]
     : Iteration limit of 1000 exceeded.
     = $Aborted
    #> u[a, b, c]
     : Iteration limit of 1000 exceeded.
     = $Aborted
    #> v[x_] := x
    #> v[]
     = v[]
    #> v[a]
     = a
    #> v[a, b] (* in Mathematica: Iteration limit of 4096 exceeded. *)
     = v[a, b]
    #> v[a, b, c] (* in Mathematica: Iteration limit of 4096 exceeded. *)
     : Iteration limit of 1000 exceeded.
     = $Aborted
    """

    summary_text = "attribute for associative symbols"


class Orderless(Predefined):
    """<dl>
        <dt>'Orderless'
        <dd>is an attribute that can be assigned to a symbol $f$ to
        indicate that the elements $ei$ in expressions of the form
        $f$[$e1$, $e2$, ...] should automatically be sorted into
        canonical order. This property is accounted for in pattern
        matching.
    </dl>

    The leaves of an 'Orderless' function are automatically sorted:
    >> SetAttributes[f, Orderless]
    >> f[c, a, b, a + b, 3, 1.0]
     = f[1., 3, a, b, c, a + b]

    A symbol with the 'Orderless' attribute represents a commutative
    mathematical operation.
    >> f[a, b] == f[b, a]
     = True

    'Orderless' affects pattern matching:
    >> SetAttributes[f, Flat]
    >> f[a, b, c] /. f[a, c] -> d
     = f[b, d]

    """

    summary_text = "attribute for commutative symbols"


class OneIdentity(Predefined):
    """
    <dl>
    <dt>'OneIdentity'
        <dd>is an attribute specifying that '$f$[$x$]' should be treated
        as equivalent to $x$ in pattern matching.
    </dl>

    'OneIdentity' affects pattern matching:
    >> SetAttributes[f, OneIdentity]
    >> a /. f[args___] -> {args}
     = {a}
    It does not affect evaluation:
    >> f[a]
     = f[a]
    """

    summary_text = "attribute for idempotent symbols"


class SequenceHold(Predefined):
    """
    <dl>
    <dt>'SequenceHold'
        <dd>is an attribute that prevents 'Sequence' objects from being
        spliced into a function's arguments.
    </dl>

    Normally, 'Sequence' will be spliced into a function:
    >> f[Sequence[a, b]]
     = f[a, b]
    It does not for 'SequenceHold' functions:
    >> SetAttributes[f, SequenceHold]
    >> f[Sequence[a, b]]
     = f[Sequence[a, b]]

    E.g., 'Set' has attribute 'SequenceHold' to allow assignment of sequences to variables:
    >> s = Sequence[a, b];
    >> s
     = Sequence[a, b]
    >> Plus[s]
     = a + b
    """

    summary_text = "attribute for symbols that do not expand sequences"


class HoldFirst(Predefined):
    """
    <dl>
    <dt>'HoldFirst'
        <dd>is an attribute specifying that the first argument of a
        function should be left unevaluated.
    </dl>

    >> Attributes[Set]
     = {HoldFirst, Protected, SequenceHold}
    """

    summary_text = "attribute for symbols that keep unevaluated their first element"


class HoldRest(Predefined):
    """
    <dl>
    <dt>'HoldRest'
        <dd>is an attribute specifying that all but the first argument
        of a function should be left unevaluated.
    </dl>

    >> Attributes[If]
     = {HoldRest, Protected}
    """

    summary_text = (
        "attribute for symbols that keep unevaluated all but their first element"
    )


class HoldAll(Predefined):
    """
    <dl>
    <dt>'HoldAll'
        <dd>is an attribute specifying that all arguments of a
        function should be left unevaluated.
    </dl>

    >> Attributes[Function]
     = {HoldAll, Protected}
    """

    summary_text = "attribute for symbols that keep unevaluated all their elements"


class HoldAllComplete(Predefined):
    """
    <dl>
    <dt>'HoldAllComplete'
        <dd>is an attribute that includes the effects of 'HoldAll' and
        'SequenceHold', and also protects the function from being
        affected by the upvalues of any arguments.
    </dl>

    'HoldAllComplete' even prevents upvalues from being used, and
    includes 'SequenceHold'.
    >> SetAttributes[f, HoldAllComplete]
    >> f[a] ^= 3;
    >> f[a]
     = f[a]
    >> f[Sequence[a, b]]
     = f[Sequence[a, b]]
    """

    summary_text = "attribute for symbols that keep unevaluated all their elements, and discards upvalues"


class NHoldAll(Predefined):
    """
    <dl>
    <dt>'NHoldAll'
        <dd>is an attribute that protects all arguments of a
        function from numeric evaluation.
    </dl>

    >> N[f[2, 3]]
     = f[2., 3.]
    >> SetAttributes[f, NHoldAll]
    >> N[f[2, 3]]
     = f[2, 3]
    """

    summary_text = "prevent numerical evaluation of elements"


class NHoldFirst(Predefined):
    """
    <dl>
    <dt>'NHoldFirst'
        <dd>is an attribute that protects the first argument of a
        function from numeric evaluation.
    </dl>
    """

    summary_text = "prevent numerical evaluation of the first element"


class NHoldRest(Predefined):
    """
    <dl>
    <dt>'NHoldRest'
        <dd>is an attribute that protects all but the first argument
        of a function from numeric evaluation.
    </dl>
    """

    summary_text = "prevent numerical evaluation of all but the first element"


class Listable(Predefined):
    """
    <dl>
    <dt>'Listable'
        <dd>is an attribute specifying that a function should be
        automatically applied to each element of a list.
    </dl>

    >> SetAttributes[f, Listable]
    >> f[{1, 2, 3}, {4, 5, 6}]
     = {f[1, 4], f[2, 5], f[3, 6]}
    >> f[{1, 2, 3}, 4]
     = {f[1, 4], f[2, 4], f[3, 4]}
    >> {{1, 2}, {3, 4}} + {5, 6}
     = {{6, 7}, {9, 10}}
    """

    summary_text = "automatically thread over lists appearing in arguments"


class Constant(Predefined):
    """
    <dl>
    <dt>'Constant'
        <dd>is an attribute that indicates that a symbol is a constant.
    </dl>

    Mathematical constants like 'E' have attribute 'Constant':
    >> Attributes[E]
     = {Constant, Protected, ReadProtected}

    Constant symbols cannot be used as variables in 'Solve' and
    related functions:
    >> Solve[x + E == 0, E]
     : E is not a valid variable.
     = Solve[x + E == 0, E]
    """

    summary_text = "treat as a constant in differentiation, etc"


class NumericFunction(Predefined):
    """
    <dl>
    <dt>'NumericFunction'
        <dd>is an attribute that indicates that a symbol is the head of a numeric function.
    </dl>

    Mathematical functions like 'Sqrt' have attribute 'NumericFunction':
    >> Attributes[Sqrt]
     = {Listable, NumericFunction, Protected}

    Expressions with a head having this attribute, and with all the leaves
    being numeric expressions, are considered numeric expressions:
    >> NumericQ[Sqrt[1]]
     = True
    >> NumericQ[a]=True; NumericQ[Sqrt[a]]
     = True
    >> NumericQ[a]=False; NumericQ[Sqrt[a]]
     = False
    """

    summary_text = "treat as a numeric function"
