from abc import ABC, abstractmethod
from enum import Enum
from typing import (
    Any,
    Callable,
    Generic,
    Iterable,
    MutableMapping,
    Optional,
    Sequence,
    Tuple,
    TypeVar,
    Union,
)

import attr

import basilisp.lang.keyword as kw
import basilisp.lang.map as lmap
import basilisp.lang.set as lset
import basilisp.lang.symbol as sym
import basilisp.lang.vector as vec
from basilisp.lang.interfaces import IPersistentVector
from basilisp.lang.runtime import Namespace, Var, to_lisp
from basilisp.lang.typing import LispForm, ReaderForm as ReaderLispForm, SpecialForm
from basilisp.lang.util import munge

BODY = kw.keyword("body")
CLASS = kw.keyword("class")
LOCAL = kw.keyword("local")
STATEMENTS = kw.keyword("statements")
RET = kw.keyword("ret")
THIS_LOCAL = kw.keyword("this-local")
FIELDS = kw.keyword("fields")
METHODS = kw.keyword("methods")
PARAMS = kw.keyword("params")
TARGET = kw.keyword("target")
VAL = kw.keyword("val")
ARGS = kw.keyword("args")
FN = kw.keyword("fn")
BINDINGS = kw.keyword("bindings")
KEYS = kw.keyword("keys")
VALS = kw.keyword("vals")
EXPR = kw.keyword("expr")
EXPRS = kw.keyword("exprs")
ITEMS = kw.keyword("items")
EXCEPTION = kw.keyword("exception")
META = kw.keyword("meta")
TEST = kw.keyword("test")
THEN = kw.keyword("then")
ELSE = kw.keyword("else")

class NodeOp(Enum):
    AWAIT = kw.keyword("await")
    BINDING = kw.keyword("binding")
    CATCH = kw.keyword("catch")
    CONST = kw.keyword("const")
    DEF = kw.keyword("def")
    DEFTYPE = kw.keyword("deftype")
    DO = kw.keyword("do")
    FN = kw.keyword("fn")
    FN_METHOD = kw.keyword("fn-method")
    HOST_CALL = kw.keyword("host-call")
    HOST_FIELD = kw.keyword("host-field")
    HOST_INTEROP = kw.keyword("host-interop")
    IF = kw.keyword("if")
    IMPORT = kw.keyword("import")
    IMPORT_ALIAS = kw.keyword("import-alias")
    INVOKE = kw.keyword("invoke")
    LET = kw.keyword("let")
    LETFN = kw.keyword("letfn")
    LOCAL = kw.keyword("local")
    LOOP = kw.keyword("loop")
    MAP = kw.keyword("map")
    MAYBE_CLASS = kw.keyword("maybe-class")
    MAYBE_HOST_FORM = kw.keyword("maybe-host-form")
    METHOD = kw.keyword("method")
    PY_DICT = kw.keyword("py-dict")
    PY_LIST = kw.keyword("py-list")
    PY_SET = kw.keyword("py-set")
    PY_TUPLE = kw.keyword("py-tuple")
    QUOTE = kw.keyword("quote")
    RECUR = kw.keyword("recur")
    SET = kw.keyword("set")
    SET_BANG = kw.keyword("set!")
    THROW = kw.keyword("throw")
    TRY = kw.keyword("try")
    VAR = kw.keyword("var")
    VECTOR = kw.keyword("vector")
    WITH_META = kw.keyword("with-meta")

T = TypeVar("T")

class Node(ABC, Generic[T]):
    __slots__ = ()

    @property
    @abstractmethod
    def op(self) -> NodeOp:
        """Enumerated keyword uniquely identifying this type of Node.

        The type and NodeOp should always be in sync.

        Having a simple enum value in addition to the type allows us to use
        switch-like syntax with Python dictionaries in compiler code, which
        is much faster than performing isinstance checks."""

    @property
    @abstractmethod
    def form(self) -> T:
        """The original Lisp form corresponding to this Node."""

    @property
    @abstractmethod
    def children(self) -> Iterable[kw.Keyword]:
        """An iterable of keywords naming the attributes on the node which
        contain child nodes used for visiting all nodes in a tree.

        In most cases, children are safely defaulted at class definition.
        For certain nodes, the children may be variable and must be set at
        construction.

        Initially, this property was typed as a typing.Collection. In practice,
        children is always a Vector. However, MyPy seems to have trouble
        identifying IPersistentVector-typed attributes as typing.Collection, so
        this type was set as typing.Iterable which seems to work for now."""

    @property
    @abstractmethod
    def raw_forms(self) -> IPersistentVector[LispForm]:
        """A collection of intermediate forms produced by macros before emitting
        the final form.

        As with children above, this was formerly a more generic typing.Collection
        type, but MyPy was returning an error. If this error is ever fixed,
        this should be returned to a more protocol type."""

    @property
    @abstractmethod
    def top_level(self) -> bool:
        """True if this node is the root of the entire syntax tree, False
        otherwise.

        In practice, things such as def forms will end up being top level nodes,
        though at the REPL even something simple like a Const node could be top
        level."""

    @property
    @abstractmethod
    def env(self) -> "NodeEnv":
        """Details about the environment of the original form such as line and
        column numbers."""

    def to_map(self) -> lmap.Map:
        return to_lisp(attr.asdict(self))

    def assoc(self, **kwargs):
        return attr.evolve(self, **kwargs)

    def visit(self, f: Callable[..., None], *args, **kwargs):
        """Visit all immediate children of this node, calling
        f(child, *args, **kwargs) on each child."""
        for child_kw in self.children:
            child_attr = munge(child_kw.name)

            if child_attr.endswith("s"):
                iter_child: Iterable[Node] = getattr(self, child_attr)
                assert iter_child is not None, "Listed child must not be none"
                for item in iter_child:
                    f(item, *args, **kwargs)
            else:
                child: Node = getattr(self, child_attr)
                assert child is not None, "Listed child must not be none"
                f(child, *args, **kwargs)

    def fix_missing_locations(
        self, start_loc: Optional[Tuple[int, int]] = None
    ) -> "Node":
        """Return a transformed copy of this node with location in this node's
        environment updated to match the `start_loc` if given, or using its
        existing location otherwise. All child nodes will be recursively
        transformed and replaced. Child nodes will use their parent node
        location if they do not have one."""
        if self.env.line is None or self.env.col is None:
            loc = start_loc
        else:
            loc = (self.env.line, self.env.col)

        assert loc is not None and all(
            [e is not None for e in loc]
        ), "Must specify location information"

        new_attrs: MutableMapping[str, Union[NodeEnv, Node, Iterable[Node]]] = {
            "env": attr.evolve(self.env, line=loc[0], col=loc[1])
        }
        for child_kw in self.children:
            child_attr = munge(child_kw.name)
            assert child_attr != "env", "Node environment already set"

            if child_attr.endswith("s"):
                iter_child: Iterable[Node] = getattr(self, child_attr)
                assert iter_child is not None, "Listed child must not be none"
                new_children = []
                for item in iter_child:
                    new_children.append(item.fix_missing_locations(start_loc))
                new_attrs[child_attr] = vec.vector(new_children)
            else:
                child: Node = getattr(self, child_attr)
                assert child is not None, "Listed child must not be none"
                new_attrs[child_attr] = child.fix_missing_locations(start_loc)

        return self.assoc(**new_attrs)

class Assignable(ABC):
    __slots__ = ()

    @property
    @abstractmethod
    def is_assignable(self) -> bool:
        """True if this Node can be assigned in a set! form, False otherwise."""

class ConstType(Enum):
    NIL = kw.Keyword("nil")
    MAP = kw.keyword("map")
    SET = kw.keyword("set")
    VECTOR = kw.keyword("vector")
    BOOL = kw.keyword("bool")
    KEYWORD = kw.keyword("keyword")
    SYMBOL = kw.keyword("symbol")
    STRING = kw.keyword("string")
    NUMBER = kw.keyword("number")
    DECIMAL = kw.keyword("decimal")
    FRACTION = kw.keyword("fraction")
    RECORD = kw.keyword("record")
    SEQ = kw.keyword("seq")
    CHAR = kw.keyword("char")
    REGEX = kw.keyword("regex")
    CLASS = kw.keyword("class")
    INST = kw.keyword("inst")
    UUID = kw.keyword("uuid")
    PY_DICT = kw.keyword("py-dict")
    PY_LIST = kw.keyword("py-list")
    PY_SET = kw.keyword("py-set")
    PY_TUPLE = kw.keyword("py-tuple")
    UNKNOWN = kw.keyword("unknown")

NodeMeta = Union[None, "Const", "Map"]
LoopID = str

class LocalType(Enum):
    ARG = kw.keyword("arg")
    CATCH = kw.keyword("catch")
    DEFTYPE = kw.keyword("deftype")
    FIELD = kw.keyword("field")
    FN = kw.keyword("fn")
    LET = kw.keyword("let")
    LETFN = kw.keyword("letfn")
    LOOP = kw.keyword("loop")
    THIS = kw.keyword("this")

@attr.s(auto_attribs=True, frozen=True, slots=True)
class NodeEnv:
    ns: Namespace
    file: str
    line: Optional[int] = None
    col: Optional[int] = None

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Await(Node[ReaderLispForm]):
    form: ReaderLispForm
    expr: Node
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(EXPR)
    op: NodeOp = NodeOp.AWAIT
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Binding(Node[sym.Symbol], Assignable):
    form: sym.Symbol
    name: str
    local: LocalType
    env: NodeEnv
    arg_id: Optional[int] = None
    is_variadic: bool = False
    is_assignable: bool = False
    init: Optional[Node] = None
    meta: NodeMeta = None
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.BINDING
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Catch(Node[SpecialForm]):
    form: SpecialForm
    class_: Union["MaybeClass", "MaybeHostForm"]
    local: Binding
    body: "Do"
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(CLASS, LOCAL, BODY)
    op: NodeOp = NodeOp.CATCH
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Const(Node[ReaderLispForm]):
    form: ReaderLispForm
    type: ConstType
    val: ReaderLispForm
    is_literal: bool
    env: NodeEnv
    meta: NodeMeta = None
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.CONST
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Def(Node[SpecialForm]):
    form: SpecialForm
    name: sym.Symbol
    var: Var
    init: Optional[Node]
    doc: Optional[str]
    env: NodeEnv
    meta: NodeMeta = None
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.DEF
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

DefTypeBase = Union["MaybeClass", "MaybeHostForm", "VarRef"]

