############################################################################
# tools/pynuttx/nxgdb/fs.py
#
# SPDX-License-Identifier: Apache-2.0
#
# 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.
#
############################################################################

import argparse
import enum
from typing import Generator, Tuple

import gdb

from . import utils
from .protocols.fs import File, Inode
from .protocols.thread import Tcb

FSNODEFLAG_TYPE_MASK = utils.get_symbol_value("FSNODEFLAG_TYPE_MASK")

CONFIG_PSEUDOFS_FILE = utils.get_symbol_value("CONFIG_PSEUDOFS_FILE")
CONFIG_PSEUDOFS_ATTRIBUTES = utils.get_symbol_value("CONFIG_PSEUDOFS_ATTRIBUTES")

CONFIG_FS_BACKTRACE = utils.get_symbol_value("CONFIG_FS_BACKTRACE")
CONFIG_NFILE_DESCRIPTORS_PER_BLOCK = int(
    utils.get_symbol_value("CONFIG_NFILE_DESCRIPTORS_PER_BLOCK")
)
CONFIG_FS_SHMFS = utils.get_symbol_value("CONFIG_FS_SHMFS")

g_special_inodes = {}  # Map of the special inodes including epoll, inotify, etc.


class InodeType(enum.Enum):
    # define   FSNODEFLAG_TYPE_PSEUDODIR  0x00000000 /*   Pseudo dir (default)   */
    # define   FSNODEFLAG_TYPE_DRIVER     0x00000001 /*   Character driver       */
    # define   FSNODEFLAG_TYPE_BLOCK      0x00000002 /*   Block driver           */
    # define   FSNODEFLAG_TYPE_MOUNTPT    0x00000003 /*   Mount point            */
    # define   FSNODEFLAG_TYPE_NAMEDSEM   0x00000004 /*   Named semaphore        */
    # define   FSNODEFLAG_TYPE_MQUEUE     0x00000005 /*   Message Queue          */
    # define   FSNODEFLAG_TYPE_SHM        0x00000006 /*   Shared memory region   */
    # define   FSNODEFLAG_TYPE_MTD        0x00000007 /*   Named MTD driver       */
    # define   FSNODEFLAG_TYPE_SOFTLINK   0x00000008 /*   Soft link              */
    # define   FSNODEFLAG_TYPE_SOCKET     0x00000009 /*   Socket                 */
    # define   FSNODEFLAG_TYPE_PIPE       0x0000000a /*   Pipe                   */
    # define   FSNODEFLAG_TYPE_NAMEDEVENT 0x0000000b /*   Named event group      */
    PSEUDODIR = 0
    DRIVER = 1
    BLOCK = 2
    MOUNTPT = 3
    NAMEDSEM = 4
    MQUEUE = 5
    SHM = 6
    MTD = 7
    SOFTLINK = 8
    SOCKET = 9
    PIPE = 10
    NAMEDEVENT = 11
    UNKNOWN = 12


def get_inode_name(inode: Inode):
    if not inode:
        return ""

    def special_inode_name(inode):
        global g_special_inodes
        if not g_special_inodes:
            g_special_inodes = {}
            for special in (
                "perf",
                "timerfd",
                "signalfd",
                "dir",
                "inotify",
                "epoll",
                "eventfd",
                "sock",
            ):
                value = utils.gdb_eval_or_none(f"g_{special}_inode")
                if value:
                    g_special_inodes[special] = value.address

        for name, value in g_special_inodes.items():
            if value == inode:
                return name

        return None

    if name := special_inode_name(inode):
        return name

    ptr = inode.i_name.cast(gdb.lookup_type("char").pointer())
    return ptr.string()


def inode_getpath(inode: Inode):
    """get path fron inode"""
    if not inode:
        return ""

    name = get_inode_name(inode)

    if inode.i_parent:
        return inode_getpath(inode.i_parent) + "/" + name

    return name


def inode_gettype(inode: Inode) -> InodeType:
    if not inode:
        return InodeType.UNKNOWN

    type = int(inode.i_flags & FSNODEFLAG_TYPE_MASK)

    # check if it's a valid type in InodeType
    if type in [e.value for e in InodeType]:
        return InodeType(type)

    return InodeType.UNKNOWN


def get_file(tcb: Tcb, fd):
    group = tcb.group
    filelist = group.tg_filelist
    fl_files = filelist.fl_files
    fl_rows = filelist.fl_rows

    row = fd // CONFIG_NFILE_DESCRIPTORS_PER_BLOCK
    col = fd % CONFIG_NFILE_DESCRIPTORS_PER_BLOCK

    if row >= fl_rows:
        return None

    return fl_files[row][col]


