#!/usr/bin/python3


import time
from tkinter import ttk

from brownie.project.sources import is_inside_offset

from .bases import ToggleButton


class OpcodeList(ttk.Treeview):
    def __init__(self, parent, columns, **kwargs):
        self._last = set()
        self._seek_buffer = ""
        self._seek_last = 0
        self._highlighted = set()
        self._frame = ttk.Frame(parent)
        super().__init__(
            self._frame, columns=[i[0] for i in columns[1:]], selectmode="browse", **kwargs
        )
        self.pack(side="left", fill="y")
        self.heading("#0", text=columns[0][0])
        self.column("#0", width=columns[0][1])
        for tag, width in columns[1:]:
            self.heading(tag, text=tag)
            self.column(tag, width=width)

        scroll = ttk.Scrollbar(self._frame)
        scroll.pack(side="right", fill="y")

        self.configure(yscrollcommand=scroll.set)
        scroll.configure(command=self.yview)

        self.tag_configure("NoSource", background="#161616")
        self.bind("<<TreeviewSelect>>", self._select_bind)

        root = self.root = self._root()
        root.bind("j", self._highlight_jumps)
        root.bind("r", self._highlight_revert)
        self.bind("<3>", self._highlight_opcode)
        for i in range(10):
            root.bind(str(i), self._seek)

    def grid(self, *args, **kwargs):
        self._frame.grid(*args, **kwargs)

    def insert(self, values, tags=[]):
        super().insert("", "end", iid=values[0], text=values[0], values=values[1:], tags=tags)

    def delete_all(self):
        for item in self.get_children():
            self.delete(item)

    def clear_selection(self):
        self.selection_remove(self.selection())

    def selection_set(self, id_):
        self.see(id_)
        super().selection_set(id_)
        self.focus_set()
        self.focus(id_)

    def set_opcodes(self, pcMap):
        self.delete_all()
        for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)]:
            if "path" not in op or (
                op["path"] == pcMap[0]["path"] and op["offset"] == pcMap[0]["offset"]
            ):
                tag = "NoSource"
            else:
                # used to find all the opcodes with the same source offset
                tag = f"{op['path']}:{op['offset'][0]}:{op['offset'][1]}"
            self.insert([str(pc), op["op"]], [tag, op["op"], str(pc)])

    def _select_bind(self, event=None):
        for tag in self._last:
            self.tag_configure(tag, background="", foreground="")
        self._last.clear()
        try:
            pc = self.selection()[0]
        except IndexError:
            return
        pcMap = self.root.pcMap[pc]
        note = self.root.main.note

        console = self.root.main.console
        console.write(f"{pc} {pcMap['op']}")
        if "value" in pcMap:
            console.append(f" {pcMap['value']}")
        if "offset" in pcMap:
            console.append(f"\nOffsets: {pcMap['offset'][0]}, {pcMap['offset'][1]}")

        if pcMap["op"] in ("JUMP", "JUMPI"):
            prev = self._get_prev(pc)
            if self.root.pcMap[prev]["op"] in ("PUSH1", "PUSH2"):
                tag = int(self.root.pcMap[prev]["value"], 16)
                self.tag_configure(tag, foreground="#00ff00")
                self._last.add(tag)
                console.append(f"\nTarget: {tag}")

        if pcMap["op"] == "JUMPDEST":
            targets = _get_targets(self.root.pcMap, pc)
            if targets:
                console.append(f"\nJumps: {', '.join(targets)}")
            else:
                console.append(f"\nJumps: None")
            for item in targets:
                self.tag_configure(item, foreground="#00ff00")
                self._last.add(item)

        tag = self.item(pc, "tags")[0]
        if tag == "NoSource":
            note.active_frame().clear_highlight()
            return
        self.tag_configure(tag, background="#2a4864")
        self._last.add(tag)
        if "path" not in pcMap:
            note.active_frame().clear_highlight()
            return
        note.set_active(pcMap["path"])
        note.active_frame().highlight(*pcMap["offset"])

    def _seek(self, event):
        if self._seek_last < time.time() - 1:
            self._seek_buffer = ""
        self._seek_last = time.time()
        self._seek_buffer += event.char
        pc = sorted([int(i) for i in self.root.pcMap])[::-1]
        id_ = next(str(i) for i in pc if i <= int(self._seek_buffer))
        self.selection_set(id_)

    def _highlight_opcode(self, event):
        pc = self.identify_row(event.y)
        op = self.root.pcMap[pc]["op"]
        if op in self._highlighted:
            self.tag_configure(op, foreground="")
            self._highlighted.remove(op)
            return
        self.tag_configure(op, foreground="#dd3333" if op in ("REVERT", "INVALID") else "#dddd33")
        self._highlighted.add(op)

    def _highlight_jumps(self, event):
        try:
            pc = self.selection()[0]
        except IndexError:
            return
        if self.root.pcMap[pc]["op"] not in ("JUMP", "JUMPI"):
            return
        prev = self._get_prev(pc)
        if self.root.pcMap[prev]["op"] in ("PUSH1", "PUSH2"):
            tag = int(self.root.pcMap[prev]["value"], 16)
            self.see(tag)
            self._select_bind()

    def _highlight_revert(self, event):
        if "REVERT" in self._highlighted:
            self.tag_configure("REVERT", foreground="")
            self._highlighted.discard("REVERT")
        else:
            self.tag_configure("REVERT", foreground="#dd3333")
            self._highlighted.add("REVERT")

    def _get_prev(self, pc):
        pc_list = sorted(self.root.pcMap, key=lambda k: int(k), reverse=True)
        return next(i for i in pc_list if int(i) < int(pc))


class ScopingButton(ToggleButton):
    def __init__(self, parent):
        super().__init__(parent, "Scope", "s")
        self.oplist = self.root.main.oplist

    def toggle_on(self):
        try:
            op = self.oplist.selection()[0]
        except IndexError:
            return False
        if self.oplist.item(op, "tags")[0] == "NoSource":
            return False
        pc = self.root.pcMap[op]
        for key, value in sorted(self.root.pcMap.items(), key=lambda k: int(k[0])):
            if (
                "path" not in value
                or value["path"] != pc["path"]
                or not is_inside_offset(value["offset"], pc["offset"])
            ):
                self.oplist.detach(key)
            else:
                self.oplist.move(key, "", key)
        self.oplist.see(op)
        self.root.main.note.apply_scope(*pc["offset"])
        return True

    def toggle_off(self):
        self.root.main.note.clear_scope()
        for i in sorted(self.root.pcMap, key=lambda k: int(k)):
            self.oplist.move(i, "", i)
        if self.oplist.selection():
            self.oplist.see(self.oplist.selection()[0])


def _get_targets(pc_map, pc):
    targets = []
    for key, value in pc_map.items():
        if int(value.get("value", "0"), 16) != int(pc):
            continue
        next_pc = str(int(key) + int(value["op"][4:]) + 1)
        if pc_map[next_pc]["op"] in ("JUMP", "JUMPI"):
            targets.append(next_pc)
    return targets
