# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""TVM Script Parser Scope Handler Classes"""
# pylint: disable=redefined-builtin, unused-argument, invalid-name, relative-beyond-top-level
from typing import Tuple, Any, Callable, Optional, List, Union, Mapping

import synr
import tvm.tir
from tvm.runtime import Object
from tvm.ir import Span, Range
from tvm.tir import Stmt, PrimExpr, IterVar, Var, Buffer, BufferRegion, ForKind

from .node import BufferSlice
from .utils import buffer_slice_to_region

from ..context_maintainer import ContextMaintainer
from ..registry import register
from ..utils import (
    get_param_list,
    tvm_span_from_synr,
    call_with_error_reporting,
)


class ScopeHandler:
    """Base class for all scope handlers"""

    def __init__(self, func: Callable):
        self.func: Callable = func
        self.body: Optional[Stmt] = None
        self.node: Optional[synr.ast.Node] = None
        self.context: Optional[ContextMaintainer] = None

    def signature(self) -> Tuple[str, Tuple[list, list, Any]]:
        return "tir." + self.func.__name__, get_param_list(self.func)

    def enter_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        pass

    def exit_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        self.node = node
        self.context = context
        return call_with_error_reporting(
            context.report_error, span, self.func, *arg_list, span=tvm_span_from_synr(span)
        )


class WithScopeHandler(ScopeHandler):
    """Base class for all with scope handlers"""

    def __init__(self, func, concise_scope, def_symbol):
        super().__init__(func)
        self.concise_scope = concise_scope
        self.def_symbol = def_symbol

    @staticmethod
    def get_optional_vars(node, context):
        """Get a list synr.ast.With's optional_vars"""
        assert isinstance(
            node, synr.ast.With
        ), f"WithScopeHandler expected synr.ast.With but got {type(node)}"

        if isinstance(node.lhs, list):
            for var in node.lhs:
                if not isinstance(var, synr.ast.Var):
                    context.report_error(
                        f"Invalid optional var definition, expected Var but got {type(var)}",
                        node.span,
                    )
            vars = node.lhs
        else:
            context.report_error(
                f"Invalid optional var definition, expected list of Var but got {type(node.lhs)}",
                node.span,
            )
        return vars


@register
class Allocate(WithScopeHandler):
    """With scope handler T.allocate(extents, dtype, scope, condition, annotations)"""

    def __init__(self):
        def allocate(extents, dtype, scope, condition=True, annotations=None, span=None):
            condition = tvm.runtime.convert(condition)
            scope = tvm.runtime.convert(scope)

            return tvm.tir.Allocate(
                self.buffer_var,
                dtype,
                extents,
                condition,
                self.body,
                annotations=annotations,
                span=span,
            )

        super().__init__(allocate, concise_scope=True, def_symbol=True)
        self.buffer_var = None

    def enter_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        # define buffer vars in symbol table
        if isinstance(node, synr.ast.With):
            vars = WithScopeHandler.get_optional_vars(node, context)
            if len(vars) != 1:
                context.report_error(f"Unexpected number of vars: 1 vs. {len(vars)}", node.span)
            name = vars[0].id.name
            var_span = vars[0].id.span
        elif isinstance(node, synr.ast.Assign):
            if len(node.lhs) != 1:
                context.report_error(f"Unexpected number of vars: 1 vs. {len(node.lhs)}", node.span)
            name = node.lhs[0].id.name
            var_span = node.lhs[0].id.span
        else:
            raise Exception("Internal Bug")

        def setup_buffer_var(
            extents, dtype, scope, condition=True, annotations=None, span: Span = None
        ):
            """Setup buffer var for a given type."""
            buffer_ptr_type = tvm.ir.PointerType(tvm.ir.PrimType(dtype), scope)
            self.buffer_var = tvm.tir.Var(name, buffer_ptr_type, span)

        setup_buffer_var(*arg_list, span=tvm_span_from_synr(var_span))
        context.update_symbol(name, self.buffer_var, node)


