# vim:fileencoding=utf-8
# License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
# globals: ρσ_str, ρσ_last_exception

_traceback_state = {'source_map': None, 'decoded': None}

_VLQ_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'


def set_source_map_data(data):
    _traceback_state['source_map'] = data
    _traceback_state['decoded'] = None


def serialize_error_for_formatting(err):
    return {
        'name': err.name or 'Error',
        'stack': err.stack or '',
        'message': err.message or '',
    }


def unserialize_error_for_formatting(err):
    ans = new Error()
    ans.name = err.name
    ans.stack = err.stack
    ans.message = err.message
    return ans


def _vlq_decode(encoded):
    result = v'[]'
    i = 0
    n = encoded.length
    while i < n:
        value = 0
        shift = 0
        has_more = True
        while has_more:
            digit = _VLQ_BASE64.indexOf(encoded[i])
            i += 1
            has_more = (digit & 32) != 0
            value += (digit & 31) << shift
            shift += 5
        if value & 1:
            value = -(value >> 1)
        else:
            value = value >> 1
        result.push(value)
    return result


def _decode_source_map(smap):
    sources = smap.sources or v'[]'
    source_root = smap.sourceRoot or ''
    mappings_str = smap.mappings or ''
    mapping_lines = mappings_str.split(';')
    decoded = v'[]'
    prev_src_idx = 0
    prev_src_line = 0
    prev_src_col = 0
    for gen_line in range(mapping_lines.length):
        line_str = mapping_lines[gen_line]
        line_entries = v'[]'
        if line_str:
            segments = line_str.split(',')
            prev_col = 0
            for seg_str in segments:
                if not seg_str:
                    continue
                fields = _vlq_decode(seg_str)
                if fields.length >= 4:
                    gen_col = prev_col + fields[0]
                    prev_col = gen_col
                    src_idx = prev_src_idx + fields[1]
                    prev_src_idx = src_idx
                    src_line = prev_src_line + fields[2]
                    prev_src_line = src_line
                    src_col = prev_src_col + fields[3]
                    prev_src_col = src_col
                    src_file = (
                        sources[src_idx]
                        if src_idx >= 0 and src_idx < sources.length
                        else ''
                    )
                    if source_root and src_file:
                        src_file = source_root + src_file
                    line_entries.push({
                        'gen_col': gen_col, 'src_file': src_file,
                        'src_line': src_line, 'src_col': src_col
                    })
                elif fields.length >= 1:
                    prev_col += fields[0]
        decoded.push(line_entries)
    return decoded


def _find_mapping(decoded, gen_line, gen_col):
    if not decoded or gen_line < 0 or gen_line >= decoded.length:
        return None
    line_entries = decoded[gen_line]
    if not line_entries or not line_entries.length:
        return None
    best = None
    for entry in line_entries:
        if entry.gen_col <= gen_col:
            if best is None or entry.gen_col > best.gen_col:
                best = entry
    return best


def _parse_stack_frame(line):
    # Chrome/Node: "    at functionName (file:line:col)"
    m = /^\s*at\s+(.*?)\s+\((.+):(\d+):(\d+)\)/.exec(line)
    if m:
        return {
            'func': m[1] or None,
            'file': m[2],
            'line': parseInt(m[3], 10) - 1,
            'col': parseInt(m[4], 10) - 1,
        }
    # Chrome/Node: "    at file:line:col"
    m = /^\s*at (.+):(\d+):(\d+)$/.exec(line)
    if m:
        return {
            'func': None,
            'file': m[1],
            'line': parseInt(m[2], 10) - 1,
            'col': parseInt(m[3], 10) - 1,
        }
    # Firefox: "functionName@file:line:col"
    m = /^(.*?)@(.+):(\d+):(\d+)$/.exec(line)
    if m:
        return {
            'func': m[1] or None,
            'file': m[2],
            'line': parseInt(m[3], 10) - 1,
            'col': parseInt(m[4], 10) - 1,
        }
    return None


def _get_decoded_map():
    if _traceback_state['decoded'] is None and _traceback_state['source_map']:
        _traceback_state['decoded'] = _decode_source_map(
            _traceback_state['source_map'])
    return _traceback_state['decoded']


def _get_internal_traceback(err):
    lines = ρσ_str.splitlines(err.stack or '')
    final_lines = v'[]'
    start_line_idx = -1
    for i, line in enumerate(lines):
        if i > 0:
            line = line.trim()
            if (line.startsWith('at new ' + err.name) or
                    line.startsWith(err.name + '@')):
                # this is the constructor for a RapydScript Exception
                start_line_idx = i + 1
                break
    if start_line_idx < 0:
        start_line_idx = 1
        for i, xline in enumerate(lines):
            if i > 0:
                line = xline.trim()
                if (
                    v"line.startsWith('at ') || line.includes('@')" or
                    v"/:\d+:\d+$/.test(line)"
                ):
                    start_line_idx = i
                    break
    frame_lines = lines[start_line_idx:]
    final_lines = final_lines.concat(frame_lines)
    decoded = _get_decoded_map()
    mapped_lines = v'[]'
    if decoded:
        for frame_line in frame_lines:
            frame = _parse_stack_frame(frame_line)
            if frame:
                mapping = _find_mapping(decoded, frame.line, frame.col)
                if mapping:
                    loc = (
                        '  File "' + mapping.src_file + '", line ' +
                        (mapping.src_line + 1) + ', col ' +
                        (mapping.src_col + 1)
                    )
                    if frame.func:
                        loc += ', in ' + frame.func
                    mapped_lines.push(loc)
    return lines[0], final_lines, mapped_lines


def format_exception(exc, limit):
    if jstype(exc) is 'undefined':
        exc = ρσ_last_exception
    if not isinstance(exc, Error):
        if exc and exc.toString:
            return [exc.toString()]
        return []
    e, js_lines, rs_lines = _get_internal_traceback(exc)
    if js_lines.length > 0:
        if limit:
            js_lines = js_lines[:limit] if limit > 0 else js_lines[limit:]
            rs_lines = rs_lines[:limit] if limit > 0 else rs_lines[limit:]
        js_lines.reverse()
        rs_lines.reverse()
        js_lines.push(e)
        js_lines = v"['Traceback (most recent call last):'].concat(js_lines)"
        if rs_lines.length > 0:
            js_lines = js_lines.concat(
                ['', 'Mapped Traceback (RapydScript):']).concat(rs_lines)
        return [l + '\n' for l in js_lines]
    return [exc.toString()]


def format_exc(limit):
    return format_exception(ρσ_last_exception, limit).join('')


def print_exc(limit):
    print(format_exc(limit))


def format_stack(limit):
    stack = Error().stack
    if not stack:
        return []
    lines = str.splitlines(stack)[2:]
    lines.reverse()
    if limit:
        lines = lines[:limit + 1] if limit > 0 else lines[limit:]
    return [l + '\n' for l in lines]


def print_stack(limit):
    print(format_stack(limit).join(''))