@attr.s(auto_attribs=True, frozen=True, slots=True)
class DefType(Node[SpecialForm]):
    form: SpecialForm
    name: str
    interfaces: Iterable[DefTypeBase]
    fields: Iterable[Binding]
    methods: Iterable["Method"]
    env: NodeEnv
    is_frozen: bool = True
    meta: NodeMeta = None
    children: Sequence[kw.Keyword] = vec.v(FIELDS, METHODS)
    op: NodeOp = NodeOp.DEFTYPE
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Do(Node[SpecialForm]):
    form: SpecialForm
    statements: Iterable[Node]
    ret: Node
    env: NodeEnv
    is_body: bool = False
    children: Sequence[kw.Keyword] = vec.v(STATEMENTS, RET)
    op: NodeOp = NodeOp.DO
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Fn(Node[SpecialForm]):
    form: SpecialForm
    max_fixed_arity: int
    methods: IPersistentVector["FnMethod"]
    env: NodeEnv
    local: Optional[Binding] = None
    is_variadic: bool = False
    is_async: bool = False
    children: Sequence[kw.Keyword] = vec.v(METHODS)
    op: NodeOp = NodeOp.FN
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class FnMethod(Node[SpecialForm]):
    form: SpecialForm
    loop_id: LoopID
    params: Iterable[Binding]
    fixed_arity: int
    body: Do
    env: NodeEnv
    is_variadic: bool = False
    children: Sequence[kw.Keyword] = vec.v(PARAMS, BODY)
    op: NodeOp = NodeOp.FN_METHOD
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class HostCall(Node[SpecialForm]):
    form: SpecialForm
    method: str
    target: Node
    args: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(TARGET, ARGS)
    op: NodeOp = NodeOp.HOST_CALL
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class HostField(Node[SpecialForm], Assignable):
    form: SpecialForm
    field: str
    target: Node
    env: NodeEnv
    is_assignable: bool = True
    children: Sequence[kw.Keyword] = vec.v(TARGET)
    op: NodeOp = NodeOp.HOST_FIELD
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class If(Node[SpecialForm]):
    form: SpecialForm
    test: Node
    then: Node
    env: NodeEnv
    else_: Node
    children: Sequence[kw.Keyword] = vec.v(TEST, THEN, ELSE)
    op: NodeOp = NodeOp.IF
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Import(Node[SpecialForm]):
    form: SpecialForm
    aliases: Iterable["ImportAlias"]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.IMPORT
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class ImportAlias(Node[Union[sym.Symbol, vec.Vector]]):
    form: Union[sym.Symbol, vec.Vector]
    name: str
    alias: Optional[str]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.IMPORT_ALIAS
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Invoke(Node[SpecialForm]):
    form: SpecialForm
    fn: Node
    args: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(FN, ARGS)
    op: NodeOp = NodeOp.INVOKE
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Let(Node[SpecialForm]):
    form: SpecialForm
    bindings: Iterable[Binding]
    body: Do
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(BINDINGS, BODY)
    op: NodeOp = NodeOp.LET
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class LetFn(Node[SpecialForm]):
    form: SpecialForm
    bindings: Iterable[Binding]
    body: Do
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(BINDINGS, BODY)
    op: NodeOp = NodeOp.LETFN
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Local(Node[sym.Symbol], Assignable):
    form: sym.Symbol
    name: str
    local: LocalType
    env: NodeEnv
    is_assignable: bool = False
    arg_id: Optional[int] = None
    is_variadic: bool = False
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.LOCAL
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Loop(Node[SpecialForm]):
    form: SpecialForm
    bindings: Iterable[Binding]
    body: Do
    loop_id: LoopID
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(BINDINGS, BODY)
    op: NodeOp = NodeOp.LOOP
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Map(Node[lmap.Map]):
    form: lmap.Map
    keys: Iterable[Node]
    vals: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(KEYS, VALS)
    op: NodeOp = NodeOp.MAP
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class MaybeClass(Node[sym.Symbol]):
    form: sym.Symbol
    class_: str
    target: Any
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.MAYBE_CLASS
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class MaybeHostForm(Node[sym.Symbol]):
    form: sym.Symbol
    class_: str
    field: str
    target: Any
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.MAYBE_HOST_FORM
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Method(Node[SpecialForm]):
    form: SpecialForm
    name: str
    interface: DefTypeBase
    this_local: Binding
    loop_id: LoopID
    params: Iterable[Binding]
    is_variadic: bool
    body: Do
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(THIS_LOCAL, PARAMS, BODY)
    op: NodeOp = NodeOp.METHOD
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, cmp=False, frozen=True, slots=True)
class PyDict(Node[dict]):
    form: dict
    keys: Iterable[Node]
    vals: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(KEYS, VALS)
    op: NodeOp = NodeOp.PY_DICT
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, cmp=False, frozen=True, slots=True)
class PyList(Node[list]):
    form: list
    items: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(ITEMS)
    op: NodeOp = NodeOp.PY_LIST
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, cmp=False, frozen=True, slots=True)
class PySet(Node[Union[frozenset, set]]):
    form: Union[frozenset, set]
    items: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(ITEMS)
    op: NodeOp = NodeOp.PY_SET
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class PyTuple(Node[tuple]):
    form: tuple
    items: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(ITEMS)
    op: NodeOp = NodeOp.PY_TUPLE
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Quote(Node[SpecialForm]):
    form: SpecialForm
    expr: Const
    env: NodeEnv
    is_literal: bool = True
    children: Sequence[kw.Keyword] = vec.v(EXPR)
    op: NodeOp = NodeOp.QUOTE
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Recur(Node[SpecialForm]):
    form: SpecialForm
    exprs: Iterable[Node]
    loop_id: LoopID
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(EXPRS)
    op: NodeOp = NodeOp.RECUR
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Set(Node[lset.Set]):
    form: lset.Set
    items: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(ITEMS)
    op: NodeOp = NodeOp.SET
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class SetBang(Node[SpecialForm]):
    form: SpecialForm
    target: Union[Assignable, Node]
    val: Node
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(TARGET, VAL)
    op: NodeOp = NodeOp.SET_BANG
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Throw(Node[SpecialForm]):
    form: SpecialForm
    exception: Node
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(EXCEPTION)
    op: NodeOp = NodeOp.THROW
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Try(Node[SpecialForm]):
    form: SpecialForm
    body: Do
    catches: Iterable[Catch]
    children: Sequence[kw.Keyword]
    env: NodeEnv
    finally_: Optional[Do] = None
    op: NodeOp = NodeOp.TRY
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class VarRef(Node[sym.Symbol], Assignable):
    form: sym.Symbol
    var: Var
    env: NodeEnv
    return_var: bool = False
    children: Sequence[kw.Keyword] = vec.Vector.empty()
    op: NodeOp = NodeOp.VAR
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

    @property
    def is_assignable(self) -> bool:
        return self.var.dynamic

@attr.s(auto_attribs=True, frozen=True, slots=True)
class Vector(Node[vec.Vector]):
    form: vec.Vector
    items: Iterable[Node]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(ITEMS)
    op: NodeOp = NodeOp.VECTOR
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

@attr.s(auto_attribs=True, frozen=True, slots=True)
class WithMeta(Node[LispForm]):
    form: LispForm
    meta: Union[Const, Map]
    expr: Union[Fn, Map, Set, Vector]
    env: NodeEnv
    children: Sequence[kw.Keyword] = vec.v(META, EXPR)
    op: NodeOp = NodeOp.WITH_META
    top_level: bool = False
    raw_forms: IPersistentVector[LispForm] = vec.Vector.empty()

ParentNode = Union[
    Await,
    Const,
    Def,
    Do,
    Fn,
    HostCall,
    HostField,
    If,
    Import,
    Invoke,
    Let,
    LetFn,
    Loop,
    Map,
    MaybeClass,
    MaybeHostForm,
    PyDict,
    PyList,
    PySet,
    PyTuple,
    Quote,
    Set,
    SetBang,
    Throw,
    Try,
    VarRef,
    Vector,
    WithMeta,
]
ChildOnlyNode = Union[Binding, Catch, FnMethod, ImportAlias, Local, Recur]
AnyNode = Union[ParentNode, ChildOnlyNode]
SpecialFormNode = Union[
    Await,
    Def,
    DefType,
    Do,
    Fn,
    If,
    HostCall,
    HostField,
    Import,
    Invoke,
    Let,
    Loop,
    Quote,
    Recur,
    SetBang,
    Throw,
    Try,
    VarRef,
]


import ast
import itertools
import os
import types
from typing import Any, Callable, Iterable, List, Mapping, Optional

from astor import code_gen as codegen

import basilisp.lang.runtime as runtime
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase  # noqa
from basilisp.lang.compiler.generator import (  # noqa
    GeneratedPyAST,
    GeneratorContext,
    USE_VAR_INDIRECTION,
    WARN_ON_VAR_INDIRECTION,
    expressionize as _expressionize,
    gen_py_ast,
    py_module_preamble,
    statementize as _statementize,
)
from basilisp.lang.compiler.optimizer import PythonASTOptimizer
from basilisp.lang.compiler.parser import (  # noqa
    ParserContext,
    WARN_ON_SHADOWED_NAME,
    WARN_ON_SHADOWED_VAR,
    WARN_ON_UNUSED_NAMES,
    parse_ast,
)
from basilisp.lang.typing import ReaderForm
from basilisp.lang.util import genname

_DEFAULT_FN = "__lisp_expr__"

def to_py_str(t: ast.AST) -> str:
    """Return a string of the Python code which would generate the input
    AST node."""
    return codegen.to_source(t)

BytecodeCollector = Optional[Callable[[types.CodeType], None]]

class CompilerContext:
    __slots__ = ("_filename", "_gctx", "_pctx", "_optimizer")

    def __init__(self, filename: str, opts: Optional[Mapping[str, bool]] = None):
        self._filename = filename
        self._gctx = GeneratorContext(filename=filename, opts=opts)
        self._pctx = ParserContext(filename=filename, opts=opts)
        self._optimizer = PythonASTOptimizer()

    @property
    def filename(self) -> str:
        return self._filename

    @property
    def generator_context(self) -> GeneratorContext:
        return self._gctx

    @property
    def parser_context(self) -> ParserContext:
        return self._pctx

    @property
    def py_ast_optimizer(self) -> PythonASTOptimizer:
        return self._optimizer

def _emit_ast_string(module: ast.AST) -> None:  # pragma: no cover
    """Emit the generated Python AST string either to standard out or to the
    *generated-python* dynamic Var for the current namespace. If the
    BASILISP_EMIT_GENERATED_PYTHON env var is not set True, this method is a
    no-op."""
    # TODO: eventually, this default should become "false" but during this
    #       period of heavy development, having it set to "true" by default
    #       is tremendously useful
    if os.getenv("BASILISP_EMIT_GENERATED_PYTHON", "true") != "true":
        return

    if runtime.print_generated_python():
        print(to_py_str(module))
    else:
        runtime.add_generated_python(to_py_str(module))

