# MIT License
#
# Copyright (c) 2023-2025 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import absolute_import

import otf2
import pandas as pd

from otf2.events import Enter, Leave


class Region(object):
    """ """

    def __init__(self, _enter, _leave, _depth, _location=None):
        if _enter.region != _leave.region:
            _location_info = f"\n{_location}" if _location else ""
            raise ValueError(
                f"enter region != leave region :: '{_enter}' != '{_leave}'{_location_info}"
            )

        if _depth < 0:
            _location_info = f". location: '{_location}'" if _location else ""
            raise ValueError(
                f"negative depth ({_depth})! enter: '{_enter}'. leave: '{_leave}'{_location_info}"
            )

        self.region = _enter.region
        self.depth = _depth
        self.name = _enter.region.name
        self.attributes = [
            itr for itr in [_enter.attributes, _leave.attributes] if itr is not None
        ]
        self.enter_nsec = _enter.time
        self.leave_nsec = _leave.time
        self.delta_nsec = _leave.time - _enter.time
        if self.delta_nsec < 0:
            raise ValueError(
                f"negative timestamp delta :: '{_enter.time}' > '{_leave.time}'"
            )

        for itr in self.attributes:
            for key, val in itr.items():
                _key = f"{key.name}"
                if not hasattr(self, _key):
                    setattr(self, _key, val)

        if not hasattr(self, "category"):
            self.category = "unk"

    def __str__(self):
        return f"{self.name:<35} :: {self.delta_nsec} nsec"


class OTF2Reader:
    """Read in perfetto protobuf output"""

    def __init__(self, filename):
        self.filename = filename if isinstance(filename, (list, tuple)) else [filename]

    def read(self):
        def _read_trace(trace_name):

            def get_node_domain(tree, node_to_domain):
                """Return the domain value for a system tree node, or None if not found."""
                return node_to_domain.get(tree, {}).get("domain", None)

            trace = otf2.reader.Reader(trace_name)
            # print(f"Read {len(trace.definitions.strings)} string definitions")
            # for string in trace.definitions.strings:
            #     print(f"String definition with value '{string}' in trace.")
            # print("Read {} events".format(len(trace.events)))

            events = [[loc, evt] for loc, evt in trace.events]
            locations = [itr for itr in trace.definitions.locations]
            location_groups = [itr for itr in trace.definitions.location_groups]
            system_tree_nodes = [itr for itr in trace.definitions.system_tree_nodes]
            system_tree_node_domains = [
                itr for itr in trace.definitions.system_tree_node_domains
            ]

            call_stack = {}
            partial_call_stack = {}
            node_to_domain = {}

            for domain_obj in system_tree_node_domains:
                node = getattr(domain_obj, "system_tree_node", None)
                # Collect all relevant attributes you want (example: domain, name, etc.)
                node_to_domain[node] = {
                    "domain": getattr(domain_obj, "system_tree_node_domain", None),
                }

            for itr in system_tree_nodes:
                call_stack[itr] = {}
                partial_call_stack[itr] = {}

            for itr in location_groups:
                call_stack[itr.system_tree_parent][itr] = {}
                partial_call_stack[itr.system_tree_parent][itr] = {}

            for itr in locations:
                call_stack[itr.group.system_tree_parent][itr.group][itr] = []
                partial_call_stack[itr.group.system_tree_parent][itr.group][itr] = []

            for location, event in events:
                _stree = location.group.system_tree_parent
                _group = location.group
                _partial = partial_call_stack[_stree][_group][location]
                if isinstance(event, Enter):
                    # expected length
                    _elen = len(_partial) + 1
                    _partial.append(event)
                elif isinstance(event, Leave):
                    # expected length
                    _elen = len(_partial) - 1
                    _depth = len(_partial)
                    _leave = event

                    # it appears that on MI300, the end of A may exceed the
                    # begin of B kernels very slightly (i.e. overlap in same
                    # stream/queue). This leads to slightly out of order
                    # Enter/Leave regions and thus we need to occasionally
                    # search further back in the callstack to find the correct
                    # Enter region
                    _enter = _partial[-1]
                    if _enter.region == _leave.region:
                        _partial.pop()
                    else:
                        for ridx, ritr in enumerate(reversed(_partial)):
                            if ritr.region == _leave.region:
                                _enter = _partial.pop(len(_partial) - ridx - 1)
                                break

                    # below is what is expected on non-MI300
                    # _enter = _partial.pop()

                    # add the region
                    call_stack[_stree][_group][location].append(
                        Region(_enter, _leave, _depth - 1, location)
                    )

                # modified length
                _mlen = len(partial_call_stack[_stree][_group][location])
                # if modified length != expected length
                if _mlen != _elen:
                    raise RuntimeError(
                        f"Modified length ({_mlen}) != Expected length({_elen}) for {event} at {location}"
                    )

            data = {
                "system_tree_node": [],
                "system_tree_node_domain": [],
                "location_group": [],
                "location": [],
                "region": [],
                "attributes": [],
                "depth": [],
                "name": [],
                "category": [],
                "start_ts": [],
                "end_ts": [],
            }

            for tree, lgitr in call_stack.items():
                for group, gitr in lgitr.items():
                    for loc, ritr in gitr.items():
                        for region in ritr:
                            data["system_tree_node"] += [tree]
                            data["system_tree_node_domain"] += [
                                get_node_domain(tree, node_to_domain)
                            ]
                            data["location_group"] += [group]
                            data["location"] += [loc]
                            data["region"] += [region.region]
                            data["attributes"] += [region.attributes]
                            data["depth"] += [region.depth]
                            data["category"] += [region.category]
                            data["name"] += [region.name]
                            data["start_ts"] += [region.enter_nsec]
                            data["end_ts"] += [region.leave_nsec]

            return (trace, pd.DataFrame.from_dict(data))

        readers = []
        df = pd.DataFrame()
        for itr in self.filename:
            _reader, _df = _read_trace(itr)
            readers += [_reader]
            df = pd.concat([df, _df])

        return (df, readers)


def read_trace(filename):
    data = OTF2Reader(filename).read()[0]

    print(f"\nDATA:\n{data}")

    attributes = list(data["attributes"])

    print(f"\nATTRIBUTES:\n{attributes}")