@register
class LaunchThread(WithScopeHandler):
    """With scope handler T.launch_thread(env_var, extent)"""

    def __init__(self):
        def launch_thread(env_var, extent, span):
            extent = tvm.runtime.convert(extent, span=span)
            thread_id = self.context.func_var_env_dict[env_var]
            attr_key = "virtual_thread" if thread_id == "vthread" else "thread_extent"
            return tvm.tir.AttrStmt(
                IterVar(
                    (0, extent),
                    env_var,
                    getattr(IterVar, "ThreadIndex"),
                    thread_id,
                    span=span,
                ),
                attr_key,
                extent,
                self.body,
                span=span,
            )

        super().__init__(launch_thread, concise_scope=True, def_symbol=False)


@register
class Realize(WithScopeHandler):
    """With scope handler T.realize(buffer_bounds, scope, condition)"""

    def __init__(self):
        def realize(
            buffer_slice: BufferSlice, scope: str, condition: bool = True, span: bool = None
        ):
            assert self.context, "call 'exit_scope' before 'enter_scope'"
            buffer: Buffer = buffer_slice.buffer
            bounds: List[Range] = []
            for s in buffer_slice.slices:
                min: Union[PrimExpr, int] = s.start
                extent: Union[PrimExpr, int] = 1 if s.stop is None else s.stop - s.start
                if isinstance(extent, PrimExpr):
                    extent = self.context.analyzer.simplify(extent)
                bounds.append(Range.from_min_extent(min, extent, span=s.span))

            scope = tvm.runtime.convert(scope, span=span)
            return tvm.tir.AttrStmt(
                buffer,
                "realize_scope",
                scope,
                tvm.tir.BufferRealize(buffer, bounds, condition, self.body, span=span),
                span=span,
            )

        super().__init__(realize, concise_scope=True, def_symbol=False)


@register
class Attr(WithScopeHandler):
    """With scope handler T.attr(attr_node, attr_key, value)"""

    def __init__(self):
        def attr(attr_node, attr_key, value, span):
            attr_node = tvm.runtime.convert(attr_node, span=span)
            value = tvm.runtime.convert(value, span=span)
            return tvm.tir.AttrStmt(attr_node, attr_key, value, self.body, span=span)

        super().__init__(attr, concise_scope=True, def_symbol=False)


@register
class AssertHandler(WithScopeHandler):
    """With scope handler T.Assert(condition, message)"""

    def __init__(self):
        def Assert(condition, message, span):
            return tvm.tir.AssertStmt(condition, tvm.runtime.convert(message), self.body, span=span)

        super().__init__(Assert, concise_scope=True, def_symbol=False)


@register
class Let(WithScopeHandler):
    """With scope handler T.let(var, value)"""

    def __init__(self):
        def let(var, value, span):
            return tvm.tir.LetStmt(var, value, self.body, span=span)

        super().__init__(let, concise_scope=False, def_symbol=False)


@register
class Block(WithScopeHandler):
    """With scope handler T.block(name)"""

    def __init__(self):
        def block(name_hint: str = "", span: Optional[Span] = None):
            assert (
                self.node and self.context and self.body
            ), "call 'exit_scope' before 'enter_scope'"
            block_info = self.context.block_info_stack[-1]

            # create block read/write regions
            reads: List[BufferRegion] = (
                [buffer_slice_to_region(read) for read in block_info.reads]
                if block_info.reads
                else []
            )
            writes: List[BufferRegion] = (
                [buffer_slice_to_region(write) for write in block_info.writes]
                if block_info.writes
                else []
            )

            region_detect_mask: int = (block_info.reads is None) | (
                (block_info.writes is None) << 1
            )
            annotations = {} if block_info.annotations is None else block_info.annotations
            if region_detect_mask != 0:
                annotations["tir.script_parsing_detect_access"] = region_detect_mask
            inner = tvm.tir.Block(
                block_info.iter_vars,
                reads,
                writes,
                name_hint,
                self.body,
                block_info.init,
                block_info.alloc_buffers,
                block_info.match_buffers,
                annotations,
                span,
            )
            assert len(block_info.iter_vars) == len(block_info.iter_values)
            predicate = (
                tvm.tir.const(True, "bool")
                if block_info.predicate is None
                else block_info.predicate
            )
            body = tvm.tir.BlockRealize(block_info.iter_values, predicate, inner, span)
            return body

        super().__init__(func=block, concise_scope=False, def_symbol=True)
        self.block_vars = None

    def enter_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        # define block vars
        assert isinstance(
            node, synr.ast.With
        ), f"BlockScopeHandler expected to work on synr.ast.With but got {type(node)}"

        optional_vars = [var.id.name for var in WithScopeHandler.get_optional_vars(node, context)]
        if optional_vars:
            context.report_error(
                f"Block expected no optional_vars (e.g., `x` in `with block() as x`), "
                f"but got {optional_vars}",
                node.span,
            )