def compile_and_exec_form(  # pylint: disable= too-many-arguments
    form: ReaderForm,
    ctx: CompilerContext,
    module: types.ModuleType,
    wrapped_fn_name: str = _DEFAULT_FN,
    collect_bytecode: Optional[BytecodeCollector] = None,
) -> Any:
    """Compile and execute the given form. This function will be most useful
    for the REPL and testing purposes. Returns the result of the executed expression.

    Callers may override the wrapped function name, which is used by the
    REPL to evaluate the result of an expression and print it back out."""
    if form is None:
        return None

    if not module.__basilisp_bootstrapped__:  # type: ignore
        _bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, module)

    final_wrapped_name = genname(wrapped_fn_name)

    lisp_ast = parse_ast(ctx.parser_context, form)
    py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
    form_ast = list(
        map(
            _statementize,
            itertools.chain(
                py_ast.dependencies,
                [_expressionize(GeneratedPyAST(node=py_ast.node), final_wrapped_name)],
            ),
        )
    )

    ast_module = ast.Module(body=form_ast)
    ast_module = ctx.py_ast_optimizer.visit(ast_module)
    ast.fix_missing_locations(ast_module)

    _emit_ast_string(ast_module)

    bytecode = compile(ast_module, ctx.filename, "exec")
    if collect_bytecode:
        collect_bytecode(bytecode)
    exec(bytecode, module.__dict__)
    return getattr(module, final_wrapped_name)()

def _incremental_compile_module(
    optimizer: PythonASTOptimizer,
    py_ast: GeneratedPyAST,
    mod: types.ModuleType,
    source_filename: str,
    collect_bytecode: Optional[BytecodeCollector] = None,
) -> None:
    """Incrementally compile a stream of AST nodes in module mod.

    The source_filename will be passed to Python's native compile.

    Incremental compilation is an integral part of generating a Python module
    during the same process as macro-expansion."""
    module_body = list(
        map(_statementize, itertools.chain(py_ast.dependencies, [py_ast.node]))
    )

    module = ast.Module(body=list(module_body))
    module = optimizer.visit(module)
    ast.fix_missing_locations(module)

    _emit_ast_string(module)

    bytecode = compile(module, source_filename, "exec")
    if collect_bytecode:
        collect_bytecode(bytecode)
    exec(bytecode, mod.__dict__)

def _bootstrap_module(
    gctx: GeneratorContext,
    optimizer: PythonASTOptimizer,
    mod: types.ModuleType,
    collect_bytecode: Optional[BytecodeCollector] = None,
) -> None:
    """Bootstrap a new module with imports and other boilerplate."""
    _incremental_compile_module(
        optimizer,
        py_module_preamble(gctx),
        mod,
        source_filename=gctx.filename,
        collect_bytecode=collect_bytecode,
    )
    mod.__basilisp_bootstrapped__ = True  # type: ignore

def compile_module(
    forms: Iterable[ReaderForm],
    ctx: CompilerContext,
    module: types.ModuleType,
    collect_bytecode: Optional[BytecodeCollector] = None,
) -> None:
    """Compile an entire Basilisp module into Python bytecode which can be
    executed as a Python module.

    This function is designed to generate bytecode which can be used for the
    Basilisp import machinery, to allow callers to import Basilisp modules from
    Python code.
    """
    _bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, module)

    for form in forms:
        nodes = gen_py_ast(ctx.generator_context, parse_ast(ctx.parser_context, form))
        _incremental_compile_module(
            ctx.py_ast_optimizer,
            nodes,
            module,
            source_filename=ctx.filename,
            collect_bytecode=collect_bytecode,
        )

def compile_bytecode(
    code: List[types.CodeType],
    gctx: GeneratorContext,
    optimizer: PythonASTOptimizer,
    module: types.ModuleType,
) -> None:
    """Compile cached bytecode into the given module.

    The Basilisp import hook attempts to cache bytecode while compiling Basilisp
    namespaces. When the cached bytecode is reloaded from disk, it needs to be
    compiled within a bootstrapped module. This function bootstraps the module
    and then proceeds to compile a collection of bytecodes into the module."""
    _bootstrap_module(gctx, optimizer, module)
    for bytecode in code:
        exec(bytecode, module.__dict__)


from typing import Any, Callable, Iterable, Iterator, Optional, TypeVar

from basilisp.lang.interfaces import IMeta, ISeq
from basilisp.util import Maybe

T = TypeVar("T")

class _EmptySequence(ISeq[T]):
    def __repr__(self):
        return "()"

    def __bool__(self):
        return False

    @property
    def is_empty(self) -> bool:
        return True

    @property
    def first(self) -> Optional[T]:
        return None

    @property
    def rest(self) -> ISeq[T]:
        return self

    def cons(self, elem):
        return Cons(elem, self)

EMPTY: ISeq = _EmptySequence()

class Cons(ISeq[T], IMeta):
    __slots__ = ("_first", "_rest", "_meta")

    def __init__(self, first=None, seq: Optional[ISeq[T]] = None, meta=None) -> None:
        self._first = first
        self._rest = Maybe(seq).or_else_get(EMPTY)
        self._meta = meta

    @property
    def is_empty(self) -> bool:
        return False

    @property
    def first(self) -> Optional[T]:
        return self._first

    @property
    def rest(self) -> ISeq[T]:
        return self._rest

    def cons(self, elem: T) -> "Cons[T]":
        return Cons(elem, self)

    @property
    def meta(self):
        return self._meta

    def with_meta(self, meta) -> "Cons":
        new_meta = meta if self._meta is None else self._meta.update(meta)
        return Cons(first=self._first, seq=self._rest, meta=new_meta)

class _Sequence(ISeq[T]):
    """Sequences are a thin wrapper over Python Iterable values so they can
    satisfy the Basilisp `ISeq` interface.

    Sequences are singly linked lists which lazily traverse the input Iterable.

    Do not directly instantiate a Sequence. Instead use the `sequence` function
    below."""

    __slots__ = ("_first", "_seq", "_rest")

    def __init__(self, s: Iterator, first: T) -> None:
        self._seq = s  # pylint:disable=assigning-non-slot
        self._first = first  # pylint:disable=assigning-non-slot
        self._rest: Optional[ISeq] = None  # pylint:disable=assigning-non-slot

    @property
    def is_empty(self) -> bool:
        return False

    @property
    def first(self) -> Optional[T]:
        return self._first

    @property
    def rest(self) -> "ISeq[T]":
        if self._rest:
            return self._rest

        try:
            n = next(self._seq)
            self._rest = _Sequence(self._seq, n)  # pylint:disable=assigning-non-slot
        except StopIteration:
            self._rest = EMPTY  # pylint:disable=assigning-non-slot

        return self._rest

    def cons(self, elem):
        return Cons(elem, self)

class LazySeq(ISeq[T]):
    """LazySeqs are wrappers for delaying sequence computation. Create a LazySeq
    with a function that can either return None or a Seq. If a Seq is returned,
    the LazySeq is a proxy to that Seq."""

    __slots__ = ("_gen", "_realized", "_seq")

    def __init__(self, gen: Callable[[], Optional[ISeq[T]]]) -> None:
        self._gen = gen  # pylint:disable=assigning-non-slot
        self._realized = False  # pylint:disable=assigning-non-slot
        self._seq: Optional[ISeq[T]] = None  # pylint:disable=assigning-non-slot

    def _realize(self):
        if not self._realized:
            self._seq = self._gen()  # pylint:disable=assigning-non-slot
            self._realized = True  # pylint:disable=assigning-non-slot

    @property
    def is_empty(self) -> bool:
        if not self._realized:
            self._realize()
            return self.is_empty
        if self._seq is None or self._seq.is_empty:
            return True
        return False

    @property
    def first(self) -> Optional[T]:
        if not self._realized:
            self._realize()
        try:
            return self._seq.first  # type: ignore
        except AttributeError:
            return None

    @property
    def rest(self) -> "ISeq[T]":
        if not self._realized:
            self._realize()
        try:
            return self._seq.rest  # type: ignore
        except AttributeError:
            return EMPTY

    def cons(self, elem):
        return Cons(elem, self)

    @property
    def is_realized(self):
        return self._realized

    def __iter__(self):
        o = self
        if o:
            first = o.first
            if isinstance(o, LazySeq):
                if o.is_empty:
                    return
            yield first
            yield from o.rest

def sequence(s: Iterable) -> ISeq[Any]:
    """Create a Sequence from Iterable s."""
    try:
        i = iter(s)
        return _Sequence(i, next(i))
    except StopIteration:
        return EMPTY


import builtins
import datetime
import keyword
import re
import uuid
from decimal import Decimal
from fractions import Fraction
from typing import Iterable, Match, Pattern

import dateutil.parser as dateparser

import basilisp.lang.atom as atom

_MUNGE_REPLACEMENTS = {
    "+": "__PLUS__",
    "-": "_",
    "*": "__STAR__",
    "/": "__DIV__",
    ">": "__GT__",
    "<": "__LT__",
    "!": "__BANG__",
    "=": "__EQ__",
    "?": "__Q__",
    "\\": "__IDIV__",
    "&": "__AMP__",
    "$": "__DOLLAR__",
}

def count(seq: Iterable) -> int:
    return sum([1 for _ in seq])

def munge(s: str, allow_builtins: bool = False) -> str:
    """Replace characters which are not valid in Python symbols
    with valid replacement strings."""
    new_str = []
    for c in s:
        new_str.append(_MUNGE_REPLACEMENTS.get(c, c))

    new_s = "".join(new_str)

    if keyword.iskeyword(new_s):
        return f"{new_s}_"

    if not allow_builtins and new_s in builtins.__dict__:
        return f"{new_s}_"

    return new_s

_DEMUNGE_PATTERN = re.compile(r"(__[A-Z]+__)")
_DEMUNGE_REPLACEMENTS = {v: k for k, v in _MUNGE_REPLACEMENTS.items()}

def demunge(s: str) -> str:
    """Replace munged string components with their original
    representation."""

    def demunge_replacer(match: Match) -> str:
        full_match = match.group(0)
        replacement = _DEMUNGE_REPLACEMENTS.get(full_match, None)
        if replacement:
            return replacement
        return full_match

    return re.sub(_DEMUNGE_PATTERN, demunge_replacer, s).replace("_", "-")

# Use an atomically incremented integer as a suffix for all
# user-defined function and variable names compiled into Python
# code so no conflicts occur
_NAME_COUNTER = atom.Atom(1)

def next_name_id() -> int:
    """Increment the name counter and return the next value."""
    return _NAME_COUNTER.swap(lambda x: x + 1)

def genname(prefix: str) -> str:
    """Generate a unique function name with the given prefix."""
    i = next_name_id()
    return f"{prefix}_{i}"

def decimal_from_str(decimal_str: str) -> Decimal:
    """Create a Decimal from a numeric string."""
    return Decimal(decimal_str)

def fraction(numerator: int, denominator: int) -> Fraction:
    """Create a Fraction from a numerator and denominator."""
    return Fraction(numerator=numerator, denominator=denominator)

def inst_from_str(inst_str: str) -> datetime.datetime:
    """Create a datetime instance from an RFC 3339 formatted date string."""
    return dateparser.parse(inst_str)

def regex_from_str(regex_str: str) -> Pattern:
    """Create a new regex pattern from the input string."""
    return re.compile(regex_str)

def uuid_from_str(uuid_str: str) -> uuid.UUID:
    """Create a new UUID instance from the canonical string representation
    of a UUID."""
    return uuid.UUID(f"{{{uuid_str}}}")


import logging
import os

def get_level() -> str:
    """Get the default logging level for Basilisp."""
    return os.getenv("BASILISP_LOGGING_LEVEL", "WARNING")

