"""Module contains classes required to create Protobuf editor tabs."""
import traceback
import base64
import zlib
import burp
import blackboxprotobuf
from javax.swing import JSplitPane, JPanel, JButton, BoxLayout, JOptionPane
from java.awt import Component
from java.awt.event import ActionListener
from blackboxprotobuf.burp import user_funcs
from blackboxprotobuf.burp import typedef_editor

class ProtoBufEditorTabFactory(burp.IMessageEditorTabFactory):
    """Just returns instances of ProtoBufEditorTab"""

    def __init__(self, extender):
        self._extender = extender

    def createNewInstance(self, controller, editable):
        """Return new instance of editor tab for a new message"""
        return ProtoBufEditorTab(self._extender, controller, editable)

class ProtoBufEditorTab(burp.IMessageEditorTab):
    """Tab in interceptor/repeater for editing protobuf message.
    Decodes them to JSON and back.
    The message type is attached to this object.
    """

    def __init__(self, extender, controller, editable):

        self._extender = extender
        self._callbacks = extender.callbacks
        self._helpers = extender.helpers

        self._controller = controller

        self._text_editor = self._callbacks.createTextEditor()
        self._text_editor.setEditable(editable)
        self._editable = editable

        self._component = JSplitPane(JSplitPane.HORIZONTAL_SPLIT)
        self._component.setLeftComponent(self._text_editor.getComponent())
        self._component.setRightComponent(self.createButtonPane())
        self._component.setResizeWeight(0.8)

        self.message_type = None
        self._is_request = None
        self._encoder = None
        self._original_json = None
        self._content_info = None
        self._request_content_info = None
        self._request = None
        self._original_content = None

    def getTabCaption(self):
        """Return message tab caption"""
        return "Protobuf"

    def getMessage(self):
        """Transform the JSON format back to the binary protobuf message"""
        try:
            if self.message_type is None or not self.isModified():
                return self._original_content

            json_data = self._text_editor.getText().tostring()

            protobuf_data = blackboxprotobuf.protobuf_from_json(json_data, self.message_type)

            protobuf_data = self.encodePayload(protobuf_data)
            if 'set_protobuf_data' in dir(user_funcs):
                result = user_funcs.set_protobuf_data(
                    protobuf_data, self._original_content,
                    self._is_request, self._content_info,
                    self._helpers, self._request,
                    self._request_content_info)
                if result is not None:
                    return result

            headers = self._content_info.getHeaders()
            return self._helpers.buildHttpMessage(headers, str(protobuf_data))

        except Exception as exc:
            self._callbacks.printError(traceback.format_exc())
            JOptionPane.showMessageDialog(self._component, "Error encoding protobuf: " + str(exc))
            # Resets state
            return self._original_content

    def setMessage(self, content, is_request, retry=True):
        """Get the data from the request/response and parse into JSON.
           sets self.message_type
        """
        # Save original content
        self._original_content = content
        if is_request:
            self._content_info = self._helpers.analyzeRequest(self._controller.getHttpService(),
                                                              content)
        else:
            self._content_info = self._helpers.analyzeResponse(content)
        self._is_request = is_request
        self._request = None
        self._request_content_info = None

        if not is_request:
            self._request = self._controller.getRequest()
            self._request_content_info = self._helpers.analyzeRequest(
                self._controller.getHttpService(), self._request)

        message_hash = self.getMessageHash()

        # Try to find saved messsage type
        self.message_type = None
        if message_hash in self._extender.known_types:
            self.message_type = self._extender.known_types[message_hash]

        try:
            protobuf_data = None
            if 'get_protobuf_data' in dir(user_funcs):
                protobuf_data = user_funcs.get_protobuf_data(
                    content, is_request, self._content_info, self._helpers,
                    self._request, self._request_content_info)
            if protobuf_data is None:
                protobuf_data = content[self._content_info.getBodyOffset():].tostring()

            protobuf_data = self.decodePayload(protobuf_data)
            json_data, self.message_type = blackboxprotobuf.protobuf_to_json(
                protobuf_data, self.message_type)

            # Save the message type
            self._extender.known_types[message_hash] = self.message_type

            self._original_json = json_data
            self._text_editor.setText(json_data)
            success = True
        except Exception as exc:
            success = False
            self._callbacks.printError(traceback.format_exc())

        # Bring out of exception handler to avoid nexting handlers
        if not success:
            if retry:
                # Clear existing type info and retry
                prev_type = self.message_type
                self.message_type = None
                if message_hash in self._extender.known_types:
                    del self._extender.known_types[message_hash]
                try:
                    self.setMessage(content, is_request, False)
                except Exception as exc:
                    # If it still won't parse, restore the types
                    self.message_type = prev_type
                    self._extender.known_types[message_hash] = prev_type
            else:
                self._text_editor.setText("Error parsing protobuf")

    def decodePayload(self, payload):
        """Add support for decoding a few default methods. Including Base64 and GZIP"""
        if payload.startswith(bytearray([0x1f, 0x8b, 0x08])):
            gzip_decompress = zlib.decompressobj(-zlib.MAX_WBITS)
            self._encoder = 'gzip'
            return gzip_decompress.decompress(payload)

        # Try to base64 decode
        try:
            protobuf = base64.b64decode(payload, validate=True)
            self._encoder = 'base64'
            return protobuf
        except Exception as exc:
            pass

        #try:
        #    protobuf = base64.urlsafe_b64decode(payload)
        #    self._encoder = 'base64_url'
        #    return protobuf
        #except Exception as exc:
        #    pass

        self._encoder = None
        return payload

    def encodePayload(self, payload):
        """If we detected an encoding like gzip or base64 when decoding, redo
           that encoding step here
        """
        if self._encoder == 'base64':
            return base64.b64encode(payload)
        elif self._encoder == 'base64_url':
            return  base64.urlsafe_b64encode(payload)
        elif self._encoder == 'gzip':
            gzip_compress = zlib.compressobj(-1, zlib.DEFLATED, -zlib.MAX_WBITS)
            self._encoder = 'gzip'
            return gzip_compress.compress(payload)
        else:
            return payload

    def getSelectedData(self):
        """Get text currently selected in message"""
        return self._text_editor.getSelectedText()

    def getUiComponent(self):
        """Return Java AWT component for this tab"""
        return self._component

    def isEnabled(self, content, is_request):
        """Try to detect a protobuf in the message to enable the tab. Defaults
           to content-type header of 'x-protobuf'. User overridable
        """
        # TODO implement some more default checks
        if is_request:
            info = self._helpers.analyzeRequest(content)
        else:
            info = self._helpers.analyzeResponse(content)

        if 'detect_protobuf' in dir(user_funcs):
            result = user_funcs.detect_protobuf(content, is_request, info, self._helpers)
            if result is not None:
                return result

        # Bail early if there is no body
        if info.getBodyOffset() == len(content):
            return False

        protobuf_content_types = ['x-protobuf', 'application/protobuf']
        # Check all headers for x-protobuf
        for header in info.getHeaders():
            if 'content-type' in header.lower():
                for protobuf_content_type in protobuf_content_types:
                    if protobuf_content_type in header.lower():
                        return True

        return False

    def isModified(self):
        """Return if the message was modified"""
        return self._text_editor.isTextModified()

    def createButtonPane(self):
        """Create a new button pane for the message editor tab"""
        self._button_listener = EditorButtonListener(self)

        panel = JPanel()
        panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS))

        if self._editable:
            panel.add(self.createButton("Validate", "validate"))
        panel.add(self.createButton("Save Type", "save-type"))
        panel.add(self.createButton("Load Type", "load-type"))
        panel.add(self.createButton("Edit Type", "edit-type"))
        panel.add(self.createButton("Reset", "reset"))
        return panel

    def createButton(self, text, command):
        """Create a new button with the given text and command"""
        button = JButton(text)
        button.setAlignmentX(Component.CENTER_ALIGNMENT)
        button.setActionCommand(command)
        button.addActionListener(self._button_listener)
        return button

    def validateMessage(self):
        """Callback for validate button. Attempts to encode the message with
           the current type definition
        """
        try:
            json_data = self._text_editor.getText().tostring()
            blackboxprotobuf.protobuf_from_json(json_data, self.message_type)
            # If it works, save the message
            self._original_json = json_data
        except Exception as exc:
            JOptionPane.showMessageDialog(self._component, str(exc))
            self._callbacks.printError(traceback.format_exc())

    def resetMessage(self):
        """Drop any changes and reset the message. Callback for "reset"
           button
        """
        self._text_editor.setText(self._original_json)

    def getMessageHash(self):
        """Compute an "identifier" for the message which is used for sticky
           type definitions. User modifiable
        """
        message_hash = None
        if 'hash_message' in dir(user_funcs):
            message_hash = user_funcs.hash_message(
                self._original_content, self._is_request, self._content_info,
                self._helpers, self._request, self._request_content_info)
        if message_hash is None:
            # Base it off just the URL and request/response

            content_info = self._content_info if self._is_request else self._request_content_info
            url = content_info.getUrl().getPath()
            message_hash = (url, self._is_request)
        return message_hash

    def saveTypeMenu(self):
        """Open an input dialog to save the current type definiton"""
        type_defs = blackboxprotobuf.known_messages.keys()
        type_defs.insert(0, "New...")
        selection = JOptionPane.showInputDialog(
            self._component, "Select name for type", "Type selection",
            JOptionPane.PLAIN_MESSAGE, None, type_defs, None)
        if selection is None:
            return
        elif selection == "New...":
            selection = JOptionPane.showInputDialog("Enter new name")
        blackboxprotobuf.known_messages[selection] = self.message_type
        self._extender.suite_tab.updateList()

    def loadTypeMenu(self):
        """Open an input menu dialog to load a type definition"""
        type_defs = blackboxprotobuf.known_messages.keys()
        selection = JOptionPane.showInputDialog(
            self._component, "Select type", "Type selection",
            JOptionPane.PLAIN_MESSAGE, None, type_defs, None)

        if selection is None:
            return

        message_type = blackboxprotobuf.known_messages[selection]

        try:
            self.applyType(message_type)
        except Exception as exc:
            self._callbacks.printError(traceback.format_exc())
            JOptionPane.showMessageDialog(self._component, "Error applying type: " + str(exc))

    def applyType(self, typedef):
        """Apply a new typedef to the message. Throws an exception if type is invalid."""
        # Convert to protobuf as old type and re-interpret as new type
        old_message_type = self.message_type
        json_data = self._text_editor.getText().tostring()
        protobuf_data = blackboxprotobuf.protobuf_from_json(json_data, old_message_type)
        new_json, message_type = blackboxprotobuf.protobuf_to_json(str(protobuf_data), typedef)

        # Should exception out before now if there is an issue
        # Set the message type and reparse with the new type
        self.message_type = message_type
        self._text_editor.setText(str(new_json))

        message_hash = self.getMessageHash()
        self._extender.known_types[message_hash] = message_type

class EditorButtonListener(ActionListener):
    """Callback listener for buttons in the message editor tab"""
    def __init__(self, editor_tab):
        self._editor_tab = editor_tab

    def actionPerformed(self, event):
        """Called when when a button in the message editor is pressed"""
        if event.getActionCommand() == "validate":
            self._editor_tab.validateMessage()
        elif event.getActionCommand() == "reset":
            self._editor_tab.resetMessage()
        elif event.getActionCommand() == "edit-type":
            typedef_editor.TypeEditorWindow(
                self._editor_tab._callbacks,
                self._editor_tab.message_type,
                self._editor_tab.applyType).show()
        elif event.getActionCommand() == "save-type":
            self._editor_tab.saveTypeMenu()
        elif event.getActionCommand() == "load-type":
            self._editor_tab.loadTypeMenu()