#!/usr/bin/python

# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import re

import fbchisellldbbase as fb
import lldb


def lldbcommands():
    return [FBPrintInvocation()]


class FBPrintInvocation(fb.FBCommand):
    def name(self):
        return "pinvocation"

    def description(self):
        return "Print the stack frame, receiver, and arguments of the current invocation. It will fail to print all arguments if any arguments are variadic (varargs).\n\nNOTE: Sadly this is currently only implemented on x86."

    def options(self):
        return [
            fb.FBCommandArgument(
                short="-a",
                long="--all",
                arg="all",
                default=False,
                boolean=True,
                help="Specify to print the entire stack instead of just the current frame.",
            )
        ]

    def run(self, arguments, options):
        target = lldb.debugger.GetSelectedTarget()

        if not re.match(r".*i386.*", target.GetTriple()):
            print("Only x86 is currently supported (32-bit iOS Simulator or Mac OS X).")
            return

        thread = target.GetProcess().GetSelectedThread()

        if options.all:
            for frame in thread:
                printInvocationForFrame(frame)
                print("---------------------------------")
        else:
            frame = thread.GetSelectedFrame()
            printInvocationForFrame(frame)


def printInvocationForFrame(frame):
    print(frame)

    symbolName = frame.GetSymbol().GetName()
    if not re.match(r"[-+]\s*\[.*\]", symbolName):
        return

    self = findArgAtIndexFromStackFrame(frame, 0)
    cmd = findArgAtIndexFromStackFrame(frame, 1)

    commandForSignature = (
        "[(id)"
        + self
        + " methodSignatureForSelector:(char *)sel_getName((SEL)"
        + cmd
        + ")]"
    )
    signatureValue = fb.evaluateExpressionValue("(id)" + commandForSignature)

    if (
        signatureValue.GetError() is not None
        and str(signatureValue.GetError()) != "success"
    ):
        print(
            "My sincerest apologies. I couldn't find a method signature for the selector."
        )
        return

    signature = signatureValue.GetValue()

    arg0 = stackStartAddressInSelectedFrame(frame)
    commandForInvocation = (
        "[NSInvocation _invocationWithMethodSignature:(id)"
        + signature
        + " frame:((void *)"
        + str(arg0)
        + ")]"
    )
    invocation = fb.evaluateExpression("(id)" + commandForInvocation)

    if invocation:
        prettyPrintInvocation(frame, invocation)
    else:
        print(frame)


def stackStartAddressInSelectedFrame(frame):
    # Determine if the %ebp register has already had the
    # stack register pushed into it (always the first instruction)
    frameSymbol = frame.GetSymbolContext(0).GetSymbol()
    frameStartAddress = frameSymbol.GetStartAddress().GetLoadAddress(
        lldb.debugger.GetSelectedTarget()
    )

    currentPC = frame.GetPC()

    offset = currentPC - frameStartAddress

    if offset == 0:
        return int(frame.EvaluateExpression("($esp + 4)").GetValue())
    elif offset == 1:
        return int(frame.EvaluateExpression("($esp + 8)").GetValue())
    else:
        return int(frame.EvaluateExpression("($ebp + 8)").GetValue())


def findArgAtIndexFromStackFrame(frame, index):
    return fb.evaluateExpression(
        "*(int *)" + str(findArgAdressAtIndexFromStackFrame(frame, index))
    )


def findArgAdressAtIndexFromStackFrame(frame, index):
    arg0 = stackStartAddressInSelectedFrame(frame)
    arg = arg0 + 4 * index
    return arg


def prettyPrintInvocation(frame, invocation):
    object = fb.evaluateExpression("(id)[(id)" + invocation + " target]")
    description = fb.evaluateExpressionValue("(id)" + invocation).GetObjectDescription()
    argDescriptions = description.splitlines(True)[4:]

    print("NSInvocation: " + invocation)
    print("self: " + fb.evaluateExpression("(id)" + object))

    if len(argDescriptions) > 0:
        print(
            "\n" + str(len(argDescriptions)) + " Arguments:"
            if len(argDescriptions) > 1
            else "\nArgument:"
        )

        index = 2
        for argDescription in argDescriptions:
            s = re.sub(r"argument [0-9]+: ", "", argDescription)

            address = findArgAdressAtIndexFromStackFrame(frame, index)

            encoding = s.split(" ")[0]
            description = " ".join(s.split(" ")[1:])

            readableString = argumentAsString(frame, address, encoding)

            if readableString:
                print(readableString)
            else:
                if encoding[0] == "{":
                    encoding = encoding[1:]
                print(
                    (
                        hex(address) + ", address of " + encoding + " " + description
                    ).strip()
                )

            index += 1


def argumentAsString(frame, address, encoding):  # noqa C901
    if encoding[0] == "{":
        encoding = encoding[1:]

    encodingMap = {
        "c": "char",
        "i": "int",
        "s": "short",
        "l": "long",
        "q": "long long",
        "C": "unsigned char",
        "I": "unsigned int",
        "S": "unsigned short",
        "L": "unsigned long",
        "Q": "unsigned long long",
        "f": "float",
        "d": "double",
        "B": "bool",
        "v": "void",
        "*": "char *",
        "@": "id",
        "#": "Class",
        ":": "SEL",
    }

    pointers = ""
    while encoding[0] == "^":
        pointers += "*"
        encoding = encoding[1:]

    type = None
    if encoding in encodingMap:
        type = encodingMap[encoding]

    if type and pointers:
        type = type + " " + pointers

    if not type:
        # Handle simple structs: {CGPoint=ff}, {CGSize=ff},
        # {CGRect={CGPoint=ff}{CGSize=ff}}
        if encoding[0] == "{":
            encoding = encoding[1:]

        type = re.sub(r"=.*", "", encoding)
        if pointers:
            type += " " + pointers

    if type:
        value = frame.EvaluateExpression("*(" + type + " *)" + str(address))

        if value.GetError() is None or str(value.GetError()) == "success":
            description = None

            if encoding == "@":
                description = value.GetObjectDescription()

            if not description:
                description = value.GetValue()
            if not description:
                description = value.GetSummary()
            if description:
                return type + ": " + description

    return None