def get_handler(level: str, fmt: str) -> logging.Handler:
    """Get the default logging handler for Basilisp."""
    handler: logging.Handler = logging.NullHandler()
    if os.getenv("BASILISP_USE_DEV_LOGGER") == "true":
        handler = logging.StreamHandler()

    handler.setFormatter(logging.Formatter(fmt))
    handler.setLevel(level)
    return handler

TRACE = 5

logging.addLevelName(TRACE, "TRACE")

DEFAULT_FORMAT = (
    "%(asctime)s %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] - %(message)s"
)
DEFAULT_LEVEL = get_level()
DEFAULT_HANDLER = get_handler(DEFAULT_LEVEL, DEFAULT_FORMAT)


from builtins import map as pymap
from typing import Callable, Dict, Iterable, Mapping, Sequence, TypeVar, Union

from pyrsistent import (  # noqa # pylint: disable=unused-import
    PMap,
    PVector,
    pmap,
    pvector,
)

from basilisp.lang.interfaces import ILispObject, IMapEntry, IMeta, IPersistentMap, ISeq
from basilisp.lang.obj import map_lrepr as _map_lrepr
from basilisp.lang.seq import sequence
from basilisp.lang.vector import Vector
from basilisp.util import partition

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

class MapEntry(IMapEntry[K, V], Vector[Union[K, V]]):  # type: ignore
    __slots__ = ()

    def __init__(self, wrapped: "PVector[Union[K, V]]") -> None:
        try:
            if not len(wrapped) == 2:
                raise ValueError("Vector arg to map conj must be a pair")
        except TypeError as e:
            raise TypeError(f"Cannot make map entry from {type(wrapped)}") from e

        super().__init__(wrapped)

    @property
    def key(self) -> K:
        return self[0]

    @property
    def value(self) -> V:
        return self[1]

    @staticmethod
    def of(k: K, v: V) -> "MapEntry[K, V]":
        return MapEntry(pvector([k, v]))

    @staticmethod
    def from_vec(v: Sequence[Union[K, V]]) -> "MapEntry[K, V]":
        return MapEntry(pvector(v))

class Map(ILispObject, IMeta, IPersistentMap[K, V]):
    """Basilisp Map. Delegates internally to a pyrsistent.PMap object.
    Do not instantiate directly. Instead use the m() and map() factory
    methods below."""

    __slots__ = ("_inner", "_meta")

    def __init__(self, wrapped: "PMap[K, V]", meta=None) -> None:
        self._inner = wrapped
        self._meta = meta

    def __call__(self, key, default=None):
        return self._inner.get(key, default)

    def __contains__(self, item):
        return item in self._inner

    def __eq__(self, other):
        return self._inner == other

    def __getattr__(self, item):
        return getattr(self._inner, item)

    def __getitem__(self, item):
        return self._inner[item]

    def __hash__(self):
        return hash(self._inner)

    def __iter__(self):
        for k, v in self._inner.iteritems():
            yield MapEntry.of(k, v)

    def __len__(self):
        return len(self._inner)

    def _lrepr(self, **kwargs):
        return _map_lrepr(
            self._inner.iteritems, start="{", end="}", meta=self._meta, **kwargs
        )

    def items(self):
        return self._inner.items()

    def keys(self):
        return self._inner.keys()

    def values(self):
        return self._inner.values()

    @property
    def meta(self):
        return self._meta

    def with_meta(self, meta: "IPersistentMap") -> "Map":
        new_meta = meta if self._meta is None else self._meta.update(meta)
        return Map(self._inner, meta=new_meta)

    def assoc(self, *kvs):
        m = self._inner.evolver()
        for k, v in partition(kvs, 2):
            m[k] = v
        return Map(m.persistent())

    def contains(self, k):
        if k in self._inner:
            return True
        return False

    def dissoc(self, *ks):
        m = self._inner.evolver()
        for k in ks:
            try:
                del m[k]
            except KeyError:
                pass
        return Map(m.persistent())

    def entry(self, k, default=None):
        return self._inner.get(k, default)

    def update(self, *maps: Mapping[K, V]) -> "Map":
        m: PMap = self._inner.update(*maps)
        return Map(m)

    def update_with(
        self, merge_fn: Callable[[V, V], V], *maps: Mapping[K, V]
    ) -> "Map[K, V]":
        m: PMap = self._inner.update_with(merge_fn, *maps)
        return Map(m)

    def cons(
        self,
        *elems: Union["Map[K, V]", Dict[K, V], IMapEntry[K, V], Vector[Union[K, V]]],
    ) -> "Map[K, V]":
        # For now, this definition does not take the generic Mapping[K, V] type
        # because Vectors are technically Mapping[int, V] types, so this would
        # require restructuring of the logic.
        e = self._inner.evolver()
        try:
            for elem in elems:
                if isinstance(elem, Map):
                    for entry in elem:
                        e.set(entry.key, entry.value)
                elif isinstance(elem, dict):
                    for k, v in elem.items():
                        e.set(k, v)
                elif isinstance(elem, IMapEntry):
                    e.set(elem.key, elem.value)
                else:
                    entry = MapEntry.from_vec(elem)
                    e.set(entry.key, entry.value)
            return Map(e.persistent(), meta=self.meta)
        except (TypeError, ValueError):
            raise ValueError(
                "Argument to map conj must be another Map or castable to MapEntry"
            )

    @staticmethod
    def empty() -> "Map":
        return m()

    def seq(self) -> ISeq[IMapEntry[K, V]]:
        return sequence(self)

def map(kvs: Mapping[K, V], meta=None) -> Map[K, V]:  # pylint:disable=redefined-builtin
    """Creates a new map."""
    return Map(pmap(initial=kvs), meta=meta)

def m(**kvs) -> Map[str, V]:
    """Creates a new map from keyword arguments."""
    return Map(pmap(initial=kvs))

def from_entries(entries: Iterable[MapEntry[K, V]]) -> Map[K, V]:
    m = pmap().evolver()  # type: ignore
    for entry in entries:
        m.set(entry.key, entry.value)
    return Map(m.persistent())

def hash_map(*pairs) -> Map:
    entries = pymap(lambda v: MapEntry.of(v[0], v[1]), partition(pairs, 2))
    return from_entries(entries)


import contextlib
import time
from typing import Optional, Callable, TypeVar, Generic

from functional import seq
from functional.pipeline import Sequence

@contextlib.contextmanager
def timed(f: Optional[Callable[[int], None]] = None):
    """Time the execution of code in the with-block, calling the function
    f (if it is given) with the resulting time in nanoseconds."""
    start = time.perf_counter()
    yield
    end = time.perf_counter()
    if f:
        ns = int((end - start) * 1_000_000_000)
        f(ns)

T = TypeVar("T")
U = TypeVar("U")

class Maybe(Generic[T]):
    __slots__ = ("_inner",)

    def __init__(self, inner: Optional[T]) -> None:
        self._inner = inner  # pylint:disable=assigning-non-slot

    def __eq__(self, other):
        if isinstance(other, Maybe):
            return self._inner == other.value
        return self._inner == other

    def __repr__(self):
        return repr(self._inner)

    def __str__(self):
        return str(self._inner)

    def or_else(self, else_fn: Callable[[], T]) -> T:
        if self._inner is None:
            return else_fn()
        return self._inner

    def or_else_get(self, else_v: T) -> T:
        if self._inner is None:
            return else_v
        return self._inner

    def or_else_raise(self, raise_fn: Callable[[], Exception]) -> T:
        if self._inner is None:
            raise raise_fn()
        return self._inner

    def map(self, f: Callable[[T], U]) -> "Maybe[U]":
        if self._inner is None:
            return Maybe(None)
        return Maybe(f(self._inner))

    @property
    def value(self) -> Optional[T]:
        return self._inner

    def stream(self) -> Sequence:
        if self._inner is None:
            return seq([])
        return seq([self._inner])

    @property
    def is_present(self) -> bool:
        return self._inner is not None

def partition(coll, n: int):
    """Partition coll into groups of size n."""
    assert n > 0
    start = 0
    stop = n
    while stop <= len(coll):
        yield tuple(e for e in coll[start:stop])
        start += n
        stop += n
    if start < len(coll) < stop:
        stop = len(coll)
        yield tuple(e for e in coll[start:stop])


import collections
import contextlib
import decimal
import functools
import io
import re
import uuid
from datetime import datetime
from fractions import Fraction
from typing import (
    Any,
    Callable,
    Collection,
    Deque,
    Iterable,
    List,
    MutableMapping,
    Optional,
    Pattern,
    Tuple,
    TypeVar,
    Union,
    cast,
)

from functional import seq

import basilisp.lang.keyword as keyword
import basilisp.lang.list as llist
import basilisp.lang.map as lmap
import basilisp.lang.set as lset
import basilisp.lang.symbol as symbol
import basilisp.lang.util as langutil
import basilisp.lang.vector as vector
import basilisp.walker as walk
from basilisp.lang.interfaces import IMeta
from basilisp.lang.typing import IterableLispForm, LispForm, ReaderForm
from basilisp.util import Maybe

ns_name_chars = re.compile(r"\w|-|\+|\*|\?|/|\=|\\|!|&|%|>|<|\$|\.")
alphanumeric_chars = re.compile(r"\w")
begin_num_chars = re.compile(r"[0-9\-]")
num_chars = re.compile("[0-9]")
whitespace_chars = re.compile(r"[\s,]")
newline_chars = re.compile("(\r\n|\r|\n)")
fn_macro_args = re.compile("(%)(&|[0-9])?")
unicode_char = re.compile(r"u(\w+)")

DataReaders = Optional[lmap.Map]
GenSymEnvironment = MutableMapping[str, symbol.Symbol]
Resolver = Callable[[symbol.Symbol], symbol.Symbol]
LispReaderFn = Callable[["ReaderContext"], LispForm]
W = TypeVar("W", bound=LispReaderFn)

READER_LINE_KW = keyword.keyword("line", ns="basilisp.lang.reader")
READER_COL_KW = keyword.keyword("col", ns="basilisp.lang.reader")

_AMPERSAND = symbol.symbol("&")
_FN = symbol.symbol("fn*")
_INTEROP_CALL = symbol.symbol(".")
_INTEROP_PROP = symbol.symbol(".-")
_QUOTE = symbol.symbol("quote")
_VAR = symbol.symbol("var")

_APPLY = symbol.symbol("apply", ns="basilisp.core")
_CONCAT = symbol.symbol("concat", ns="basilisp.core")
_DEREF = symbol.symbol("deref", ns="basilisp.core")
_HASH_MAP = symbol.symbol("hash-map", ns="basilisp.core")
_HASH_SET = symbol.symbol("hash-set", ns="basilisp.core")
_LIST = symbol.symbol("list", ns="basilisp.core")
_SEQ = symbol.symbol("seq", ns="basilisp.core")
_UNQUOTE = symbol.symbol("unquote", ns="basilisp.core")
_UNQUOTE_SPLICING = symbol.symbol("unquote-splicing", ns="basilisp.core")
_VECTOR = symbol.symbol("vector", ns="basilisp.core")