@register
class InitBlock(WithScopeHandler):
    """With scope handler T.init()"""

    def __init__(self):
        def init(span: Span = None):
            assert self.context, "call 'exit_scope' before 'enter_scope'"
            if self.context.block_info_stack[-2].init is not None:
                self.context.report_error("Duplicate init block declaration", span)
            self.context.block_info_stack[-2].init = self.body

        super().__init__(func=init, concise_scope=False, def_symbol=True)


class LoopInfo:
    """Helper class for loop information"""

    loop_var: Var
    begin: PrimExpr
    extent: PrimExpr
    kind: ForKind
    thread_binding: Optional[str]
    annotations: Optional[Mapping[str, Object]]

    def __init__(
        self,
        begin: PrimExpr,
        extent: PrimExpr,
        kind: ForKind,
        thread_binding: Optional[str] = None,
        annotations: Optional[Mapping[str, Object]] = None,
    ) -> None:
        self.begin = begin
        self.extent = extent
        self.kind = kind
        self.thread_binding = thread_binding
        self.annotations = annotations


class ForScopeHandler(ScopeHandler):
    """Base class for all for scope handlers"""

    def __init__(self, func):
        super().__init__(func)
        self.loop_vars: List[Var] = []
        self.loop_info: List[LoopInfo] = []

    def enter_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        assert isinstance(
            node, synr.ast.For
        ), f"ForScopeHandler expected synr.ast.For but got {type(node)}"

        loop_var_names = list()
        spans = list()
        if isinstance(node.lhs, synr.ast.Var):
            loop_var_names.append(node.lhs.id.name)
            spans.append(tvm_span_from_synr(node.lhs.id.span))
        elif isinstance(node.lhs, list):
            for elt in node.lhs:
                if not isinstance(elt, synr.ast.Var):
                    context.report_error(
                        f"Invalid loop var. Expected a var, but got {type(elt)}", elt.span
                    )
                loop_var_names.append(elt.id.name)
                spans.append(tvm_span_from_synr(elt.id.span))
        else:
            context.report_error(
                f"Invalid loop var. Expected var or list of vars as lhs, but got {type(node.lhs)}",
                span,
            )

        self.node = node
        self.context = context
        # generate loop vars
        self.loop_vars = [
            tvm.te.var(name, dtype="int32", span=span) for name, span in zip(loop_var_names, spans)
        ]
        # collect loop infos by calling self.func
        call_with_error_reporting(context.report_error, span, self.func, *arg_list)
        if len(self.loop_vars) != len(self.loop_info):
            self.context.report_error(
                f"Inconsistent number of vars and loops, got {len(self.loop_vars)} "
                + f"vs {len(self.loop_info)}",
                self.node.span,
            )
        for loop_var, loop_info in zip(self.loop_vars, self.loop_info):
            context.update_symbol(loop_var.name, loop_var, node)
            context.loop_stack[loop_var] = Range.from_min_extent(loop_info.begin, loop_info.extent)

    def exit_scope(
        self,
        node: synr.ast.Node,
        context: ContextMaintainer,
        arg_list: List[Any],
        span: synr.ast.Span,
    ):
        assert self.loop_vars, "call 'exit_scope' before 'enter_scope'"
        for loop_var in self.loop_vars:
            context.loop_stack.pop(loop_var)
        # Use assert here since we have check it in `enter_scope`
        assert len(self.loop_vars) == len(self.loop_info)

        body = self.body
        for var, info in zip(reversed(self.loop_vars), reversed(self.loop_info)):
            body = tvm.tir.For(
                var,
                info.begin,
                info.extent,
                info.kind,
                body,
                info.thread_binding,
                info.annotations,
                span=tvm_span_from_synr(span),
            )

        return body

    def create_loop_info(
        self,
        begin: PrimExpr,
        end: PrimExpr,
        kind: ForKind,
        thread_binding: Optional[str] = None,
        annotations: Optional[Mapping[str, Object]] = None,
    ) -> None:
        """
        Helper function for creating For in TVM Script parser.

        Parameters
        ----------
        begin : PrimExpr
            The beginning value.

        end : PrimExpr
            The endding value.

        kind : ForKind
            The type of the for.

        thread_binding: Optional[str]
            The thread this loop binds to.

        annotations : Optional[Mapping[str, Object]]
            Additional annotation hints.

        span : Optional[Span]
            The location of this for in the source code.

        Returns
        -------
        for : For
            The constructed For.
        """
        assert self.context and self.node, "call 'exit_scope' before 'enter_scope'"
        extent = end if begin == 0 else self.context.analyzer.simplify(end - begin)
        self.annotations = annotations
        self.loop_info.append(LoopInfo(begin, extent, kind, thread_binding, self.annotations))