def foreach_inode(root=None, path="") -> Generator[Tuple[Inode, str], None, None]:
    node: Inode = root or utils.parse_and_eval("g_root_inode").i_child
    while node:
        newpath = path + "/" + get_inode_name(node)
        yield node, newpath
        if node.i_child:
            yield from foreach_inode(node.i_child, newpath)
        node = node.i_peer


def foreach_file(tcb: Tcb):
    """Iterate over all file descriptors in a tcb"""
    group = tcb.group
    filelist = group.tg_filelist
    fl_files = filelist.fl_files
    fl_rows = filelist.fl_rows

    for row in range(fl_rows):
        for col in range(CONFIG_NFILE_DESCRIPTORS_PER_BLOCK):
            file = fl_files[row][col]

            if not file or not file.f_inode:
                continue

            fd = row * CONFIG_NFILE_DESCRIPTORS_PER_BLOCK + col

            yield fd, file


class Fdinfo(gdb.Command):
    """Dump fd info information of process"""

    def __init__(self):
        super().__init__("fdinfo", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION)
        self.total_fd_count = 0

    def print_file_info(self, fd, file: File, formatter: str):
        backtrace_formatter = "{0:<5} {1:<36} {2}"

        oflags = int(file.f_oflags)
        pos = int(file.f_pos)
        path = inode_getpath(file.f_inode)

        output = []
        if CONFIG_FS_BACKTRACE:
            backtrace = utils.Backtrace(
                utils.ArrayIterator(file.f_backtrace), formatter=backtrace_formatter
            )

            output.append(
                formatter.format(
                    fd, hex(file.address), oflags, pos, path, backtrace.formatted[0]
                )
            )
            output.extend(
                formatter.format("", "", "", "", "", bt)
                for bt in backtrace.formatted[1:]
            )
            output.append("")  # separate each backtrace
        else:
            output = [
                formatter.format(fd, hex(file.address), hex(oflags), pos, path, "")
            ]

        gdb.write("\n".join(output).rstrip())  # strip trailing spaces
        gdb.write("\n")

    def print_fdinfo_by_tcb(self, tcb):
        """print fdlist from tcb"""
        gdb.write(f"PID: {tcb['pid']}\n")
        group = tcb.group

        if not group:
            return

        if group in self.processed_groups:
            return

        self.processed_groups.add(group)

        headers = ["FD", "FILEP", "OFLAGS", "POS", "PATH", "BACKTRACE"]
        formatter = "{:<4}{:<12}{:<12}{:<10}{:<48}{:<50}"
        gdb.write(formatter.format(*headers) + "\n")

        fd_count = 0
        for fd, file in foreach_file(tcb):
            self.print_file_info(fd, file, formatter)
            fd_count += 1
        self.total_fd_count += fd_count

        gdb.write("\n")

    def diagnose(self, *args, **kwargs):
        output = gdb.execute("fdinfo", to_string=True)

        return {
            "title": "fdinfo report",
            "summary": f"Total files opened:{self.total_fd_count}",
            "result": "info",
            "command": "fdinfo",
            "message": output,
        }

    def invoke(self, arg, from_tty):
        parser = argparse.ArgumentParser(
            description="Get fdinfo for a process or all processes."
        )
        parser.add_argument("-p", "--pid", type=int, help="Optional process ID")

        try:
            args = parser.parse_args(gdb.string_to_argv(arg))
        except SystemExit:
            gdb.write("Invalid arguments.\n")
            return

        self.processed_groups = set()
        self.total_fd_count = 0
        tcbs = [utils.get_tcb(args.pid)] if args.pid else utils.get_tcbs()
        for tcb in tcbs:
            self.print_fdinfo_by_tcb(tcb)


class Mount(gdb.Command):
    def __init__(self):
        if not utils.get_symbol_value("CONFIG_DISABLE_MOUNTPOINT"):
            super().__init__("mount", gdb.COMMAND_USER)
            self.mount_count = 0

    def diagnose(self, *args, **kwargs):
        output = gdb.execute("mount", to_string=True)

        return {
            "title": "File system mount information",
            "summary": f"Total {self.mount_count} mount points",
            "command": "mount",
            "result": "info",
            "message": output or "No mount",
        }

    def invoke(self, args, from_tty):
        self.mount_count = 0
        nodes = filter(
            lambda x: inode_gettype(x[0]) == InodeType.MOUNTPT, foreach_inode()
        )
        for node, path in nodes:
            statfs = node.u.i_mops.statfs
            funcname = gdb.block_for_pc(int(statfs)).function.print_name
            fstype = funcname.split("_")[0]
            gdb.write("  %s type %s\n" % (path, fstype))
            self.mount_count += 1