class Comment:
    pass

COMMENT = Comment()

LispReaderForm = Union[ReaderForm, Comment]

class SyntaxError(Exception):  # pylint:disable=redefined-builtin
    pass

class StreamReader:
    """A simple stream reader with n-character lookahead."""

    DEFAULT_INDEX = -2

    __slots__ = ("_stream", "_pushback_depth", "_idx", "_buffer", "_line", "_col")

    def __init__(self, stream: io.TextIOBase, pushback_depth: int = 5) -> None:
        self._stream = stream
        self._pushback_depth = pushback_depth
        self._idx = -2
        init_buffer = [self._stream.read(1), self._stream.read(1)]
        self._buffer = collections.deque(init_buffer, pushback_depth)
        self._line = collections.deque([1], pushback_depth)
        self._col = collections.deque([1], pushback_depth)

        for c in init_buffer[1:]:
            self._update_loc(c)

    @property
    def col(self):
        return self._col[self._idx]

    @property
    def line(self):
        return self._line[self._idx]

    @property
    def loc(self):
        return self.line, self.col

    def _update_loc(self, c):
        """Update the internal line and column buffers after a new character
        is added.

        The column number is set to 0, so the first character on the next line
        is column number 1."""
        if newline_chars.match(c):
            self._col.append(0)
            self._line.append(self._line[-1] + 1)
        else:
            self._col.append(self._col[-1] + 1)
            self._line.append(self._line[-1])

    def peek(self) -> str:
        """Peek at the next character in the stream."""
        return self._buffer[self._idx]

    def pushback(self) -> None:
        """Push one character back onto the stream, allowing it to be
        read again."""
        if abs(self._idx - 1) > self._pushback_depth:
            raise IndexError("Exceeded pushback depth")
        self._idx -= 1

    def advance(self) -> str:
        """Advance the current token pointer by one and return the
        previous token value from before advancing the counter."""
        cur = self.peek()
        self.next_token()
        return cur

    def next_token(self) -> str:
        """Advance the stream forward by one character and return the
        next token in the stream."""
        if self._idx < StreamReader.DEFAULT_INDEX:
            self._idx += 1
        else:
            c = self._stream.read(1)
            self._update_loc(c)
            self._buffer.append(c)
        return self.peek()

@functools.singledispatch
def _py_from_lisp(
    form: Union[llist.List, lmap.Map, lset.Set, vector.Vector]
) -> ReaderForm:
    raise SyntaxError(f"Unrecognized Python type: {type(form)}")

@_py_from_lisp.register(llist.List)
def _py_tuple_from_list(form: llist.List) -> tuple:
    return tuple(form)

@_py_from_lisp.register(lmap.Map)
def _py_dict_from_map(form: lmap.Map) -> dict:
    return dict(form)

@_py_from_lisp.register(lset.Set)
def _py_set_from_set(form: lset.Set) -> set:
    return set(form)

@_py_from_lisp.register(vector.Vector)
def _py_list_from_vec(form: vector.Vector) -> list:
    return list(form)

def _inst_from_str(inst_str: str) -> datetime:
    try:
        return langutil.inst_from_str(inst_str)
    except (ValueError, OverflowError):
        raise SyntaxError(f"Unrecognized date/time syntax: {inst_str}")

def _uuid_from_str(uuid_str: str) -> uuid.UUID:
    try:
        return langutil.uuid_from_str(uuid_str)
    except (ValueError, TypeError):
        raise SyntaxError(f"Unrecognized UUID format: {uuid_str}")

class ReaderContext:
    _DATA_READERS = lmap.map(
        {
            symbol.symbol("inst"): _inst_from_str,
            symbol.symbol("py"): _py_from_lisp,
            symbol.symbol("uuid"): _uuid_from_str,
        }
    )

    __slots__ = (
        "_data_readers",
        "_reader",
        "_resolve",
        "_in_anon_fn",
        "_syntax_quoted",
        "_gensym_env",
        "_eof",
    )

    def __init__(
        self,
        reader: StreamReader,
        resolver: Resolver = None,
        data_readers: DataReaders = None,
        eof: Any = None,
    ) -> None:
        data_readers = Maybe(data_readers).or_else_get(lmap.Map.empty())
        for entry in data_readers:
            if not isinstance(entry.key, symbol.Symbol):
                raise TypeError("Expected symbol for data reader tag")
            if not entry.key.ns:
                raise ValueError(f"Non-namespaced tags are reserved by the reader")

        self._data_readers = ReaderContext._DATA_READERS.update_with(
            lambda l, r: l,  # Do not allow callers to overwrite existing builtin readers
            data_readers,
        )
        self._reader = reader
        self._resolve = Maybe(resolver).or_else_get(lambda x: x)
        self._in_anon_fn: Deque[bool] = collections.deque([])
        self._syntax_quoted: Deque[bool] = collections.deque([])
        self._gensym_env: Deque[GenSymEnvironment] = collections.deque([])
        self._eof = eof

    @property
    def data_readers(self) -> lmap.Map:
        return self._data_readers

    @property
    def eof(self) -> Any:
        return self._eof

    @property
    def reader(self) -> StreamReader:
        return self._reader

    def resolve(self, sym: symbol.Symbol) -> symbol.Symbol:
        return self._resolve(sym)

    @contextlib.contextmanager
    def in_anon_fn(self):
        self._in_anon_fn.append(True)
        yield
        self._in_anon_fn.pop()

    @property
    def is_in_anon_fn(self) -> bool:
        try:
            return self._in_anon_fn[-1] is True
        except IndexError:
            return False

    @property
    def gensym_env(self) -> GenSymEnvironment:
        return self._gensym_env[-1]

    @contextlib.contextmanager
    def syntax_quoted(self):
        self._syntax_quoted.append(True)
        self._gensym_env.append({})
        yield
        self._gensym_env.pop()
        self._syntax_quoted.pop()

    @contextlib.contextmanager
    def unquoted(self):
        self._syntax_quoted.append(False)
        yield
        self._syntax_quoted.pop()

    @property
    def is_syntax_quoted(self) -> bool:
        try:
            return self._syntax_quoted[-1] is True
        except IndexError:
            return False

EOF = "EOF"

def _with_loc(f: W) -> W:
    """Wrap a reader function in a decorator to supply line and column
    information along with relevant forms."""

    @functools.wraps(f)
    def with_lineno_and_col(ctx):
        meta = lmap.map(
            {READER_LINE_KW: ctx.reader.line, READER_COL_KW: ctx.reader.col}
        )
        v = f(ctx)
        try:
            return v.with_meta(meta)  # type: ignore
        except AttributeError:
            return v

    return cast(W, with_lineno_and_col)

def _read_namespaced(
    ctx: ReaderContext, allowed_suffix: Optional[str] = None
) -> Tuple[Optional[str], str]:
    """Read a namespaced token from the input stream."""
    ns: List[str] = []
    name: List[str] = []
    reader = ctx.reader
    has_ns = False
    while True:
        token = reader.peek()
        if token == "/":
            reader.next_token()
            if has_ns:
                raise SyntaxError("Found '/'; expected word character")
            elif len(name) == 0:
                name.append("/")
            else:
                if "/" in name:
                    raise SyntaxError("Found '/' after '/'")
                has_ns = True
                ns = name
                name = []
        elif ns_name_chars.match(token):
            reader.next_token()
            name.append(token)
        elif allowed_suffix is not None and token == allowed_suffix:
            reader.next_token()
            name.append(token)
        else:
            break

    ns_str = None if not has_ns else "".join(ns)
    name_str = "".join(name)

    # A small exception for the symbol '/ used for division
    if ns_str is None:
        if "/" in name_str and name_str != "/":
            raise SyntaxError("'/' character disallowed in names")

    assert ns_str is None or len(ns_str) > 0

    return ns_str, name_str

def _read_coll(
    ctx: ReaderContext,
    f: Callable[[Collection[Any]], Union[llist.List, lset.Set, vector.Vector]],
    end_token: str,
    coll_name: str,
):
    """Read a collection from the input stream and create the
    collection using f."""
    coll: List = []
    reader = ctx.reader
    while True:
        token = reader.peek()
        if token == "":
            raise SyntaxError(f"Unexpected EOF in {coll_name}")
        if whitespace_chars.match(token):
            reader.advance()
            continue
        if token == end_token:
            reader.next_token()
            return f(coll)
        elem = _read_next(ctx)
        if elem is COMMENT:
            continue
        coll.append(elem)

def _consume_whitespace(reader: StreamReader) -> None:
    token = reader.peek()
    while whitespace_chars.match(token):
        token = reader.advance()

@_with_loc
def _read_list(ctx: ReaderContext) -> llist.List:
    """Read a list element from the input stream."""
    start = ctx.reader.advance()
    assert start == "("
    return _read_coll(ctx, llist.list, ")", "list")

@_with_loc
def _read_vector(ctx: ReaderContext) -> vector.Vector:
    """Read a vector element from the input stream."""
    start = ctx.reader.advance()
    assert start == "["
    return _read_coll(ctx, vector.vector, "]", "vector")

@_with_loc
def _read_set(ctx: ReaderContext) -> lset.Set:
    """Return a set from the input stream."""
    start = ctx.reader.advance()
    assert start == "{"

    def set_if_valid(s: Collection) -> lset.Set:
        if len(s) != len(set(s)):
            raise SyntaxError("Duplicated values in set")
        return lset.set(s)

    return _read_coll(ctx, set_if_valid, "}", "set")

@_with_loc
def _read_map(ctx: ReaderContext) -> lmap.Map:
    """Return a map from the input stream."""
    reader = ctx.reader
    start = reader.advance()
    assert start == "{"
    d: MutableMapping[Any, Any] = {}
    while True:
        if reader.peek() == "}":
            reader.next_token()
            break
        k = _read_next(ctx)
        if k is COMMENT:
            continue
        while True:
            if reader.peek() == "}":
                raise SyntaxError("Unexpected token '}'; expected map value")
            v = _read_next(ctx)
            if v is COMMENT:
                continue
            if k in d:
                raise SyntaxError(f"Duplicate key '{k}' in map literal")
            break
        d[k] = v

    return lmap.map(d)

# Due to some ambiguities that arise in parsing symbols, numbers, and the
# special keywords `true`, `false`, and `nil`, we have to have a looser
# type defined for the return from these reader functions.
MaybeSymbol = Union[bool, None, symbol.Symbol]
MaybeNumber = Union[complex, decimal.Decimal, float, Fraction, int, MaybeSymbol]