@register
class Serial(ForScopeHandler):
    """For scope handler T.serial(begin, end, annotations)"""

    def __init__(self):
        def serial(
            begin: PrimExpr,
            end: PrimExpr = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if end is None:
                end = begin
                begin = 0
            self.create_loop_info(begin, end, ForKind.SERIAL, annotations=annotations)

        super().__init__(serial)


@register
class Parallel(ForScopeHandler):
    """For scope handler T.parallel(begin, end, annotations)"""

    def __init__(self):
        def parallel(
            begin: PrimExpr,
            end: PrimExpr = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if end is None:
                end = begin
                begin = 0
            self.create_loop_info(begin, end, ForKind.PARALLEL, annotations=annotations)

        super().__init__(parallel)


@register
class Vectorized(ForScopeHandler):
    """For scope handler T.vectorized(begin, end, annotations)"""

    def __init__(self):
        def vectorized(
            begin: PrimExpr,
            end: PrimExpr = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if end is None:
                end = begin
                begin = 0
            self.create_loop_info(begin, end, ForKind.VECTORIZED, annotations=annotations)

        super().__init__(vectorized)


@register
class Unroll(ForScopeHandler):
    """For scope handler T.unroll(begin, end, annotations)"""

    def __init__(self):
        def unroll(
            begin: PrimExpr,
            end: PrimExpr = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if end is None:
                end = begin
                begin = 0
            self.create_loop_info(begin, end, ForKind.UNROLLED, annotations=annotations)

        super().__init__(unroll)


@register
class ThreadBinding(ForScopeHandler):
    """For scope handler T.thread_binding(begin, end, thread, annotations)"""

    def __init__(self):
        def thread_binding(
            begin: PrimExpr,
            end: PrimExpr = None,
            thread: str = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if thread is None:
                if isinstance(end, str):  # handle case like thread_binding(128, "threadIdx.x")
                    thread = end
                    end = None
                else:
                    raise ValueError("Thread cannot be None for thread_binding")
            if end is None:
                end = begin
                begin = 0
            thread_iter_var = IterVar(None, None, IterVar.ThreadIndex, thread)
            self.create_loop_info(
                begin,
                end,
                ForKind.THREAD_BINDING,
                thread_binding=thread_iter_var,
                annotations=annotations,
            )

        super().__init__(thread_binding)


@register
class RangeHandler(ForScopeHandler):
    """For scope handler range(begin, end, annotations)
    Note that tir.range is totally the same as T.serial
    """

    def __init__(self):
        def for_range(
            begin: PrimExpr,
            end: PrimExpr = None,
            annotations: Optional[Mapping[str, Object]] = None,
        ):
            if end is None:
                end = begin
                begin = 0
            self.create_loop_info(begin, end, ForKind.SERIAL, annotations=annotations)

        super().__init__(for_range)

    def signature(self):
        return "range", get_param_list(self.func)


@register
class Grid(ForScopeHandler):
    """For scope handler T.grid(extents)"""

    def __init__(self):
        def grid(*extents: List[PrimExpr]):
            for extent in extents:
                self.create_loop_info(0, extent, ForKind.SERIAL)

        super().__init__(grid)
