#!/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 string

import fbchisellldbbase as fb
import fbchisellldbobjcruntimehelpers as runtimeHelpers


def lldbcommands():
    return [FBPrintMethods(), FBPrintProperties(), FBPrintBlock()]


class FBPrintMethods(fb.FBCommand):
    def name(self):
        return "pmethods"

    def description(self):
        return "Print the class and instance methods of a class."

    def options(self):
        return [
            fb.FBCommandArgument(
                short="-a",
                long="--address",
                arg="showaddr",
                help="Print the implementation address of the method",
                default=False,
                boolean=True,
            ),
            fb.FBCommandArgument(
                short="-i",
                long="--instance",
                arg="insmethod",
                help="Print the instance methods",
                default=False,
                boolean=True,
            ),
            fb.FBCommandArgument(
                short="-c",
                long="--class",
                arg="clsmethod",
                help="Print the class methods",
                default=False,
                boolean=True,
            ),
            fb.FBCommandArgument(
                short="-n",
                long="--name",
                arg="clsname",
                help="Take the argument as class name",
                default=False,
                boolean=True,
            ),
        ]

    def args(self):
        return [
            fb.FBCommandArgument(
                arg="instance or class",
                type="instance or Class",
                help="an Objective-C Class.",
            )
        ]

    def run(self, arguments, options):
        cls = getClassFromArgument(arguments[0], options.clsname)

        if options.clsmethod:
            print("Class Methods:")
            printClassMethods(cls, options.showaddr)

        if options.insmethod:
            print("\nInstance Methods:")
            printInstanceMethods(cls, options.showaddr)

        if not options.clsmethod and not options.insmethod:
            print("Class Methods:")
            printClassMethods(cls, options.showaddr)
            print("\nInstance Methods:")
            printInstanceMethods(cls, options.showaddr)


class FBPrintProperties(fb.FBCommand):
    def name(self):
        return "pproperties"

    def description(self):
        return "Print the properties of an instance or Class"

    def options(self):
        return [
            fb.FBCommandArgument(
                short="-n",
                long="--name",
                arg="clsname",
                help="Take the argument as class name",
                default=False,
                boolean=True,
            )
        ]

    def args(self):
        return [
            fb.FBCommandArgument(
                arg="instance or class",
                type="instance or Class",
                help="an Objective-C Class.",
            )
        ]

    def run(self, arguments, options):
        cls = getClassFromArgument(arguments[0], options.clsname)

        printProperties(cls)


class FBPrintBlock(fb.FBCommand):
    def name(self):
        return "pblock"

    def description(self):
        return "Print the block`s implementation address and signature"

    def args(self):
        return [
            fb.FBCommandArgument(arg="block", help="The block object you want to print")
        ]

    def run(self, arguments, options):
        block = arguments[0]

        # http://clang.llvm.org/docs/Block-ABI-Apple.html
        tmpString = """
        enum {
          BLOCK_HAS_COPY_DISPOSE =  (1 << 25),
          BLOCK_HAS_CTOR =          (1 << 26), // helpers have C++ code
          BLOCK_IS_GLOBAL =         (1 << 28),
          BLOCK_HAS_STRET =         (1 << 29), // IFF BLOCK_HAS_SIGNATURE
          BLOCK_HAS_SIGNATURE =     (1 << 30),
        };
        struct Block_literal_1 {
          void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
          int flags;
          int reserved;
          void (*invoke)(void *, ...);
          struct Block_descriptor_1 {
              unsigned long int reserved; // NULL
              unsigned long int size;         // sizeof(struct Block_literal_1)
              // optional helper functions
              void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
              void (*dispose_helper)(void *src);             // IFF (1<<25)
              // required ABI.2010.3.16
              const char *signature;                         // IFF (1<<30)
          } *descriptor;
          // imported variables
        };
        struct Block_literal_1 real = *((__bridge struct Block_literal_1 *)$block);
        NSMutableDictionary *dict = (id)[NSMutableDictionary dictionary];
        
        [dict setObject:(id)[NSNumber numberWithLong:(long)real.invoke] forKey:@"invoke"];
        
        if (real.flags & BLOCK_HAS_SIGNATURE) {
          char *signature;
          if (real.flags & BLOCK_HAS_COPY_DISPOSE) {
              signature = (char *)(real.descriptor)->signature;
          } else {
              signature = (char *)(real.descriptor)->copy_helper;
          }

          NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:signature];
          NSMutableArray *types = [NSMutableArray array];

          [types addObject:(id)[NSString stringWithUTF8String:(char *)[sig methodReturnType]]];

          for (NSUInteger i = 0; i < sig.numberOfArguments; i++) {
              char *type = (char *)[sig getArgumentTypeAtIndex:i];
              [types addObject:(id)[NSString stringWithUTF8String:type]];
          }
          
          [dict setObject:types forKey:@"signature"];
        }
        
        RETURN(dict);
        """
        command = string.Template(tmpString).substitute(block=block)
        json = fb.evaluate(command)

        signature = json["signature"]
        if not signature:
            print("Imp: " + hex(json["invoke"]))
            return

        sigStr = "{} ^(".format(decode(signature[0]))
        # the block`s implementation always take the block as it`s first argument, so we ignore it
        sigStr += ", ".join([decode(m) for m in signature[2:]])
        sigStr += ");"

        print("Imp: " + hex(json["invoke"]) + "    Signature: " + sigStr)


