# -*- coding: utf-8 -*-
#
#   Copyright (C) 2024 Rocky Bernstein <rocky@gnu.org>
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.

import linecache
import os.path as osp
import re
import sys
from inspect import ismodule, currentframe
from tempfile import NamedTemporaryFile
from types import CodeType

import pyficache

from trepan.lib.stack import check_path_with_frame, frame2file, is_eval_or_exec_stmt
from trepan.processor import cmdfns
from trepan.processor.cmdfns import deparse_fn

from trepan.lib.format import (  # Opcode,
    Filename,
    Hex,
    LineNumber,
    Symbol,
    format_token,
)

try:
    from trepan.lib.deparse import deparse_and_cache

    have_deparse_and_cache = True
except ImportError:
    have_deparse_and_cache = False
    pass

warned_file_mismatches = set()


def format_code(code_object: CodeType, style) -> str:
    """
    Format according to "style" a Python code object. The
    formatted string is returned.
    """
    formatted_line = format_token(LineNumber, str(code_object.co_firstlineno), style=style)
    formatted_id = format_token(Hex, hex(id(code_object)), style=style)
    formatted_name = format_token(Symbol, code_object.co_name, style=style)
    formatted_filename = format_token(Filename, code_object.co_filename, style=style)
    return (
        f"<code object {formatted_name} at {formatted_id} "
        f"file {formatted_filename}, line {formatted_line}>"
    )


def format_frame(frame_object, style) -> str:
    """
    Format according to "style" a Python frame object. The
    formatted string is returned.
    """
    formatted_line = format_token(LineNumber, str(frame_object.f_lineno), style=style)
    formatted_id = format_token(Hex, hex(id(frame_object)), style=style)
    formatted_filename = format_token(Filename, frame_object.f_code.co_filename, style=style)
    return (
        f"<frame at {formatted_id} "
        f"file {formatted_filename}, line {formatted_line}>"
    )


def print_source_line(msg, lineno, line, event_str=None):
    """Print out a source line of text , e.g. the second
    line in:
        (/tmp.py:2):  <module>
        L -- 2 import sys,os
        (trepan3k)

    We define this method
    specifically so it can be customized for such applications
    like ipython."""

    # We don't use the filename normally. ipython and other applications
    # however might.
    return msg(f"{event_str} {lineno} {line}")


def print_source_location_info(
    print_fn, filename, lineno, fn_name=None, f_lasti=None, remapped_file=None
):
    """Print out a source location , e.g. the first line in
    line in:
        (/tmp.py:2 @21):  <module>
        L -- 2 import sys,os
        (trepan3k)
    """
    if remapped_file and filename != remapped_file:
        mess = f"({remapped_file}:{lineno} remapped {filename}"
    else:
        mess = f"({filename}:{lineno}"
    if f_lasti and f_lasti != -1:
        mess += " @%d" % f_lasti
        pass
    mess += "):"
    if fn_name and fn_name != "?":
        mess += f" {fn_name}"
        pass
    print_fn(mess)
    return