class ForeachInode(gdb.Command):
    """Dump each inode info"""

    def __init__(self):
        super().__init__("foreach inode", gdb.COMMAND_USER)
        self.level = 4096

    def get_root_inode(self, addr_or_expr):
        try:
            return utils.Value(int(addr_or_expr, 0)).cast(
                gdb.lookup_type("struct inode").pointer()
            )
        except ValueError:
            return utils.gdb_eval_or_none(addr_or_expr)

    def parse_arguments(self, argv):
        parser = argparse.ArgumentParser(description="foreach inode command")
        parser.add_argument(
            "-L",
            "--level",
            type=int,
            default=None,
            help="Only render the tree to a specific depth",
        )
        parser.add_argument(
            "addr_or_expr",
            type=str,
            nargs="?",
            default=None,
            help="set the start inode to be tranversed",
        )
        try:
            args = parser.parse_args(argv)
        except SystemExit:
            return None
        return {
            "level": args.level if args.level else 4096,
            "root_inode": (
                self.get_root_inode(args.addr_or_expr)
                if args.addr_or_expr
                else utils.gdb_eval_or_none("g_root_inode")
            ),
        }

    def print_inode_info(self, node: Inode, level, prefix):
        if level > self.level:
            return
        while node:
            if node.i_peer:
                initial_indent = prefix + "├── "
                subsequent_indent = prefix + "│   "
                newprefix = prefix + "│   "
            else:
                initial_indent = prefix + "└── "
                subsequent_indent = prefix + "    "
                newprefix = prefix + "    "
            gdb.write(
                "%s [%s], %s, %s\n"
                % (initial_indent, get_inode_name(node), node.i_ino, node)
            )
            gdb.write(
                "%s i_crefs: %s, i_flags: %s, i_private: %s\n"
                % (
                    subsequent_indent,
                    node.i_crefs,
                    node.i_flags,
                    node.i_private,
                )
            )
            if CONFIG_PSEUDOFS_FILE:
                gdb.write("%s i_size: %s\n" % (subsequent_indent, node.i_size))
            if CONFIG_PSEUDOFS_ATTRIBUTES:
                gdb.write(
                    "%s i_mode: %s, i_owner: %s, i_group: %s\n"
                    % (
                        subsequent_indent,
                        node.i_mode,
                        node.i_owner,
                        node.i_group,
                    )
                )
                gdb.write(
                    "%s i_atime: %s, i_mtime: %s, i_ctime: %s\n"
                    % (
                        subsequent_indent,
                        node.i_atime,
                        node.i_mtime,
                        node.i_ctime,
                    )
                )
            if node.i_child:
                self.print_inode_info(node.i_child, level + 1, newprefix)
            node = node.i_peer

    def diagnose(self, *args, **kwargs):
        output = gdb.execute("foreach inode", to_string=True)

        return {
            "title": "File node information",
            "summary": "inode formation dump",
            "command": "foreach inode",
            "result": "info",
            "message": output,
        }

    def invoke(self, args, from_tty):
        arg = self.parse_arguments(args.split(" "))
        if not arg:
            return
        self.level = arg["level"]
        self.print_inode_info(arg["root_inode"], 1, "")


class InfoShmfs(gdb.Command):
    """Show share memory usage"""

    def __init__(self):
        if CONFIG_FS_SHMFS:
            super().__init__("info shm", gdb.COMMAND_USER)
            self.total_size = 0
            self.block_count = 0

    def shm_filter(self, node: Inode, path):
        if inode_gettype(node) != InodeType.SHM:
            return

        obj = node.i_private.cast(gdb.lookup_type("struct shmfs_object_s").pointer())
        length = obj.length
        paddr = obj.paddr
        print(f"  {path} memsize: {length}, paddr: {paddr}")

        self.total_size += length / 1024
        self.block_count += 1

    def diagnose(self, *args, **kwargs):
        output = gdb.execute("info shm", to_string=True)

        return {
            "title": "Share memory usage",
            "summary": f"Total used:{self.total_size}kB, {self.block_count}blocks",
            "result": "info",
            "command": "info shm",
            "message": output or "No InfoShmfs",
        }

    def invoke(self, args, from_tty):
        self.total_size = 0
        self.block_count = 0
        nodes = filter(lambda x: inode_gettype(x[0]) == InodeType.SHM, foreach_inode())
        for node, path in nodes:
            obj = node.i_private.cast(
                gdb.lookup_type("struct shmfs_object_s").pointer()
            )
            length = obj.length
            paddr = obj.paddr
            print(f"  {path} memsize: {length}, paddr: {paddr}")

            self.total_size += length / 1024
            self.block_count += 1