# helpers
def isClassObject(arg):
    return runtimeHelpers.class_isMetaClass(runtimeHelpers.object_getClass(arg))


def getClassFromArgument(arg, is_classname):
    cls = arg
    if is_classname:
        cls = runtimeHelpers.objc_getClass(cls)
        if not int(cls, 16):
            raise Exception('Class "{}" not found'.format(arg))
    else:
        if not isClassObject(cls):
            cls = runtimeHelpers.object_getClass(cls)
            if not isClassObject(cls):
                raise Exception(
                    "Invalid argument. Please specify an instance or a Class."
                )

    return cls


def printInstanceMethods(cls, showaddr=False, prefix="-"):
    methods = getMethods(cls)
    if not methods:
        print("No methods were found")

    for m in methods:
        if showaddr:
            print(prefix + " " + m.prettyPrintString() + " " + str(m.imp))
        else:
            print(prefix + " " + m.prettyPrintString())


def printClassMethods(cls, showaddr=False):
    printInstanceMethods(runtimeHelpers.object_getClass(cls), showaddr, "+")


def printProperties(cls, showvalue=False):
    props = getProperties(cls)
    for p in props:
        print(p.prettyPrintString())


def decode(code):
    encodeMap = {
        "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",
    }

    ret = code
    if code in encodeMap:
        ret = encodeMap[code]
    elif ret[0:1] == "@":
        if ret[1:2] == "?":  # @? represent a block
            ret = code
        elif ret[2:3] == "<":  # @"<aDelegate><bDelegate>"
            ret = "id" + ret[2:-1].replace("><", ", ")
        else:
            ret = ret[2:-1] + " *"
    elif ret[0:1] == "^":
        ret = decode(ret[1:]) + " *"

    return ret


# Notice that evaluateExpression doesn't work with variable arguments. such as -[NSString stringWithFormat:]
# I remove the "free(methods)" because it would cause evaluateExpressionValue to raise exception some time.
def getMethods(klass):
    tmpString = """
    unsigned int outCount;
    Method *methods = (Method *)class_copyMethodList((Class)$cls, &outCount);
    NSMutableArray *result = (id)[NSMutableArray array];
    
    for (int i = 0; i < outCount; i++) {
      NSMutableDictionary *m = (id)[NSMutableDictionary dictionary];

      SEL name = (SEL)method_getName(methods[i]);
      [m setObject:(id)NSStringFromSelector(name) forKey:@"name"];
      
      char * encoding = (char *)method_getTypeEncoding(methods[i]);
      [m setObject:(id)[NSString stringWithUTF8String:encoding] forKey:@"type_encoding"];
      
      NSMutableArray *types = (id)[NSMutableArray array];
      NSInteger args = (NSInteger)method_getNumberOfArguments(methods[i]);
      for (int idx = 0; idx < args; idx++) {
          char *type = (char *)method_copyArgumentType(methods[i], idx);
          [types addObject:(id)[NSString stringWithUTF8String:type]];
      }
      [m setObject:types forKey:@"parameters_type"];
      
      char *ret_type = (char *)method_copyReturnType(methods[i]);
      [m setObject:(id)[NSString stringWithUTF8String:ret_type] forKey:@"return_type"];
      
      long imp = (long)method_getImplementation(methods[i]);
      [m setObject:[NSNumber numberWithLongLong:imp] forKey:@"implementation"];
      
      [result addObject:m];
    }
    RETURN(result);
  """
    command = string.Template(tmpString).substitute(cls=klass)
    methods = fb.evaluate(command)
    return [Method(m) for m in methods]