def _read_num(  # noqa: C901  # pylint: disable=too-many-statements
    ctx: ReaderContext
) -> MaybeNumber:
    """Return a numeric (complex, Decimal, float, int, Fraction) from the input stream."""
    chars: List[str] = []
    reader = ctx.reader

    is_complex = False
    is_decimal = False
    is_float = False
    is_integer = False
    is_ratio = False
    while True:
        token = reader.peek()
        if token == "-":
            following_token = reader.next_token()
            if not begin_num_chars.match(following_token):
                reader.pushback()
                try:
                    for _ in chars:
                        reader.pushback()
                except IndexError:
                    raise SyntaxError(
                        "Requested to pushback too many characters onto StreamReader"
                    )
                return _read_sym(ctx)
            chars.append(token)
            continue
        elif token == ".":
            if is_float:
                raise SyntaxError("Found extra '.' in float; expected decimal portion")
            is_float = True
        elif token == "J":
            if is_complex:
                raise SyntaxError("Found extra 'J' suffix in complex literal")
            is_complex = True
        elif token == "M":
            if is_decimal:
                raise SyntaxError("Found extra 'M' suffix in decimal literal")
            is_decimal = True
        elif token == "N":
            if is_integer:
                raise SyntaxError("Found extra 'N' suffix in integer literal")
            is_integer = True
        elif token == "/":
            if is_ratio:
                raise SyntaxError("Found extra '/' in ratio literal")
            is_ratio = True
        elif not num_chars.match(token):
            break
        reader.next_token()
        chars.append(token)

    assert len(chars) > 0, "Must have at least one digit in integer or float"

    s = "".join(chars)
    if (
        sum(
            [
                is_complex and is_decimal,
                is_complex and is_integer,
                is_complex and is_ratio,
                is_decimal or is_float,
                is_integer,
                is_ratio,
            ]
        )
        > 1
    ):
        raise SyntaxError(f"Invalid number format: {s}")

    if is_complex:
        imaginary = float(s[:-1]) if is_float else int(s[:-1])
        return complex(0, imaginary)
    elif is_decimal:
        try:
            return decimal.Decimal(s[:-1])
        except decimal.InvalidOperation:
            raise SyntaxError(f"Invalid number format: {s}") from None
    elif is_float:
        return float(s)
    elif is_ratio:
        assert "/" in s, "Ratio must contain one '/' character"
        num, denominator = s.split("/")
        return Fraction(numerator=int(num), denominator=int(denominator))
    elif is_integer:
        return int(s[:-1])
    return int(s)

_STR_ESCAPE_CHARS = {
    '"': '"',
    "\\": "\\",
    "a": "\a",
    "b": "\b",
    "f": "\f",
    "n": "\n",
    "r": "\r",
    "t": "\t",
    "v": "\v",
}

def _read_str(ctx: ReaderContext, allow_arbitrary_escapes: bool = False) -> str:
    """Return a string from the input stream.

    If allow_arbitrary_escapes is True, do not throw a SyntaxError if an
    unknown escape sequence is encountered."""
    s: List[str] = []
    reader = ctx.reader
    while True:
        token = reader.next_token()
        if token == "":
            raise SyntaxError("Unexpected EOF in string")
        if token == "\\":
            token = reader.next_token()
            escape_char = _STR_ESCAPE_CHARS.get(token, None)
            if escape_char:
                s.append(escape_char)
                continue
            if allow_arbitrary_escapes:
                s.append("\\")
            else:
                raise SyntaxError("Unknown escape sequence: \\{token}")
        if token == '"':
            reader.next_token()
            return "".join(s)
        s.append(token)

@_with_loc
def _read_sym(ctx: ReaderContext) -> MaybeSymbol:
    """Return a symbol from the input stream.

    If a symbol appears in a syntax quoted form, the reader will attempt
    to resolve the symbol using the resolver in the ReaderContext `ctx`.
    The resolver will look into the current namespace for an alias or
    namespace matching the symbol's namespace."""
    ns, name = _read_namespaced(ctx, allowed_suffix="#")
    if not ctx.is_syntax_quoted and name.endswith("#"):
        raise SyntaxError("Gensym may not appear outside syntax quote")
    if ns is not None:
        if any(map(lambda s: len(s) == 0, ns.split("."))):
            raise SyntaxError(
                "All '.' separated segments of a namespace "
                "must contain at least one character."
            )
    if name.startswith(".") and ns is not None:
        raise SyntaxError("Symbols starting with '.' may not have a namespace")
    if ns is None:
        if name == "nil":
            return None
        elif name == "true":
            return True
        elif name == "false":
            return False
    if ctx.is_syntax_quoted and not name.endswith("#"):
        return ctx.resolve(symbol.symbol(name, ns))
    return symbol.symbol(name, ns=ns)

def _read_kw(ctx: ReaderContext) -> keyword.Keyword:
    """Return a keyword from the input stream."""
    start = ctx.reader.advance()
    assert start == ":"
    ns, name = _read_namespaced(ctx)
    if "." in name:
        raise SyntaxError("Found '.' in keyword name")
    return keyword.keyword(name, ns=ns)

def _read_meta(ctx: ReaderContext) -> IMeta:
    """Read metadata and apply that to the next object in the
    input stream."""
    start = ctx.reader.advance()
    assert start == "^"
    meta = _read_next_consuming_comment(ctx)

    meta_map: Optional[lmap.Map[LispForm, LispForm]] = None
    if isinstance(meta, symbol.Symbol):
        meta_map = lmap.map({keyword.keyword("tag"): meta})
    elif isinstance(meta, keyword.Keyword):
        meta_map = lmap.map({meta: True})
    elif isinstance(meta, lmap.Map):
        meta_map = meta
    else:
        raise SyntaxError(
            f"Expected symbol, keyword, or map for metadata, not {type(meta)}"
        )

    obj_with_meta = _read_next_consuming_comment(ctx)
    try:
        return obj_with_meta.with_meta(meta_map)  # type: ignore
    except AttributeError:
        raise SyntaxError(
            f"Can not attach metadata to object of type {type(obj_with_meta)}"
        )

@_with_loc
def _read_function(ctx: ReaderContext) -> llist.List:
    """Read a function reader macro from the input stream."""
    if ctx.is_in_anon_fn:
        raise SyntaxError(f"Nested #() definitions not allowed")

    with ctx.in_anon_fn():
        form = _read_list(ctx)
    arg_set = set()

    def arg_suffix(arg_num):
        if arg_num is None:
            return "1"
        elif arg_num == "&":
            return "rest"
        else:
            return arg_num

    def sym_replacement(arg_num):
        suffix = arg_suffix(arg_num)
        return symbol.symbol(f"arg-{suffix}")

    def identify_and_replace(f):
        if isinstance(f, symbol.Symbol):
            if f.ns is None:
                match = fn_macro_args.match(f.name)
                if match is not None:
                    arg_num = match.group(2)
                    suffix = arg_suffix(arg_num)
                    arg_set.add(suffix)
                    return sym_replacement(arg_num)
        return f

    body = walk.postwalk(identify_and_replace, form) if len(form) > 0 else None

    arg_list: List[symbol.Symbol] = []
    numbered_args = sorted(map(int, filter(lambda k: k != "rest", arg_set)))
    if len(numbered_args) > 0:
        max_arg = max(numbered_args)
        arg_list = [sym_replacement(str(i)) for i in range(1, max_arg + 1)]
        if "rest" in arg_set:
            arg_list.append(_AMPERSAND)
            arg_list.append(sym_replacement("rest"))

    return llist.l(_FN, vector.vector(arg_list), body)

@_with_loc
def _read_quoted(ctx: ReaderContext) -> llist.List:
    """Read a quoted form from the input stream."""
    start = ctx.reader.advance()
    assert start == "'"
    next_form = _read_next_consuming_comment(ctx)
    return llist.l(_QUOTE, next_form)

def _is_unquote(form: ReaderForm) -> bool:
    """Return True if this form is unquote."""
    try:
        return form.first == _UNQUOTE  # type: ignore
    except AttributeError:
        return False

def _is_unquote_splicing(form: ReaderForm) -> bool:
    """Return True if this form is unquote-splicing."""
    try:
        return form.first == _UNQUOTE_SPLICING  # type: ignore
    except AttributeError:
        return False

def _expand_syntax_quote(
    ctx: ReaderContext, form: IterableLispForm
) -> Iterable[LispForm]:
    """Expand syntax quoted forms to handle unquoting and unquote-splicing.

    The unquoted form (unquote x) becomes:
        (list x)

    The unquote-spliced form (unquote-splicing x) becomes
        x

    All other forms are recursively processed as by _process_syntax_quoted_form
    and are returned as:
        (list form)"""
    expanded = []

    for elem in form:
        if _is_unquote(elem):
            expanded.append(llist.l(_LIST, elem[1]))
        elif _is_unquote_splicing(elem):
            expanded.append(elem[1])
        else:
            expanded.append(llist.l(_LIST, _process_syntax_quoted_form(ctx, elem)))

    return expanded

def _process_syntax_quoted_form(ctx: ReaderContext, form: ReaderForm) -> ReaderForm:
    """Post-process syntax quoted forms to generate forms that can be assembled
    into the correct types at runtime.

    Lists are turned into:
        (basilisp.core/seq
         (basilisp.core/concat [& rest]))

    Vectors are turned into:
        (basilisp.core/apply
         basilisp.core/vector
         (basilisp.core/concat [& rest]))

    Sets are turned into:
        (basilisp.core/apply
         basilisp.core/hash-set
         (basilisp.core/concat [& rest]))

    Maps are turned into:
        (basilisp.core/apply
         basilisp.core/hash-map
         (basilisp.core/concat [& rest]))

    The child forms (called rest above) are processed by _expand_syntax_quote.

    All other forms are passed through without modification."""
    lconcat = lambda v: llist.list(v).cons(_CONCAT)
    if _is_unquote(form):
        return form[1]  # type: ignore
    elif _is_unquote_splicing(form):
        raise SyntaxError("Cannot splice outside collection")
    elif isinstance(form, llist.List):
        return llist.l(_SEQ, lconcat(_expand_syntax_quote(ctx, form)))
    elif isinstance(form, vector.Vector):
        return llist.l(_APPLY, _VECTOR, lconcat(_expand_syntax_quote(ctx, form)))
    elif isinstance(form, lset.Set):
        return llist.l(_APPLY, _HASH_SET, lconcat(_expand_syntax_quote(ctx, form)))
    elif isinstance(form, lmap.Map):
        flat_kvs = seq(form.items()).flatten().to_list()
        return llist.l(_APPLY, _HASH_MAP, lconcat(_expand_syntax_quote(ctx, flat_kvs)))
    elif isinstance(form, symbol.Symbol):
        if form.ns is None and form.name.endswith("#"):
            try:
                return llist.l(_QUOTE, ctx.gensym_env[form.name])
            except KeyError:
                genned = symbol.symbol(langutil.genname(form.name[:-1])).with_meta(
                    form.meta
                )
                ctx.gensym_env[form.name] = genned
                return llist.l(_QUOTE, genned)
        return llist.l(_QUOTE, form)
    else:
        return form

def _read_syntax_quoted(ctx: ReaderContext) -> ReaderForm:
    """Read a syntax-quote and set the syntax-quoting state in the reader."""
    start = ctx.reader.advance()
    assert start == "`"

    with ctx.syntax_quoted():
        return _process_syntax_quoted_form(ctx, _read_next_consuming_comment(ctx))

