#!/usr/bin/env python

# Copyright JS Foundation and other contributors, http://js.foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function
from cmd import Cmd
from pprint import pprint  # For the readable stack printing.
import argparse
import logging
import re
import select
import socket
import struct
import sys

# Messages sent by the server to client.
JERRY_DEBUGGER_CONFIGURATION = 1
JERRY_DEBUGGER_PARSE_ERROR = 2
JERRY_DEBUGGER_BYTE_CODE_CP = 3
JERRY_DEBUGGER_PARSE_FUNCTION = 4
JERRY_DEBUGGER_BREAKPOINT_LIST = 5
JERRY_DEBUGGER_BREAKPOINT_OFFSET_LIST = 6
JERRY_DEBUGGER_SOURCE_CODE = 7
JERRY_DEBUGGER_SOURCE_CODE_END = 8
JERRY_DEBUGGER_SOURCE_CODE_NAME = 9
JERRY_DEBUGGER_SOURCE_CODE_NAME_END = 10
JERRY_DEBUGGER_FUNCTION_NAME = 11
JERRY_DEBUGGER_FUNCTION_NAME_END = 12
JERRY_DEBUGGER_RELEASE_BYTE_CODE_CP = 13
JERRY_DEBUGGER_MEMSTATS_RECEIVE = 14
JERRY_DEBUGGER_BREAKPOINT_HIT = 15
JERRY_DEBUGGER_EXCEPTION_HIT = 16
JERRY_DEBUGGER_EXCEPTION_STR = 17
JERRY_DEBUGGER_EXCEPTION_STR_END = 18
JERRY_DEBUGGER_BACKTRACE = 19
JERRY_DEBUGGER_BACKTRACE_END = 20
JERRY_DEBUGGER_EVAL_RESULT = 21
JERRY_DEBUGGER_EVAL_RESULT_END = 22
JERRY_DEBUGGER_EVAL_ERROR = 23
JERRY_DEBUGGER_EVAL_ERROR_END = 24


# Messages sent by the client to server.
JERRY_DEBUGGER_FREE_BYTE_CODE_CP = 1
JERRY_DEBUGGER_UPDATE_BREAKPOINT = 2
JERRY_DEBUGGER_EXCEPTION_CONFIG = 3
JERRY_DEBUGGER_MEMSTATS = 4
JERRY_DEBUGGER_STOP = 5
JERRY_DEBUGGER_CONTINUE = 6
JERRY_DEBUGGER_STEP = 7
JERRY_DEBUGGER_NEXT = 8
JERRY_DEBUGGER_GET_BACKTRACE = 9
JERRY_DEBUGGER_EVAL = 10
JERRY_DEBUGGER_EVAL_PART = 11

MAX_BUFFER_SIZE = 128
WEBSOCKET_BINARY_FRAME = 2
WEBSOCKET_FIN_BIT = 0x80


def arguments_parse():
    parser = argparse.ArgumentParser(description="JerryScript debugger client")

    parser.add_argument("address", action="store", nargs="?", default="localhost:5001",
                        help="specify a unique network address for connection (default: %(default)s)")
    parser.add_argument("-v", "--verbose", action="store_true", default=False,
                        help="increase verbosity (default: %(default)s)")
    parser.add_argument("--non-interactive", action="store_true", default=False,
                        help="disable stop when newline is pressed (default: %(default)s)")
    parser.add_argument("--color", action="store_true", default=False,
                        help="enable color highlighting on source commands (default: %(default)s)")
    parser.add_argument("--display", action="store", default=None, type=int,
                        help="set display range")
    parser.add_argument("--exception", action="store", default=None, type=int, choices=[0, 1],
                        help="set exception config, usage 1: [Enable] or 0: [Disable]")

    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
        logging.debug("Debug logging mode: ON")

    return args


class JerryBreakpoint(object):

    def __init__(self, line, offset, function):
        self.line = line
        self.offset = offset
        self.function = function
        self.active_index = -1

    def __str__(self):
        result = self.function.source_name or "<unknown>"
        result += ":%d" % (self.line)

        if self.function.is_func:
            result += " (in "
            result += self.function.name or "function"
            result += "() at line:%d, col:%d)" % (self.function.line, self.function.column)
        return result

    def __repr__(self):
        return ("Breakpoint(line:%d, offset:%d, active_index:%d)"
                % (self.line, self.offset, self.active_index))