class Method:
    def __init__(self, json):
        self.name = json["name"]
        self.type_encoding = json["type_encoding"]
        self.parameters_type = json["parameters_type"]
        self.return_type = json["return_type"]
        self.imp = self.toHex(json["implementation"])

    def prettyPrintString(self):
        argnum = len(self.parameters_type)
        names = self.name.split(":")

        # the argnum count must be bigger then 2, index 0 for self, index 1 for SEL
        for i in range(2, argnum):
            arg_type = self.parameters_type[i]
            names[i - 2] = names[i - 2] + ":(" + decode(arg_type) + ")arg" + str(i - 2)

        string = " ".join(names)
        return "({}){}".format(decode(self.return_type), string)

    def toHex(self, addr):
        return hex(addr)

    def __str__(self):
        return (
            "<Method:"
            + self.oc_method
            + "> "
            + self.name
            + " --- "
            + self.type
            + " --- "
            + self.imp
        )


def getProperties(klass):
    tmpString = """
      NSMutableArray *result = (id)[NSMutableArray array];
      unsigned int count;
      objc_property_t *props = (objc_property_t *)class_copyPropertyList((Class)$cls, &count);
      for (int i = 0; i < count; i++) {
          NSMutableDictionary *dict = (id)[NSMutableDictionary dictionary];
          
          char *name = (char *)property_getName(props[i]);
          [dict setObject:(id)[NSString stringWithUTF8String:name] forKey:@"name"];
          
          char *attrstr = (char *)property_getAttributes(props[i]);
          [dict setObject:(id)[NSString stringWithUTF8String:attrstr] forKey:@"attributes_string"];
          
          NSMutableDictionary *attrsDict = (id)[NSMutableDictionary dictionary];
          unsigned int pcount;
          objc_property_attribute_t *attrs = (objc_property_attribute_t *)property_copyAttributeList(props[i], &pcount);
          for (int i = 0; i < pcount; i++) {
              NSString *name = (id)[NSString stringWithUTF8String:(char *)attrs[i].name];
              NSString *value = (id)[NSString stringWithUTF8String:(char *)attrs[i].value];
              [attrsDict setObject:value forKey:name];
          }
          [dict setObject:attrsDict forKey:@"attributes"];
          
          [result addObject:dict];
      }
      RETURN(result);
    """
    command = string.Template(tmpString).substitute(cls=klass)
    propsJson = fb.evaluate(command)
    return [Property(m) for m in propsJson]


class Property:
    def __init__(self, json):
        self.name = json["name"]
        self.attributes_string = json["attributes_string"]
        self.attributes = json["attributes"]

    # https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1
    def prettyPrintString(self):
        attrs = []
        if "N" in self.attributes:
            attrs.append("nonatomic")
        else:
            attrs.append("atomic")

        if "&" in self.attributes:
            attrs.append("strong")
        elif "C" in self.attributes:
            attrs.append("copy")
        elif "W" in self.attributes:
            attrs.append("weak")
        else:
            attrs.append("assign")

        if "R" in self.attributes:
            attrs.append("readonly")

        if "G" in self.attributes:
            attrs.append("getter={}".format(self.attributes["G"]))
        if "S" in self.attributes:
            attrs.append("setter={}".format(self.attributes["S"]))

        return "@property ({}) {} {};".format(
            ", ".join(attrs), decode(self.attributes["T"]), self.name
        )