def print_location(proc_obj):
    """Show where we are. GUI's and front-end interfaces often
    use this to update displays. So it is helpful to make sure
    we give at least some place that's located in a file.
    """

    def prefix_for_filename(deparsed_text: str) -> str:
        """
        Return a reasonable filename-friendly string from some
        deparsed text. We do this so that the user gets some idea of
        what the string (source code text) is contained in the file.

        """
        lines = deparsed_text.split("\n")
        # FIXME Rather than blindly take the first line,
        # check if it is blank and if so use other lines.
        first_text_line = lines[0]
        return proc_obj._saferepr(first_text_line)[1:-1][:10]

    i_stack = proc_obj.curindex
    if i_stack is None or proc_obj.stack is None:
        return False
    core_obj = proc_obj.core
    dbgr_obj = proc_obj.debugger
    intf_obj = dbgr_obj.intf[-1]

    # Evaluation routines like "exec" don't show useful location
    # info. In these cases, we will use the position before that in
    # the stack.  Hence the looping below which in practices loops
    # once and sometimes twice.
    remapped_file = None
    source_text = None
    eval_kind = None
    i_stack = min(i_stack, len(proc_obj.stack) - 1)
    while i_stack >= 0:
        frame, lineno = proc_obj.stack[i_stack]

        # Before starting a program a location for a module with
        # line number 0 may be reported. Treat that as though
        # we were on the first line.
        if frame.f_code.co_name == "<module>" and lineno == 0:
            lineno = 1

        i_stack -= 1

        #         # Next check to see that local variable breadcrumb exists and
        #         # has the magic dynamic value.
        #         # If so, it's us and we don't normally show this.a
        #         if 'breadcrumb' in frame.f_locals:
        #             if self.run == frame.f_locals['breadcrumb']:
        #                 break

        filename = frame2file(core_obj, frame, canonic=False)
        if "<string>" == filename and dbgr_obj.eval_string:
            eval_kind = "string"
            remapped_file = filename
            filename = pyficache.unmap_file(filename)
            if "<string>" == filename:
                remapped_file = cmdfns.source_tempfile_remap(
                    "eval_string",
                    dbgr_obj.eval_string,
                    tempdir=proc_obj.settings("tempdir"),
                )
                pyficache.remap_file(filename, remapped_file)
                filename, lineno = pyficache.unmap_file_line(filename, lineno)
                pass
            pass
        elif "<string>" == filename:
            # FIXME: should change filename to disambiguated <string> everywhere.
            eval_kind = is_eval_or_exec_stmt(frame) or "code-"
            deparsed = deparse_fn(frame.f_code)
            if deparsed:
                # Create a nice prefix for the temporary file to write.
                # Use the exec type and first line of the deparsed text.
                leading_code_str = prefix_for_filename(deparsed.text)
                prefix = f"{eval_kind}-{leading_code_str}-"

                remapped_file = cmdfns.source_tempfile_remap(
                    prefix,
                    deparsed.text,
                    tempdir=proc_obj.settings("tempdir"),
                )
                # FIXME: pyficache remaps seems backwards
                pyficache.remap_file(remapped_file, filename)
                filename = remapped_file
            else:
                eval_kind = is_eval_or_exec_stmt(frame)
                deparsed = deparse_fn(frame.f_code)
                if deparsed is not None:
                    source_text = deparsed.text
                # else:
                #   print("Can't deparse", frame.f_code)
                if source_text is None and eval_kind:
                    source_text = f"{eval_kind}(...)"
                    pass
                pass
            pass
        else:
            m = re.search("^<frozen (.*)>", filename)
            if m and m.group(1) in pyficache.file2file_remap:
                remapped_file = pyficache.file2file_remap[m.group(1)]
                pass
            elif filename in pyficache.file2file_remap:
                remapped_file = pyficache.file2file_remap[filename]
                filename = remapped_file
                pass
            elif pyficache.main.remap_re_hash:
                remapped_file = pyficache.remap_file_pat(
                    filename, pyficache.main.remap_re_hash
                )
            elif m and m.group(1) in sys.modules:
                remapped_file = m.group(1)
                pyficache.remap_file(filename, remapped_file)
            pass

        opts = {
            "reload_on_change": proc_obj.settings("reload"),
            "output": proc_obj.settings("highlight"),
        }

        if proc_obj.debugger.settings.get("highlight", "plain") == "plain":
            opts["style"] = "plain"
        elif "style" in proc_obj.debugger.settings:
            opts["style"] = proc_obj.settings("style")

        pyficache.update_cache(filename)
        line = pyficache.getline(filename, lineno, opts)
        if not line:
            if (
                not source_text
                and filename.startswith("<string: ")
                and proc_obj.curframe.f_code
                and have_deparse_and_cache
            ):
                # Deparse the code object into a temp file and remap the line from code
                # into the corresponding line of the tempfile
                co = proc_obj.curframe.f_code
                tempdir = proc_obj.settings("tempdir")
                temp_filename, _ = deparse_and_cache(
                    co, proc_obj.errmsg, tempdir=tempdir
                )
                lineno = 1
                # _, lineno = pyficache.unmap_file_line(temp_filename, lineno, True)
                if temp_filename:
                    filename = temp_filename
                pass

            else:
                # FIXME:
                if source_text:
                    lines = source_text.split("\n")
                    temp_name = "string-" + prefix_for_filename(source_text)
                else:
                    # try with good ol linecache and consider fixing pyficache
                    lines = linecache.getlines(filename)
                    temp_name = filename
                if lines and not filename.endswith(".pyasm"):
                    # FIXME: DRY code with version in cmdproc.py print_location
                    prefix = osp.basename(temp_name).split(".")[0]
                    fd = NamedTemporaryFile(
                        suffix=".py",
                        prefix=prefix,
                        delete=False,
                        dir=proc_obj.settings("tempdir"),
                    )
                    with fd:
                        fd.write("".join(lines).encode("utf-8"))
                        remapped_file = fd.name
                        pyficache.remap_file(remapped_file, filename)
                    fd.close()
                    intf_obj.msg(f"remapped file {filename} to {remapped_file}")

                    pass

            if isinstance(proc_obj.curframe, int):
                breakpoint()
            line = linecache.getline(filename, lineno, proc_obj.curframe.f_globals)
            if not line:
                m = re.search("^<frozen (.*)>", filename)
                if m and m.group(1):
                    remapped_file = m.group(1)
                    try_module = sys.modules.get(remapped_file)
                    if (
                        try_module
                        and ismodule(try_module)
                        and hasattr(try_module, "__file__")
                    ):
                        remapped_file = sys.modules[remapped_file].__file__
                        pyficache.remap_file(filename, remapped_file)
                        line = linecache.getline(
                            remapped_file, lineno, proc_obj.curframe.f_globals
                        )
                    else:
                        remapped_file = m.group(1)
                        code = proc_obj.curframe.f_code
                        filename, line = cmdfns.deparse_getline(
                            code, remapped_file, lineno, opts
                        )
                    pass
            pass

        if eval_kind is None:
            fn_name = frame.f_code.co_name
            try:
                match, reason = check_path_with_frame(frame, filename)
                if not match:
                    if filename not in warned_file_mismatches:
                        proc_obj.errmsg(reason)
                        warned_file_mismatches.add(filename)
            except Exception:
                pass
            pass
        else:
            fn_name = eval_kind

        last_i = frame.f_lasti
        print_source_location_info(
            intf_obj.msg,
            filename,
            lineno,
            fn_name,
            remapped_file=remapped_file,
            f_lasti=last_i,
        )
        if line and len(line.strip()) != 0:
            if proc_obj.event:
                print_source_line(
                    intf_obj.msg, lineno, line, proc_obj.event2short[proc_obj.event]
                )
            pass
        if "<string>" != filename:
            break
        pass

    if proc_obj.event in ["return", "exception"]:
        val = proc_obj.event_arg
        intf_obj.msg(f"R=> {proc_obj._saferepr(val)}")
        pass
    elif (
        proc_obj.event == "call"
        and proc_obj.curframe.f_locals.get("__name__", "") != "__main__"
    ):
        try:
            proc_obj.commands["info"].run(["info", "locals"])
        except Exception:
            pass
    return True

# Demo it
if __name__ == "__main__":
    def five():
        from trepan.processor.cmdproc import CommandProcessor
        from trepan.processor.command.mock import MockDebugger
        d = MockDebugger()
        cmdproc = CommandProcessor(d.core)
        cmdproc.frame = currentframe()
        cmdproc.event = "line"
        cmdproc.setup()
        print_location(cmdproc)
        cmdproc.curindex = 1
        cmdproc.curframe = cmdproc.stack[cmdproc.curindex][0]
        print_location(cmdproc)

    exec("five()")