def _read_unquote(ctx: ReaderContext) -> LispForm:
    """Read an unquoted form and handle any special logic of unquoting.

    Unquoted forms can take two, well... forms:

      `~form` is read as `(unquote form)` and any nested forms are read
      literally and passed along to the compiler untouched.

      `~@form` is read as `(unquote-splicing form)` which tells the compiler
      to splice in the contents of a sequential form such as a list or
      vector into the final compiled form. This helps macro writers create
      longer forms such as function calls, function bodies, or data structures
      with the contents of another collection they have."""
    start = ctx.reader.advance()
    assert start == "~"

    with ctx.unquoted():
        next_char = ctx.reader.peek()
        if next_char == "@":
            ctx.reader.advance()
            next_form = _read_next_consuming_comment(ctx)
            return llist.l(_UNQUOTE_SPLICING, next_form)
        else:
            next_form = _read_next_consuming_comment(ctx)
            return llist.l(_UNQUOTE, next_form)

@_with_loc
def _read_deref(ctx: ReaderContext) -> LispForm:
    """Read a derefed form from the input stream."""
    start = ctx.reader.advance()
    assert start == "@"
    next_form = _read_next_consuming_comment(ctx)
    return llist.l(_DEREF, next_form)

_SPECIAL_CHARS = {
    "newline": "\n",
    "space": " ",
    "tab": "\t",
    "formfeed": "\f",
    "backspace": "\b",
    "return": "\r",
}

def _read_character(ctx: ReaderContext) -> str:
    """Read a character literal from the input stream.

    Character literals may appear as:
      - \\a \\b \\c etc will yield 'a', 'b', and 'c' respectively

      - \\newline, \\space, \\tab, \\formfeed, \\backspace, \\return yield
        the named characters

      - \\uXXXX yield the unicode digit corresponding to the code
        point named by the hex digits XXXX"""
    start = ctx.reader.advance()
    assert start == "\\"

    s: List[str] = []
    reader = ctx.reader
    token = reader.peek()
    while True:
        if token == "" or whitespace_chars.match(token):
            break
        if not alphanumeric_chars.match(token):
            break
        s.append(token)
        token = reader.next_token()

    char = "".join(s)
    special = _SPECIAL_CHARS.get(char, None)
    if special is not None:
        return special

    match = unicode_char.match(char)
    if match is not None:
        try:
            return chr(int(f"0x{match.group(1)}", 16))
        except (ValueError, OverflowError):
            raise SyntaxError(f"Unsupported character \\u{char}") from None

    if len(char) > 1:
        raise SyntaxError(f"Unsupported character \\{char}")

    return char

def _read_regex(ctx: ReaderContext) -> Pattern:
    """Read a regex reader macro from the input stream."""
    s = _read_str(ctx, allow_arbitrary_escapes=True)
    try:
        return langutil.regex_from_str(s)
    except re.error:
        raise SyntaxError(f"Unrecognized regex pattern syntax: {s}")

def _read_reader_macro(ctx: ReaderContext) -> LispReaderForm:
    """Return a data structure evaluated as a reader
    macro from the input stream."""
    start = ctx.reader.advance()
    assert start == "#"
    token = ctx.reader.peek()
    if token == "{":
        return _read_set(ctx)
    elif token == "(":
        return _read_function(ctx)
    elif token == "'":
        ctx.reader.advance()
        s = _read_sym(ctx)
        return llist.l(_VAR, s)
    elif token == '"':
        return _read_regex(ctx)
    elif token == "_":
        ctx.reader.advance()
        _read_next(ctx)  # Ignore the entire next form
        return COMMENT
    elif ns_name_chars.match(token):
        s = _read_sym(ctx)
        assert isinstance(s, symbol.Symbol)
        v = _read_next_consuming_comment(ctx)
        if s in ctx.data_readers:
            f = ctx.data_readers[s]
            return f(v)
        else:
            raise SyntaxError(f"No data reader found for tag #{s}")

    raise SyntaxError(f"Unexpected token '{token}' in reader macro")

def _read_comment(ctx: ReaderContext) -> LispReaderForm:
    """Read (and ignore) a single-line comment from the input stream.
    Return the next form after the next line break."""
    reader = ctx.reader
    start = reader.advance()
    assert start == ";"
    while True:
        token = reader.peek()
        if newline_chars.match(token):
            reader.advance()
            return _read_next(ctx)
        if token == "":
            return ctx.eof
        reader.advance()

def _read_next_consuming_comment(ctx: ReaderContext) -> ReaderForm:
    """Read the next full form from the input stream, consuming any
    reader comments completely."""
    while True:
        v = _read_next(ctx)
        if v is ctx.eof:
            return ctx.eof
        if v is COMMENT or isinstance(v, Comment):
            continue
        return v

def _read_next(ctx: ReaderContext) -> LispReaderForm:  # noqa: C901
    """Read the next full form from the input stream."""
    reader = ctx.reader
    token = reader.peek()
    if token == "(":
        return _read_list(ctx)
    elif token == "[":
        return _read_vector(ctx)
    elif token == "{":
        return _read_map(ctx)
    elif begin_num_chars.match(token):
        return _read_num(ctx)
    elif whitespace_chars.match(token):
        reader.next_token()
        return _read_next(ctx)
    elif token == ":":
        return _read_kw(ctx)
    elif token == '"':
        return _read_str(ctx)
    elif token == "'":
        return _read_quoted(ctx)
    elif token == "\\":
        return _read_character(ctx)
    elif ns_name_chars.match(token):
        return _read_sym(ctx)
    elif token == "#":
        return _read_reader_macro(ctx)
    elif token == "^":
        return _read_meta(ctx)  # type: ignore
    elif token == ";":
        return _read_comment(ctx)
    elif token == "`":
        return _read_syntax_quoted(ctx)
    elif token == "~":
        return _read_unquote(ctx)
    elif token == "@":
        return _read_deref(ctx)
    elif token == "":
        return ctx.eof
    else:
        raise SyntaxError("Unexpected token '{token}'".format(token=token))

def read(
    stream,
    resolver: Resolver = None,
    data_readers: DataReaders = None,
    eof: Any = EOF,
    is_eof_error: bool = False,
) -> Iterable[ReaderForm]:
    """Read the contents of a stream as a Lisp expression.

    Callers may optionally specify a namespace resolver, which will be used
    to adjudicate the fully-qualified name of symbols appearing inside of
    a syntax quote.

    Callers may optionally specify a map of custom data readers that will
    be used to resolve values in reader macros. Data reader tags specified
    by callers must be namespaced symbols; non-namespaced symbols are
    reserved by the reader. Data reader functions must be functions taking
    one argument and returning a value.

    The caller is responsible for closing the input stream."""
    reader = StreamReader(stream)
    ctx = ReaderContext(reader, resolver=resolver, data_readers=data_readers, eof=eof)
    while True:
        expr = _read_next(ctx)
        if expr is ctx.eof:
            if is_eof_error:
                raise EOFError
            return
        if expr is COMMENT or isinstance(expr, Comment):
            continue
        yield expr

def read_str(
    s: str,
    resolver: Resolver = None,
    data_readers: DataReaders = None,
    eof: Any = None,
    is_eof_error: bool = False,
) -> Iterable[ReaderForm]:
    """Read the contents of a string as a Lisp expression.

    Keyword arguments to this function have the same meanings as those of
    basilisp.lang.reader.read."""
    with io.StringIO(s) as buf:
        yield from read(
            buf,
            resolver=resolver,
            data_readers=data_readers,
            eof=eof,
            is_eof_error=is_eof_error,
        )

def read_file(
    filename: str,
    resolver: Resolver = None,
    data_readers: DataReaders = None,
    eof: Any = None,
    is_eof_error: bool = False,
) -> Iterable[ReaderForm]:
    """Read the contents of a file as a Lisp expression.

    Keyword arguments to this function have the same meanings as those of
    basilisp.lang.reader.read."""
    with open(filename) as f:
        yield from read(
            f,
            resolver=resolver,
            data_readers=data_readers,
            eof=eof,
            is_eof_error=is_eof_error,
        )


import importlib.machinery
import importlib.util
import logging
import marshal
import os
import os.path
import sys
import types
from importlib.abc import MetaPathFinder, SourceLoader
from typing import List, Mapping, MutableMapping, Optional

import basilisp.lang.compiler as compiler
import basilisp.lang.reader as reader
import basilisp.lang.runtime as runtime
from basilisp.lang.util import demunge
from basilisp.util import timed

_NO_CACHE_ENVVAR = "BASILISP_DO_NOT_CACHE_NAMESPACES"

MAGIC_NUMBER = (1149).to_bytes(2, "little") + b"\r\n"

logger = logging.getLogger(__name__)

def _r_long(int_bytes: bytes) -> int:
    """Convert 4 bytes in little-endian to an integer."""
    return int.from_bytes(int_bytes, "little")

def _w_long(x: int) -> bytes:
    """Convert a 32-bit integer to little-endian."""
    return (int(x) & 0xFFFFFFFF).to_bytes(4, "little")

def _basilisp_bytecode(
    mtime: int, source_size: int, code: List[types.CodeType]
) -> bytes:
    """Return the bytes for a Basilisp bytecode cache file."""
    data = bytearray(MAGIC_NUMBER)
    data.extend(_w_long(mtime))
    data.extend(_w_long(source_size))
    data.extend(marshal.dumps(code))  # type: ignore
    return data

def _get_basilisp_bytecode(
    fullname: str, mtime: int, source_size: int, cache_data: bytes
) -> List[types.CodeType]:
    """Unmarshal the bytes from a Basilisp bytecode cache file, validating the
    file header prior to returning. If the file header does not match, throw
    an exception."""
    exc_details = {"name": fullname}
    magic = cache_data[:4]
    raw_timestamp = cache_data[4:8]
    raw_size = cache_data[8:12]
    if magic != MAGIC_NUMBER:
        message = (
            f"Incorrect magic number ({magic}) in {fullname}; expected {MAGIC_NUMBER}"
        )
        logger.debug(message)
        raise ImportError(message, **exc_details)  # type: ignore
    elif len(raw_timestamp) != 4:
        message = f"Reached EOF while reading timestamp in {fullname}"
        logger.debug(message)
        raise EOFError(message)
    elif _r_long(raw_timestamp) != mtime:
        message = f"Non-matching timestamp ({_r_long(raw_timestamp)}) in {fullname} bytecode cache; expected {mtime}"
        logger.debug(message)
        raise ImportError(message, **exc_details)  # type: ignore
    elif len(raw_size) != 4:
        message = f"Reached EOF while reading size of source in {fullname}"
        logger.debug(message)
        raise EOFError(message)
    elif _r_long(raw_size) != source_size:
        message = f"Non-matching filesize ({_r_long(raw_size)}) in {fullname} bytecode cache; expected {source_size}"
        logger.debug(message)
        raise ImportError(message, **exc_details)  # type: ignore

    return marshal.loads(cache_data[12:])  # type: ignore

def _cache_from_source(path: str) -> str:
    """Return the path to the cached file for the given path. The original path
    does not have to exist."""
    cache_path, cache_file = os.path.split(importlib.util.cache_from_source(path))
    filename, _ = os.path.splitext(cache_file)
    return os.path.join(cache_path, filename + ".lpyc")