class JerryFunction(object):

    def __init__(self, is_func, byte_code_cp, source, source_name, line, column, name, lines, offsets):
        self.is_func = is_func
        self.byte_code_cp = byte_code_cp
        self.source = re.split("\r\n|[\r\n]", source)
        self.source_name = source_name
        self.name = name
        self.lines = {}
        self.offsets = {}
        self.line = line
        self.column = column
        self.first_breakpoint_line = lines[0]
        self.first_breakpoint_offset = offsets[0]

        for i in range(len(lines)):
            line = lines[i]
            offset = offsets[i]
            breakpoint = JerryBreakpoint(line, offset, self)
            self.lines[line] = breakpoint
            self.offsets[offset] = breakpoint

    def __repr__(self):
        result = ("Function(byte_code_cp:0x%x, source_name:%r, name:%r, line:%d, column:%d { "
                  % (self.byte_code_cp, self.source_name, self.name, self.line, self.column))

        result += ','.join([str(breakpoint) for breakpoint in self.lines.values()])

        return result + " })"


class DebuggerPrompt(Cmd):

    def __init__(self, debugger):
        Cmd.__init__(self)
        self.debugger = debugger
        self.stop = False
        self.quit = False
        self.cont = True
        self.non_interactive = False

    def precmd(self, line):
        self.stop = False
        self.cont = False
        if self.non_interactive:
            print("%s" % line)
        return line

    def postcmd(self, stop, line):
        return self.stop

    def disable_args(self, args):
        if args:
            print("Error: No argument expected")
            return True
        return False

    def do_quit(self, args):
        """ Exit JerryScript debugger """
        self.do_delete("all")
        self.do_exception("0")  # disable the exception handler
        self.exec_command(args, JERRY_DEBUGGER_CONTINUE)
        self.stop = True
        self.quit = True

    def do_break(self, args):
        """ Insert breakpoints on the given lines or functions """
        if args == "":
            print("Error: Breakpoint index expected")
        else:
            set_breakpoint(self.debugger, False, args)

    do_b = do_break

    def do_fbreak(self, args):
        """ Insert breakpoints on the given lines or functions, if not found add the pending list """
        if args == "":
            print("Error: Breakpoint index expected")
        else:
            set_breakpoint(self.debugger, True, args)

    def exec_command(self, args, command_id):
        self.stop = True
        if args != "":
            print("Error: No argument expected")
        else:
            self.debugger.send_command(command_id)

    def do_continue(self, args):
        """ Continue execution """
        self.exec_command(args, JERRY_DEBUGGER_CONTINUE)
        self.stop = True
        self.cont = True
        if not self.non_interactive:
            print("Press enter to stop JavaScript execution.")

    do_c = do_continue

    def do_step(self, args):
        """ Next breakpoint, step into functions """
        self.exec_command(args, JERRY_DEBUGGER_STEP)
        self.cont = True

    do_s = do_step

    def do_next(self, args):
        """ Next breakpoint in the same context """
        self.exec_command(args, JERRY_DEBUGGER_NEXT)
        self.cont = True

    do_n = do_next

    def do_list(self, args):
        """ Lists the available breakpoints, use 'pending' to list all the available pending breakpoints """
        if args == "pending":
            for index, breakpoint in enumerate(self.debugger.pending_breakpoint_list):
                print("%d: %s" % (index, breakpoint))
            return

        for breakpoint in self.debugger.active_breakpoint_list.values():
            print("%d: %s" % (breakpoint.active_index, breakpoint))

    def do_delete(self, args):
        """ Delete the given breakpoint, use 'delete all' to clear the breakpoints in the whole program"""
        if not args:
            print("Error: Breakpoint index expected")
            return
        elif args == "all":
            for i in self.debugger.active_breakpoint_list.values():
                breakpoint = self.debugger.active_breakpoint_list[i.active_index]
                del self.debugger.active_breakpoint_list[i.active_index]
                breakpoint.active_index = -1
                self.debugger.send_breakpoint(breakpoint)
        else:
            try:
                breakpoint_index = int(args)
            except ValueError as val_errno:
                print("Error: Integer number expected, %s" % (val_errno))
                return

            if breakpoint_index in self.debugger.active_breakpoint_list:
                breakpoint = self.debugger.active_breakpoint_list[breakpoint_index]
                del self.debugger.active_breakpoint_list[breakpoint_index]
                breakpoint.active_index = -1
                self.debugger.send_breakpoint(breakpoint)
            else:
                print("Error: Breakpoint %d not found" % (breakpoint_index))

    def do_pendingdel(self, args):
        """ Delete the given pending breakpoints from the list """
        if not args:
            print("Error: Pending breakpoint index expected")
            return

        else:
            try:
                breakpoint_index = int(args)
            except ValueError as val_errno:
                print("Error: Integer number expectes, %s" % (val_errno))
                return

            if breakpoint_index <= len(self.debugger.pending_breakpoint_list):
                try:
                    del self.debugger.pending_breakpoint_list[breakpoint_index]
                except IndexError as index_errno:
                    print("Error: Index not found, %s" % (index_errno))
                    return
            else:
                print("Error: Pending breakpoint %d not found" % (breakpoint_index))

    def exec_backtrace(self, args):
        max_depth = 0

        if args:
            try:
                max_depth = int(args)
                if max_depth <= 0:
                    print("Error: Positive integer number expected")
                    return
            except ValueError as val_errno:
                print("Error: Positive integer number expected, %s" % (val_errno))
                return

        message = struct.pack(self.debugger.byte_order + "BBIB" + self.debugger.idx_format,
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + 1 + 4,
                              0,
                              JERRY_DEBUGGER_GET_BACKTRACE,
                              max_depth)
        self.debugger.send_message(message)
        self.stop = True

    def do_backtrace(self, args):
        """ Get backtrace data from debugger """
        self.exec_backtrace(args)

    do_bt = do_backtrace

    def do_src(self, args):
        """ Get current source code """
        if args:
            line_num = src_check_args(args)
            if line_num >= 0:
                print_source(self.debugger, line_num)
            elif line_num == 0:
                print_source(self.debugger, self.debugger.default_viewrange)
            else:
                return

    do_source = do_src

    def do_display(self, args):
        """ Toggle source code display after breakpoints """
        if args:
            line_num = src_check_args(args)
            if line_num >= 0:
                self.debugger.display = line_num
            else:
                return

        else:
            print("Non-negative integer number expected, 0 turns off this function")
            return

    def do_dump(self, args):
        """ Dump all of the debugger data """
        if self.disable_args(args):
            return

        pprint(self.debugger.function_list)

    def eval_string(self, args):
        size = len(args)
        if size == 0:
            return

        # 1: length of type byte
        # 4: length of an uint32 value
        message_header = 1 + 4
        max_fragment = min(self.debugger.max_message_size - message_header, size)

        message = struct.pack(self.debugger.byte_order + "BBIBI",
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + max_fragment + message_header,
                              0,
                              JERRY_DEBUGGER_EVAL,
                              size)

        if size == max_fragment:
            self.debugger.send_message(message + args)
            self.stop = True
            return

        self.debugger.send_message(message + args[0:max_fragment])
        offset = max_fragment

        # 1: length of type byte
        message_header = 1

        max_fragment = self.debugger.max_message_size - message_header
        while offset < size:
            next_fragment = min(max_fragment, size - offset)

            message = struct.pack(self.debugger.byte_order + "BBIB",
                                  WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                                  WEBSOCKET_FIN_BIT + next_fragment + message_header,
                                  0,
                                  JERRY_DEBUGGER_EVAL_PART)

            prev_offset = offset
            offset += next_fragment
            self.debugger.send_message(message + args[prev_offset:offset])

        self.stop = True

    def do_eval(self, args):
        """ Evaluate JavaScript source code """
        self.eval_string(args)

    do_e = do_eval

    def do_exception(self, args):
        """ Config the exception handler module """
        if not args:
            print("Error: Status expected!")
            return

        enable = int(args)

        if enable == 1:
            logging.debug("Stop at exception enabled")
        elif enable == 0:
            logging.debug("Stop at exception disabled")
        else:
            print("Error: Invalid input! Usage 1: [Enable] or 0: [Disable].")
            return

        self.debugger.send_exception_config(enable)

    def do_memstats(self, args):
        """ Memory statistics """
        self.exec_command(args, JERRY_DEBUGGER_MEMSTATS)
        return

    do_ms = do_memstats

