import idaapi
import os, sys, types
from idc import *
from payload import Item
from copy import deepcopy
import dataviewers

drgadget_plugins_path = idaapi.idadir(os.path.join("plugins", "drgadget", "plugins"))

sys.path.append(drgadget_plugins_path)


# TODO: remove load- and save payload dialogs from context menu
# and move to IDA's File menu?
class ropviewer_t(idaapi.simplecustviewer_t):
    def __init__(self, payload):
        self.payload = payload

        # FIXME: ugly
        self.menu_loadfromfile = None
        self.menu_savetofile = None
        self.menu_copyitem = None
        self.menu_cutitem = None
        self.menu_pasteitem = None
        self.menu_insertitem = None
        self.menu_jumpto = None
        self.menu_toggle = None
        self.menu_deleteitem = None
        self.menu_edititem = None
        self.menu_reset = None
        self.menu_comment = None
        self.menu_refresh = None

        self.window_created = False

        self.capGadget = "Dr. Gadget"
        self.capCodeData = "Code / Data"
        self.capHex = "Hexdump"
        self.capInfo = "Info"

        self.pluginlist = self.load_plugins()

        self.clipboard = None

        self.dav = dataviewers.simpledataviewer_t()
        self.dav.Create(self.capCodeData)

        self.hv = dataviewers.simpledataviewer_t()
        self.hv.Create(self.capHex)

        self.iv = dataviewers.simpledataviewer_t()
        self.iv.Create(self.capInfo)

        idaapi.simplecustviewer_t.__init__(self)

    def load_plugins(self):
        global drgadget_plugins_path

        pluginlist = []
        print "loading extensions..."
        for (_path, _dir, files) in os.walk(drgadget_plugins_path):
            for f in files:
                name, ext = os.path.splitext(f)
                if ext == ".py":
                    print "* %s" % name
                    plugin = __import__(name)
                    # add instance of drgadgetplugin_t class to list
                    pluginlist.append(plugin.drgadgetplugin_t(self.payload, self))
        return pluginlist

    # workaround for a bug (related to IDA itself?)
    # do not allow the window to be opened more than once
    def Show(self):
        if not self.window_created:
            self.window_created = True
            return idaapi.simplecustviewer_t.Show(self)

        return

    def Create(self):
        if not idaapi.simplecustviewer_t.Create(self, self.capGadget):
            return False
        if self.payload:
            self.refresh()
        else:
            self.ClearLines()

        return True

    def OnClose(self):
        self.window_created = False

    def create_colored_line(self, n):
        # todo
        item = self.get_item(n)
        if item != None:
            typ = item.type

            width = self.payload.proc.get_pointer_size()
            cline = idaapi.COLSTR("%04X  " % (n * width), idaapi.SCOLOR_AUTOCMT)
            ea = item.ea
            fmt = self.payload.proc.get_data_fmt_string()
            elem = fmt % ea
            if typ == Item.TYPE_CODE:
                color = idaapi.SCOLOR_CODNAME if SegStart(ea) != BADADDR else idaapi.SCOLOR_ERROR
                elem = idaapi.COLSTR(elem, color)
            else:
                elem = idaapi.COLSTR(elem, idaapi.SCOLOR_DNUM)
            cline += elem

            comm = ""
            if len(item.comment):
                comm += " ; %s" % item.comment
            if len(comm):
                cline += idaapi.COLSTR(comm, idaapi.SCOLOR_AUTOCMT)
            return cline

    def clear_clipboard(self):
        self.clipboard = None

    def set_clipboard(self, item):
        self.clipboard = item

    def get_clipboard(self):
        return self.clipboard

    def create_colored_lines(self):
        lines = []
        for i in xrange(self.payload.get_number_of_items()):
            l = self.create_colored_line(i)
            lines.append(l)
        return lines

    def copy_item(self, n):
        item = self.get_item(n)
        if item != None:
            self.set_clipboard((n, "c", item))

    def paste_item(self, n):
        if self.get_clipboard() != None:
            _, mode, item = self.get_clipboard()
            self.insert_item(n, item)
            self.refresh()
            if mode == 'x':
                self.clear_clipboard()

    def cut_item(self, n):
        item = self.get_item_at_cur_line()
        if item != None:
            self.set_clipboard((n, "x", item))
            self.delete_item(n, False)

    def edit_item(self, n):
        item = self.get_item(n)
        if item != None:
            val = item.ea

            newVal = AskAddr(val, "Feed me!")
            if newVal != None:
                item.ea = newVal
                self.set_item(n, item)
                self.refresh()

    def get_item(self, n):
        item = None
        if n < 0:
            n = 0
        if n < self.payload.get_number_of_items():
            item = deepcopy(self.payload.get_item(n))
        return item

    def get_item_at_cur_line(self):
        n = self.GetLineNo()
        return self.get_item(n)

    def inc_item_value(self, n):
        item = self.get_item(n)
        if item != None:
            item.ea += 1
            self.set_item(n, item)

    def dec_item_value(self, n):
        item = self.get_item(n)
        if item != None:
            item.ea -= 1
            self.set_item(n, item)

    def insert_item(self, n, item=None):
        if self.Count() == 0:
            n = 0
        if item == None:
            item = Item(0, Item.TYPE_DATA)
        self.payload.insert_item(n, item)
        self.refresh()

    def set_item(self, n, item):
        self.payload.set_item(n, item)
        self.refresh()

    def delete_item(self, n, ask=True):
        item = self.get_item(n)
        if item != None:
            result = 1
            if ask:
                result = AskYN(0, "Delete item?")
            if result == 1:
                self.payload.remove_item(self.GetLineNo())
                self.refresh()

    def get_comment(self, n):
        result = ""
        item = self.get_item(n)
        if item != None:
            result = item.comment
        return result

    def set_comment(self, n):
        item = self.get_item(n)
        if item is not None:
            s = AskStr(item.comment, "Enter Comment")
            if s is not None:
                item.comment = s
                self.set_item(n, item)

    def toggle_item(self, n):
        item = self.get_item(n)
        if item != None:
            if item.type == Item.TYPE_CODE:
                item.type = Item.TYPE_DATA
            else:
                ea = item.ea
                item.type = Item.TYPE_CODE

            self.set_item(n, item)
            l = self.create_colored_line(n)
            self.EditLine(n, l)
            self.Refresh()

    def jump_to_item_ea(self, n):
        item = self.get_item(n)
        if item != None:
            if item.type == Item.TYPE_CODE:
                Jump(item.ea)

    def refresh(self):
        self.ClearLines()
        for line in self.create_colored_lines():
            self.AddLine(line)
        self.Refresh()

    def show_content_viewers(self):

        self.dav.Show()
        self.hv.Show()
        self.iv.Show()

        # TODO: the docking code heavily lacks documentation. seems to be bugged as well.
        # also, why do we have to call the code twice for a better alignment of the docked windows?
        # have to deal with the following layout for now :[
        for i in xrange(2):
            idaapi.set_dock_pos(self.capGadget, self.capGadget, idaapi.DP_FLOATING, 0, 0, 1200, 500)
            idaapi.set_dock_pos(self.capInfo, self.capGadget, idaapi.DP_BOTTOM)
            idaapi.set_dock_pos(self.capHex, self.capGadget, idaapi.DP_RIGHT)
            idaapi.set_dock_pos(self.capCodeData, self.capHex, idaapi.DP_RIGHT)

    def update_content_viewers(self, n=None):
        if n is None:
            n = self.GetLineNo()

        item = self.get_item(n)

        self.dav.clear()
        self.hv.clear()
        self.iv.clear()

        if item is not None:
            if item.type == Item.TYPE_CODE:
                # get disassembly and hex stream
                dis = self.payload.da.get_disasm(item.ea)
                for line in dis:
                    self.dav.add_line(line[0])
                    self.hv.add_line(line[1])

                # get various info
                seg = idaapi.getseg(item.ea)
                if seg:
                    name = idaapi.get_true_segm_name(seg)
                    perm = seg.perm
                    ltype = "ld" if seg.is_loader_segm() else "dbg"
                    ea_start = seg.startEA
                    ea_end = seg.endEA

                    perms = ""
                    perms += "R" if perm & idaapi.SEGPERM_READ != 0 else "."
                    perms += "W" if perm & idaapi.SEGPERM_WRITE != 0 else "."
                    perms += "X" if perm & idaapi.SEGPERM_EXEC != 0 else "."
                    self.iv.add_line("<%s> [%X - %X], %s, [%s]" % (name, ea_start, ea_end, ltype, perms))
            else:
                stype = GetStringType(item.ea)
                if stype is not None:
                    scontent = GetString(item.ea, -1, stype)
                    if scontent != None and len(scontent):
                        self.dav.add_line(idaapi.COLSTR("\"%s\"" % scontent, idaapi.SCOLOR_DSTR))
                        # length = idaapi.get_max_ascii_length(item.ea, stype, idaapi.ALOPT_IGNHEADS)
                        # self.hv.add_line()
                else:
                    scontent = GetString(item.ea, -1, ASCSTR_C)
                    if scontent != None and len(scontent):
                        self.dav.add_line("\"%s\"" % scontent)

        self.dav.update()
        self.hv.update()
        self.iv.update()

    def OnDblClick(self, shift):
        n = self.GetLineNo()
        self.jump_to_item_ea(n)
        return True

    def OnKeydown(self, vkey, shift):
        n = self.GetLineNo()

        # print "OnKeydown, vkey=%d shift=%d lineno = %d" % (vkey, shift, n)

        # ESCAPE
        if vkey == 27:
            self.Close()

        # ENTER
        elif vkey == 13:
            self.jump_to_item_ea(n)

        # CTRL
        elif shift == 4:
            if vkey == ord("C"):
                self.copy_item(n)

            elif vkey == ord("X"):
                self.cut_item(n)

            elif vkey == ord("X"):
                self.cut_item(n)

            elif vkey == ord("V"):
                self.paste_item(n)

            elif vkey == ord("N"):
                if AskYN(1, "Are you sure?") == 1:
                    self.erase_all()

            elif vkey == ord("L"):
                self.import_binary()

            elif vkey == ord("S"):
                self.export_binary()

        elif vkey == 45:  # Insert
            self.set_comment(self.GetLineNo())

        elif vkey == ord('O'):
            self.toggle_item(n)

        elif vkey == ord('D'):
            self.delete_item(n)

        elif vkey == ord("E"):
            self.edit_item(n)

        elif vkey == ord("I"):
            self.insert_item(n)

        elif vkey == ord("R"):
            self.refresh()

        # numeric key -
        elif vkey == 109:
            self.dec_item_value(n)

        # numeric key +
        elif vkey == 107:
            self.inc_item_value(n)

        # down key
        elif vkey == 40:
            n = min(n + 1, self.Count())

        # up key
        elif vkey == 38:
            n = max(n - 1, 0)

        self.update_content_viewers(n)
        return False  # always propagate the event onwards

    def OnCursorPosChanged(self):
        # TODO: update on Y coordinate changes only
        self.update_content_viewers()

    def OnHint(self, lineno):
        if self.payload.get_item(lineno).type != Item.TYPE_CODE:
            return None

        ea = self.payload.get_item(lineno).ea
        dis = self.payload.da.get_disasm(ea)
        hint = ""

        for l in dis:
            hint += l[0]

        size_hint = len(dis)
        return size_hint, hint

    def OnPopup(self):
        self.ClearPopupMenu()
        self.pluginmenuids = {}

        # FIXME: ugly
        if not self.Count():
            self.menu_new = self.AddPopupMenu("New", "Ctrl-N")
            self.AddPopupMenu("-")
            self.menu_loadfromfile = self.AddPopupMenu("Import ROP binary", "Ctrl-L")
            self.AddPopupMenu("-")
            self.menu_insertitem = self.AddPopupMenu("Insert item", "I")
            if self.get_clipboard() != None:
                self.menu_pasteitem = self.AddPopupMenu("Paste item", "Ctrl-V")
        else:
            self.menu_new = self.AddPopupMenu("New", "Ctrl-N")
            self.AddPopupMenu("-")
            self.menu_loadfromfile = self.AddPopupMenu("Import ROP binary", "Ctrl-L")
            self.menu_savetofile = self.AddPopupMenu("Export ROP binary", "Ctrl-S")
            self.AddPopupMenu("-")
            self.menu_insertitem = self.AddPopupMenu("Insert item", "I")
            self.menu_deleteitem = self.AddPopupMenu("Delete item", "D")
            self.menu_edititem = self.AddPopupMenu("Edit item", "E")
            self.menu_toggle = self.AddPopupMenu("Toggle item type", "O")
            self.menu_comment = self.AddPopupMenu("Add comment", "Insert")
            self.menu_reset = self.AddPopupMenu("Reset types")
            self.menu_jumpto = self.AddPopupMenu("Go to item address", "Enter")
            self.AddPopupMenu("-")
            self.menu_cutitem = self.AddPopupMenu("Cut item", "Ctrl-X")
            self.menu_copyitem = self.AddPopupMenu("Copy item", "Ctrl-C")
            self.menu_pasteitem = self.AddPopupMenu("Paste item", "Ctrl-V")
            self.AddPopupMenu("-")
            self.menu_refresh = self.AddPopupMenu("Refresh", "R")
            self.AddPopupMenu("-")

        # load dr gadget plugins
        for instance in self.pluginlist:
            result = instance.get_callback_list()
            if result != None:
                for r in result:
                    menu, cb, hotkey = r
                    self.pluginmenuids[self.AddPopupMenu(menu, hotkey)] = cb

        return True

    def import_binary(self):
        fileName = AskFile(0, "*.*", "Import ROP binary")
        if fileName and self.payload.load_from_file(fileName):
            self.refresh()

    def export_binary(self):
        fileName = AskFile(1, "*.*", "Export ROP binary")
        if fileName and self.payload.save_to_file(fileName):
            print "payload saved to %s" % fileName

    def erase_all(self):
        self.payload.init(items=[])
        self.refresh()

    def OnPopupMenu(self, menu_id):
        n = self.GetLineNo()

        if menu_id == self.menu_new:
            if AskYN(1, "Are you sure?") == 1:
                self.erase_all()

        elif menu_id == self.menu_loadfromfile:
            self.import_binary()

        elif menu_id == self.menu_savetofile:
            self.export_binary()

        elif menu_id == self.menu_jumpto:
            n = self.GetLineNo()
            Jump(self.payload.get_item(n).ea)

        elif menu_id == self.menu_reset:
            if AskYN(1, "Are you sure?") == 1:
                self.payload.reset_types()
                self.refresh()

        elif menu_id == self.menu_toggle:
            self.toggle_item(n)

        elif menu_id == self.menu_comment:
            self.set_comment(n)

        elif menu_id == self.menu_deleteitem:
            self.delete_item(n)

        elif menu_id == self.menu_insertitem:
            self.insert_item(n)

        elif menu_id == self.menu_edititem:
            self.edit_item(n)

        elif menu_id == self.menu_copyitem:
            self.copy_item(n)

        elif menu_id == self.menu_cutitem:
            self.cut_item(n)

        elif menu_id == self.menu_pasteitem:
            self.paste_item(n)

        elif menu_id == self.menu_refresh:
            self.refresh()

        elif menu_id in self.pluginmenuids.keys():
            self.pluginmenuids[menu_id]()

        else:
            return False

        return True