class BasilispImporter(MetaPathFinder, SourceLoader):
    """Python import hook to allow directly loading Basilisp code within
    Python."""

    def __init__(self):
        self._cache: MutableMapping[str, dict] = {}

    def find_spec(
        self,
        fullname: str,
        path,  # Optional[List[str]] # MyPy complains this is incompatible with supertype
        target: types.ModuleType = None,
    ) -> Optional[importlib.machinery.ModuleSpec]:
        """Find the ModuleSpec for the specified Basilisp module.

        Returns None if the module is not a Basilisp module to allow import processing to continue."""
        package_components = fullname.split(".")
        if path is None:
            path = sys.path
            module_name = package_components
        else:
            module_name = [package_components[-1]]

        for entry in path:
            filenames = [
                f"{os.path.join(entry, *module_name, '__init__')}.lpy",
                f"{os.path.join(entry, *module_name)}.lpy",
            ]
            for filename in filenames:
                if os.path.exists(filename):
                    state = {
                        "fullname": fullname,
                        "filename": filename,
                        "path": entry,
                        "target": target,
                        "cache_filename": _cache_from_source(filename),
                    }
                    logger.debug(
                        f"Found potential Basilisp module '{fullname}' in file '{filename}'"
                    )
                    return importlib.machinery.ModuleSpec(
                        fullname, self, origin=filename, loader_state=state
                    )
        return None

    def invalidate_caches(self):
        super().invalidate_caches()
        self._cache = {}

    def _cache_bytecode(
        self, source_path, cache_path, data
    ):  # pylint: disable=unused-argument
        self.set_data(cache_path, data)

    def path_stats(self, path):
        stat = os.stat(path)
        return {"mtime": int(stat.st_mtime), "size": stat.st_size}

    def get_data(self, path):
        with open(path, mode="r+b") as f:
            return f.read()

    def set_data(self, path, data):
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, mode="w+b") as f:
            f.write(data)

    def get_filename(self, fullname: str) -> str:
        try:
            cached = self._cache[fullname]
        except KeyError:
            raise ImportError(f"Could not import module '{fullname}'")
        spec = cached["spec"]
        return spec.loader_state.filename

    def create_module(self, spec: importlib.machinery.ModuleSpec):
        logger.debug(f"Creating Basilisp module '{spec.name}''")
        mod = types.ModuleType(spec.name)
        mod.__file__ = spec.loader_state["filename"]
        mod.__loader__ = spec.loader
        mod.__package__ = spec.parent
        mod.__spec__ = spec
        self._cache[spec.name] = {"spec": spec}
        return mod

    def _exec_cached_module(
        self,
        fullname: str,
        loader_state: Mapping[str, str],
        path_stats: Mapping[str, int],
        module: types.ModuleType,
    ):
        """Load and execute a cached Basilisp module."""
        filename = loader_state["filename"]
        cache_filename = loader_state["cache_filename"]

        with timed(
            lambda duration: logger.debug(
                f"Loaded cached Basilisp module '{fullname}' in {duration / 1000000}ms"
            )
        ):
            logger.debug(f"Checking for cached Basilisp module '{fullname}''")
            cache_data = self.get_data(cache_filename)
            cached_code = _get_basilisp_bytecode(
                fullname, path_stats["mtime"], path_stats["size"], cache_data
            )
            compiler.compile_bytecode(
                cached_code,
                compiler.GeneratorContext(filename=filename),
                compiler.PythonASTOptimizer(),
                module,
            )

    def _exec_module(
        self,
        fullname: str,
        loader_state: Mapping[str, str],
        path_stats: Mapping[str, int],
        module: types.ModuleType,
    ):
        """Load and execute a non-cached Basilisp module."""
        filename = loader_state["filename"]
        cache_filename = loader_state["cache_filename"]

        with timed(
            lambda duration: logger.debug(
                f"Loaded Basilisp module '{fullname}' in {duration / 1000000}ms"
            )
        ):
            # During compilation, bytecode objects are added to the list via the closure
            # add_bytecode below, which is passed to the compiler. The collected bytecodes
            # will be used to generate an .lpyc file for caching the compiled file.
            all_bytecode = []

            def add_bytecode(bytecode: types.CodeType):
                all_bytecode.append(bytecode)

            logger.debug(f"Reading and compiling Basilisp module '{fullname}'")
            forms = reader.read_file(filename, resolver=runtime.resolve_alias)
            compiler.compile_module(  # pylint: disable=unexpected-keyword-arg
                forms,
                compiler.CompilerContext(filename=filename),
                module,
                collect_bytecode=add_bytecode,
            )

        # Cache the bytecode that was collected through the compilation run.
        cache_file_bytes = _basilisp_bytecode(
            path_stats["mtime"], path_stats["size"], all_bytecode
        )
        self._cache_bytecode(filename, cache_filename, cache_file_bytes)

    def exec_module(self, module):
        """Compile the Basilisp module into Python code.

        Basilisp is fundamentally a form-at-a-time compilation, meaning that
        each form in a module may require code compiled from an earlier form, so
        we incrementally compile a Python module by evaluating a single top-level
        form at a time and inserting the resulting AST nodes into the Pyton module."""
        fullname = module.__name__
        cached = self._cache[fullname]
        cached["module"] = module
        spec = cached["spec"]
        filename = spec.loader_state["filename"]
        path_stats = self.path_stats(filename)

        # During the bootstrapping process, the 'basilisp.core namespace is created with
        # a blank module. If we do not replace the module here with the module we are
        # generating, then we will not be able to use advanced compilation features such
        # as direct Python variable access to functions and other def'ed values.
        ns_name = demunge(fullname)
        ns: runtime.Namespace = runtime.set_current_ns(ns_name).value
        ns.module = module

        # Check if a valid, cached version of this Basilisp namespace exists and, if so,
        # load it and bypass the expensive compilation process below.
        if os.getenv(_NO_CACHE_ENVVAR, None) == "true":
            self._exec_module(fullname, spec.loader_state, path_stats, module)
        else:
            try:
                self._exec_cached_module(
                    fullname, spec.loader_state, path_stats, module
                )
            except (EOFError, ImportError, IOError, OSError) as e:
                logger.debug(f"Failed to load cached Basilisp module: {e}")
                self._exec_module(fullname, spec.loader_state, path_stats, module)

        # Because we want to (by default) add 'basilisp.core into every namespace by default,
        # we want to make sure we don't try to add 'basilisp.core into itself, causing a
        # circular import error.
        #
        # Later on, we can probably remove this and just use the 'ns macro to auto-refer
        # all 'basilisp.core values into the current namespace.
        runtime.Namespace.add_default_import(ns_name)

def hook_imports():
    """Hook into Python's import machinery with a custom Basilisp code
    importer.

    Once this is called, Basilisp code may be called from within Python code
    using standard `import module.submodule` syntax."""
    if any([isinstance(o, BasilispImporter) for o in sys.meta_path]):
        return
    sys.meta_path.insert(
        0, BasilispImporter()  # pylint:disable=abstract-class-instantiated
    )


from typing import Optional

from basilisp.lang.interfaces import ILispObject, IMeta
from basilisp.lang.obj import lrepr
from basilisp.lang.util import munge

class Symbol(ILispObject, IMeta):
    __slots__ = ("_name", "_ns", "_meta")

    def __init__(self, name: str, ns: Optional[str] = None, meta=None) -> None:
        self._name = name
        self._ns = ns
        self._meta = meta

    def _lrepr(self, **kwargs) -> str:
        print_meta = kwargs["print_meta"]

        if self._ns is not None:
            sym_repr = "{ns}/{name}".format(ns=self._ns, name=self._name)
        else:
            sym_repr = self._name

        if print_meta and self._meta:
            return f"^{lrepr(self._meta, **kwargs)} {sym_repr}"
        return sym_repr

    @property
    def name(self) -> str:
        return self._name

    @property
    def ns(self) -> Optional[str]:
        return self._ns

    @property
    def meta(self):
        return self._meta

    def with_meta(self, meta) -> "Symbol":
        new_meta = meta if self._meta is None else self._meta.update(meta)
        return Symbol(self._name, self._ns, meta=new_meta)

    def as_python_sym(self) -> str:
        if self.ns is not None:
            return f"{munge(self.ns)}.{munge(self.name)}"
        return f"{munge(self.name)}"

    def __eq__(self, other):
        if not isinstance(other, Symbol):
            return False
        return self._ns == other._ns and self._name == other._name

    def __hash__(self):
        return hash((self._ns, self._name))

def symbol(name: str, ns: Optional[str] = None, meta=None) -> Symbol:
    """Create a new symbol."""
    return Symbol(name, ns=ns, meta=meta)


from typing import Iterable, Optional

from pyrsistent import PMap, pmap

import basilisp.lang.atom as atom
from basilisp.lang.interfaces import IAssociative, ILispObject

__INTERN: atom.Atom["PMap[int, Keyword]"] = atom.Atom(pmap())

class Keyword(ILispObject):
    __slots__ = ("_name", "_ns")

    def __init__(self, name: str, ns: Optional[str] = None) -> None:
        self._name = name
        self._ns = ns

    @property
    def name(self) -> str:
        return self._name

    @property
    def ns(self) -> Optional[str]:
        return self._ns

    def _lrepr(self, **kwargs) -> str:
        if self._ns is not None:
            return ":{ns}/{name}".format(ns=self._ns, name=self._name)
        return ":{name}".format(name=self._name)

    def __eq__(self, other):
        return self is other

    def __hash__(self):
        return hash((self._name, self._ns))

    def __call__(self, m: IAssociative, default=None):
        try:
            return m.entry(self, default)
        except AttributeError:
            return None

def complete(
    text: str, kw_cache: atom.Atom["PMap[int, Keyword]"] = __INTERN
) -> Iterable[str]:
    """Return an iterable of possible completions for the given text."""
    assert text.startswith(":")
    interns = kw_cache.deref()
    text = text[1:]

    if "/" in text:
        prefix, suffix = text.split("/", maxsplit=1)
        results = filter(
            lambda kw: (kw.ns is not None and kw.ns == prefix)
            and kw.name.startswith(suffix),
            interns.itervalues(),
        )
    else:
        results = filter(
            lambda kw: kw.name.startswith(text)
            or (kw.ns is not None and kw.ns.startswith(text)),
            interns.itervalues(),
        )

    return map(str, results)

def __get_or_create(
    kw_cache: "PMap[int, Keyword]", h: int, name: str, ns: Optional[str]
) -> PMap:
    """Private swap function used to either get the interned keyword
    instance from the input string."""
    if h in kw_cache:
        return kw_cache
    kw = Keyword(name, ns=ns)
    return kw_cache.set(h, kw)

def keyword(
    name: str,
    ns: Optional[str] = None,
    kw_cache: atom.Atom["PMap[int, Keyword]"] = __INTERN,
) -> Keyword:
    """Create a new keyword."""
    h = hash((name, ns))
    return kw_cache.swap(__get_or_create, h, name, ns)[h]