class Multimap(object):

    def __init__(self):
        self.map = {}

    def get(self, key):
        if key in self.map:
            return self.map[key]
        return []

    def insert(self, key, value):
        if key in self.map:
            self.map[key].append(value)
        else:
            self.map[key] = [value]

    def delete(self, key, value):
        items = self.map[key]

        if len(items) == 1:
            del self.map[key]
        else:
            del items[items.index(value)]

    def __repr__(self):
        return "Multimap(%r)" % (self.map)


class JerryDebugger(object):

    def __init__(self, address):

        if ":" not in address:
            self.host = address
            self.port = 5001  # use default port
        else:
            self.host, self.port = address.split(":")
            self.port = int(self.port)

        print("Connecting to: %s:%s" % (self.host, self.port))

        self.message_data = b""
        self.function_list = {}
        self.last_breakpoint_hit = None
        self.next_breakpoint_index = 0
        self.active_breakpoint_list = {}
        self.pending_breakpoint_list = []
        self.line_list = Multimap()
        self.display = 0
        self.default_viewrange = 3
        self.green = ''
        self.red = ''
        self.nocolor = ''
        self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client_socket.connect((self.host, self.port))

        self.send_message(b"GET /jerry-debugger HTTP/1.1\r\n" +
                          b"Upgrade: websocket\r\n" +
                          b"Connection: Upgrade\r\n" +
                          b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n")
        result = b""
        expected = (b"HTTP/1.1 101 Switching Protocols\r\n" +
                    b"Upgrade: websocket\r\n" +
                    b"Connection: Upgrade\r\n" +
                    b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n")

        len_expected = len(expected)

        while len(result) < len_expected:
            result += self.client_socket.recv(1024)

        len_result = len(result)

        if result[0:len_expected] != expected:
            raise Exception("Unexpected handshake")

        if len_result > len_expected:
            result = result[len_expected:]
        else:
            result = b""

        len_expected = 6
        # Network configurations, which has the following struct:
        # header [2] - opcode[1], size[1]
        # type [1]
        # max_message_size [1]
        # cpointer_size [1]
        # little_endian [1]

        while len(result) < len_expected:
            result += self.client_socket.recv(1024)

        len_result = len(result)

        expected = struct.pack("BBB",
                               WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                               4,
                               JERRY_DEBUGGER_CONFIGURATION)

        if result[0:3] != expected:
            raise Exception("Unexpected configuration")

        self.max_message_size = ord(result[3])
        self.cp_size = ord(result[4])
        self.little_endian = ord(result[5])

        if self.little_endian:
            self.byte_order = "<"
            logging.debug("Little-endian machine")
        else:
            self.byte_order = ">"
            logging.debug("Big-endian machine")

        if self.cp_size == 2:
            self.cp_format = "H"
        else:
            self.cp_format = "I"

        self.idx_format = "I"

        logging.debug("Compressed pointer size: %d", self.cp_size)

        if len_result > len_expected:
            self.message_data = result[len_expected:]

    def __del__(self):
        self.client_socket.close()

    def send_message(self, message):
        size = len(message)
        while size > 0:
            bytes_send = self.client_socket.send(message)
            if bytes_send < size:
                message = message[bytes_send:]
            size -= bytes_send

    def send_breakpoint(self, breakpoint):
        message = struct.pack(self.byte_order + "BBIBB" + self.cp_format + self.idx_format,
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + 1 + 1 + self.cp_size + 4,
                              0,
                              JERRY_DEBUGGER_UPDATE_BREAKPOINT,
                              int(breakpoint.active_index >= 0),
                              breakpoint.function.byte_code_cp,
                              breakpoint.offset)
        self.send_message(message)

    def send_bytecode_cp(self, byte_code_cp):
        message = struct.pack(self.byte_order + "BBIB" + self.cp_format,
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + 1 + self.cp_size,
                              0,
                              JERRY_DEBUGGER_FREE_BYTE_CODE_CP,
                              byte_code_cp)
        self.send_message(message)

    def send_command(self, command):
        message = struct.pack(self.byte_order + "BBIB",
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + 1,
                              0,
                              command)
        self.send_message(message)

    def send_exception_config(self, enable):
        message = struct.pack(self.byte_order + "BBIBB",
                              WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT,
                              WEBSOCKET_FIN_BIT + 1 + 1,
                              0,
                              JERRY_DEBUGGER_EXCEPTION_CONFIG,
                              enable)
        self.send_message(message)

    def set_colors(self):
        self.green = '\033[92m'
        self.nocolor = '\033[0m'
        self.red = '\033[31m'

    def get_message(self, blocking):
        # Connection was closed
        if self.message_data is None:
            return None

        while True:
            if len(self.message_data) >= 2:
                if ord(self.message_data[0]) != WEBSOCKET_BINARY_FRAME | WEBSOCKET_FIN_BIT:
                    raise Exception("Unexpected data frame")

                size = ord(self.message_data[1])
                if size == 0 or size >= 126:
                    raise Exception("Unexpected data frame")

                if len(self.message_data) >= size + 2:
                    result = self.message_data[0:size + 2]
                    self.message_data = self.message_data[size + 2:]
                    return result

            if not blocking:
                select_result = select.select([self.client_socket], [], [], 0)[0]
                if self.client_socket not in select_result:
                    return b''

            data = self.client_socket.recv(MAX_BUFFER_SIZE)

            if not data:
                self.message_data = None
                return None

            self.message_data += data


def parse_source(debugger, data):
    source_code = ""
    source_code_name = ""
    function_name = ""
    stack = [{"line": 1,
              "column": 1,
              "name": "",
              "lines": [],
              "offsets": []}]
    new_function_list = {}

    while True:
        if data is None:
            return

        buffer_type = ord(data[2])
        buffer_size = ord(data[1]) - 1

        logging.debug("Parser buffer type: %d, message size: %d", buffer_type, buffer_size)

        if buffer_type == JERRY_DEBUGGER_PARSE_ERROR:
            logging.error("Parser error!")
            return

        elif buffer_type in [JERRY_DEBUGGER_SOURCE_CODE, JERRY_DEBUGGER_SOURCE_CODE_END]:
            source_code += data[3:]

        elif buffer_type in [JERRY_DEBUGGER_SOURCE_CODE_NAME, JERRY_DEBUGGER_SOURCE_CODE_NAME_END]:
            source_code_name += data[3:]

        elif buffer_type in [JERRY_DEBUGGER_FUNCTION_NAME, JERRY_DEBUGGER_FUNCTION_NAME_END]:
            function_name += data[3:]

        elif buffer_type == JERRY_DEBUGGER_PARSE_FUNCTION:
            logging.debug("Source name: %s, function name: %s", source_code_name, function_name)

            position = struct.unpack(debugger.byte_order + debugger.idx_format + debugger.idx_format,
                                     data[3: 3 + 4 + 4])

            stack.append({"source": source_code,
                          "source_name": source_code_name,
                          "line": position[0],
                          "column": position[1],
                          "name": function_name,
                          "lines": [],
                          "offsets": []})
            function_name = ""

        elif buffer_type in [JERRY_DEBUGGER_BREAKPOINT_LIST, JERRY_DEBUGGER_BREAKPOINT_OFFSET_LIST]:
            name = "lines"
            if buffer_type == JERRY_DEBUGGER_BREAKPOINT_OFFSET_LIST:
                name = "offsets"

            logging.debug("Breakpoint %s received", name)

            buffer_pos = 3
            while buffer_size > 0:
                line = struct.unpack(debugger.byte_order + debugger.idx_format,
                                     data[buffer_pos: buffer_pos + 4])
                stack[-1][name].append(line[0])
                buffer_pos += 4
                buffer_size -= 4

        elif buffer_type == JERRY_DEBUGGER_BYTE_CODE_CP:
            byte_code_cp = struct.unpack(debugger.byte_order + debugger.cp_format,
                                         data[3: 3 + debugger.cp_size])[0]

            logging.debug("Byte code cptr received: {0x%x}", byte_code_cp)

            func_desc = stack.pop()

            # We know the last item in the list is the general byte code.
            if len(stack) == 0:
                func_desc["source"] = source_code
                func_desc["source_name"] = source_code_name

            function = JerryFunction(len(stack) != 0,
                                     byte_code_cp,
                                     func_desc["source"],
                                     func_desc["source_name"],
                                     func_desc["line"],
                                     func_desc["column"],
                                     func_desc["name"],
                                     func_desc["lines"],
                                     func_desc["offsets"])

            new_function_list[byte_code_cp] = function

            if len(stack) == 0:
                logging.debug("Parse completed.")
                break

        elif buffer_type == JERRY_DEBUGGER_RELEASE_BYTE_CODE_CP:
            # Redefined functions are dropped during parsing.
            byte_code_cp = struct.unpack(debugger.byte_order + debugger.cp_format,
                                         data[3: 3 + debugger.cp_size])[0]

            if byte_code_cp in new_function_list:
                del new_function_list[byte_code_cp]
                debugger.send_bytecode_cp(byte_code_cp)
            else:
                release_function(debugger, data)

        else:
            logging.error("Parser error!")
            return

        data = debugger.get_message(True)

    # Copy the ready list to the global storage.
    debugger.function_list.update(new_function_list)

    for function in new_function_list.values():
        for line, breakpoint in function.lines.items():
            debugger.line_list.insert(line, breakpoint)

    # Try to set the pending breakpoints
    if len(debugger.pending_breakpoint_list) != 0:
        logging.debug("Pending breakpoints list: %s", debugger.pending_breakpoint_list)

        for breakpoint in debugger.pending_breakpoint_list:
            if isinstance(breakpoint, int):
                breakpoint = source_code_name + ":" + str(breakpoint)
            set_breakpoint(debugger, False, breakpoint)
    else:
        logging.debug("No pending breakpoints")


def src_check_args(args):
    try:
        line_num = int(args)
        if line_num < 0:
            print("Error: Non-negative integer number expected")
            return -1

        return line_num
    except ValueError as val_errno:
        print("Error: Non-negative integer number expected: %s" % (val_errno))
        return -1


def print_source(debugger, line_num):
    last_bp = debugger.last_breakpoint_hit
    if not last_bp:
        return

    lines = last_bp.function.source
    if last_bp.function.source_name:
        print("Source: %s" % (last_bp.function.source_name))

    if line_num == 0:
        start = 0
        end = len(last_bp.function.source) - 1
    else:
        start = max(last_bp.line - line_num, 0)
        end = min(last_bp.line + line_num-1, len(last_bp.function.source)-1)

    for i in range(start, end):
        if i == last_bp.line - 1:
            print("%s%4d%s %s>%s %s" % (debugger.green, i + 1, debugger.nocolor, debugger.red, debugger.nocolor, lines[i]))
        else:
            print("%s%4d%s   %s" % (debugger.green, i + 1, debugger.nocolor, lines[i]))


def release_function(debugger, data):
    byte_code_cp = struct.unpack(debugger.byte_order + debugger.cp_format,
                                 data[3: 3 + debugger.cp_size])[0]

    function = debugger.function_list[byte_code_cp]

    for line, breakpoint in function.lines.items():
        debugger.line_list.delete(line, breakpoint)
        if breakpoint.active_index >= 0:
            del debugger.active_breakpoint_list[breakpoint.active_index]

    del debugger.function_list[byte_code_cp]

    debugger.send_bytecode_cp(byte_code_cp)

    logging.debug("Function {0x%x} byte-code released", byte_code_cp)


def enable_breakpoint(debugger, breakpoint):
    if breakpoint.active_index < 0:
        debugger.next_breakpoint_index += 1

        debugger.active_breakpoint_list[debugger.next_breakpoint_index] = breakpoint
        breakpoint.active_index = debugger.next_breakpoint_index
        debugger.send_breakpoint(breakpoint)

    print ("Breakpoint %d at %s" % (breakpoint.active_index, breakpoint))


def set_breakpoint(debugger, pending, string):
    line = re.match("(.*):(\\d+)$", string)
    found = False

    if line:
        source_name = line.group(1)
        line = int(line.group(2))

        for breakpoint in debugger.line_list.get(line):
            func_source = breakpoint.function.source_name
            if (source_name == func_source or
                    func_source.endswith("/" + source_name) or
                    func_source.endswith("\\" + source_name)):

                enable_breakpoint(debugger, breakpoint)
                found = True

    else:
        for function in debugger.function_list.values():
            if function.name == string:
                enable_breakpoint(debugger, function.lines[function.first_breakpoint_line])
                found = True

    if not found:
        print("Breakpoint not found")
        if pending:
            if line:
                debugger.pending_breakpoint_list.append(line)
            else:
                debugger.pending_breakpoint_list.append(string)
            print ("Pending breakpoint added")
    return


def get_breakpoint(debugger, breakpoint_data):
    function = debugger.function_list[breakpoint_data[0]]
    offset = breakpoint_data[1]

    if offset in function.offsets:
        return (function.offsets[offset], True)

    if offset < function.first_breakpoint_offset:
        return (function.offsets[function.first_breakpoint_offset], False)

    nearest_offset = -1

    for current_offset in function.offsets:
        if current_offset <= offset and current_offset > nearest_offset:
            nearest_offset = current_offset

    return (function.offsets[nearest_offset], False)


def main():
    args = arguments_parse()

    debugger = JerryDebugger(args.address)
    exception_string = ""

    if args.color:
        debugger.set_colors()

    non_interactive = args.non_interactive

    logging.debug("Connected to JerryScript on %d port", debugger.port)

    prompt = DebuggerPrompt(debugger)
    prompt.prompt = "(jerry-debugger) "
    prompt.non_interactive = non_interactive

    if args.display:
        prompt.do_display(args.display)

    if args.exception is not None:
        prompt.do_exception(str(args.exception))

    while True:
        if not non_interactive and prompt.cont:
            if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
                sys.stdin.readline()
                prompt.cont = False
                debugger.send_command(JERRY_DEBUGGER_STOP)

        data = debugger.get_message(False)

        if data == b'':
            continue

        if not data:  # Break the while loop if there is no more data.
            break

        buffer_type = ord(data[2])
        buffer_size = ord(data[1]) - 1

        logging.debug("Main buffer type: %d, message size: %d", buffer_type, buffer_size)

        if buffer_type in [JERRY_DEBUGGER_PARSE_ERROR,
                           JERRY_DEBUGGER_BYTE_CODE_CP,
                           JERRY_DEBUGGER_PARSE_FUNCTION,
                           JERRY_DEBUGGER_BREAKPOINT_LIST,
                           JERRY_DEBUGGER_SOURCE_CODE,
                           JERRY_DEBUGGER_SOURCE_CODE_END,
                           JERRY_DEBUGGER_SOURCE_CODE_NAME,
                           JERRY_DEBUGGER_SOURCE_CODE_NAME_END,
                           JERRY_DEBUGGER_FUNCTION_NAME,
                           JERRY_DEBUGGER_FUNCTION_NAME_END]:
            parse_source(debugger, data)

        elif buffer_type == JERRY_DEBUGGER_RELEASE_BYTE_CODE_CP:
            release_function(debugger, data)

        elif buffer_type in [JERRY_DEBUGGER_BREAKPOINT_HIT, JERRY_DEBUGGER_EXCEPTION_HIT]:
            breakpoint_data = struct.unpack(debugger.byte_order + debugger.cp_format + debugger.idx_format, data[3:])

            breakpoint = get_breakpoint(debugger, breakpoint_data)
            debugger.last_breakpoint_hit = breakpoint[0]

            if buffer_type == JERRY_DEBUGGER_EXCEPTION_HIT:
                print("Exception throw detected (to disable automatic stop type exception 0)")
                if exception_string:
                    print("Exception hint: %s" % (exception_string))
                    exception_string = ""

            if breakpoint[1]:
                breakpoint_info = "at"
            else:
                breakpoint_info = "around"

            if breakpoint[0].active_index >= 0:
                breakpoint_info += " breakpoint:%s%d%s" % (debugger.red, breakpoint[0].active_index, debugger.nocolor)

            print("Stopped %s %s" % (breakpoint_info, breakpoint[0]))
            if debugger.display:
                print_source(prompt.debugger, debugger.display)

            prompt.cmdloop()
            if prompt.quit:
                break

        elif buffer_type == JERRY_DEBUGGER_EXCEPTION_STR:
            exception_string += data[3:]

        elif buffer_type == JERRY_DEBUGGER_EXCEPTION_STR_END:
            exception_string += data[3:]

        elif buffer_type in [JERRY_DEBUGGER_BACKTRACE, JERRY_DEBUGGER_BACKTRACE_END]:
            frame_index = 0

            while True:

                buffer_pos = 3
                while buffer_size > 0:
                    breakpoint_data = struct.unpack(debugger.byte_order + debugger.cp_format + debugger.idx_format,
                                                    data[buffer_pos: buffer_pos + debugger.cp_size + 4])

                    breakpoint = get_breakpoint(debugger, breakpoint_data)

                    print("Frame %d: %s" % (frame_index, breakpoint[0]))

                    frame_index += 1
                    buffer_pos += 6
                    buffer_size -= 6

                if buffer_type == JERRY_DEBUGGER_BACKTRACE_END:
                    break

                data = debugger.get_message(True)
                buffer_type = ord(data[2])
                buffer_size = ord(data[1]) - 1

                if buffer_type not in [JERRY_DEBUGGER_BACKTRACE,
                                       JERRY_DEBUGGER_BACKTRACE_END]:
                    raise Exception("Backtrace data expected")

            prompt.cmdloop()

        elif buffer_type in [JERRY_DEBUGGER_EVAL_RESULT,
                             JERRY_DEBUGGER_EVAL_RESULT_END,
                             JERRY_DEBUGGER_EVAL_ERROR,
                             JERRY_DEBUGGER_EVAL_ERROR_END]:

            message = b""
            eval_type = buffer_type
            while True:
                message += data[3:]

                if buffer_type in [JERRY_DEBUGGER_EVAL_RESULT_END,
                                   JERRY_DEBUGGER_EVAL_ERROR_END]:
                    break

                data = debugger.get_message(True)
                buffer_type = ord(data[2])
                buffer_size = ord(data[1]) - 1

                if buffer_type not in [eval_type,
                                       eval_type + 1]:
                    raise Exception("Eval result expected")

            if buffer_type == JERRY_DEBUGGER_EVAL_ERROR_END:
                print("Uncaught exception: %s" % (message))
            else:
                print(message)

            prompt.cmdloop()

        elif buffer_type == JERRY_DEBUGGER_MEMSTATS_RECEIVE:

            memory_stats = struct.unpack(debugger.byte_order + debugger.idx_format *5,
                                         data[3: 3 + 4 *5])

            print("Allocated bytes: %d" % (memory_stats[0]))
            print("Byte code bytes: %d" % (memory_stats[1]))
            print("String bytes: %d" % (memory_stats[2]))
            print("Object bytes: %d" % (memory_stats[3]))
            print("Property bytes: %d" % (memory_stats[4]))

            prompt.cmdloop()

        else:
            raise Exception("Unknown message")


if __name__ == "__main__":
    try:
        main()
    except socket.error as error_msg:
        try:
            errno = error_msg.errno
            msg = str(error_msg)
        except:
            errno = error_msg[0]
            msg = error_msg[1]

        if errno == 111:
            sys.exit("Failed to connect to the JerryScript debugger.")
        elif errno == 32 or errno == 104:
            sys.exit("Connection closed.")
        else:
            sys.exit("Failed to connect to the JerryScript debugger.\nError: %s" % (msg))
