"""Disassembler of Python bytecode into mnemonics."""

# Adapted from stdlib dis.py, but returns structured information
# instead of printing to stdout.

import sys
import collections

from opcode import *
from opcode import __all__ as _opcodes_all

__all__ = ["dis", "disassemble", "distb", "disco",
           "findlinestarts", "findlabels"] + _opcodes_all
del _opcodes_all

def dis(x=None):
    for disline in disgen(x):
        if disline.first and disline.offset > 0:
            print()
        print(format_dis_line(disline))

def format_dis_line(disline):
    if disline.first:
        lineno = "%3d" % disline.lineno
    else:
        lineno = "   "
    if disline.target:
        label = ">>"
    else:
        label = "  "
    if disline.oparg is not None:
        oparg = repr(disline.oparg)
    else:
        oparg = ""
    return "%s    %s %4r %-20s %5s %s" % (lineno, label, disline.offset, disline.opcode, oparg, disline.argstr)

def disgen(x=None):
    """Disassemble methods, functions, or code.

    With no argument, disassemble the last traceback.

    """
    if x is None:
        return distb()
    if hasattr(x, 'im_func'):
        x = x.im_func
    if hasattr(x, 'func_code'):
        x = x.func_code
    if hasattr(x, 'co_code'):
        return disassemble(x)
    else:
        raise TypeError(
            "don't know how to disassemble %s objects" %
            type(x).__name__
        )

def distb(tb=None):
    """Disassemble a traceback (default: last traceback)."""
    if tb is None:
        try:
            tb = sys.last_traceback
        except AttributeError:
            raise RuntimeError("no last traceback to disassemble")
        while tb.tb_next: 
            tb = tb.tb_next
    return disassemble(tb.tb_frame.f_code, tb.tb_lasti)

DisLine = collections.namedtuple(
    'DisLine',
    "lineno first target offset opcode oparg argstr"
    )

def disassemble(co, lasti=-1):
    """Disassemble a code object."""
    code = co.co_code
    labels = findlabels(code)
    linestarts = dict(findlinestarts(co))
    n = len(code)
    i = 0
    extended_arg = 0
    free = None

    dislines = []
    lineno = linestarts[0]

    while i < n:
        op = byte_from_code(code, i)
        first = i in linestarts
        if first:
            lineno = linestarts[i]

        #if i == lasti: print '-->',
        #else: print '   ',
        target = i in labels
        offset = i
        opcode = opname[op]
        i = i+1
        if op >= HAVE_ARGUMENT:
            oparg = byte_from_code(code, i) + byte_from_code(code, i+1)*256 + extended_arg
            extended_arg = 0
            i = i+2
            if op == EXTENDED_ARG:
                extended_arg = oparg*65536
            if op in hasconst:
                argstr = '(' + repr(co.co_consts[oparg]) + ')'
            elif op in hasname:
                argstr = '(' + co.co_names[oparg] + ')'
            elif op in hasjabs:
                argstr = '(-> ' + repr(oparg) + ')'
            elif op in hasjrel:
                argstr = '(-> ' + repr(i + oparg) + ')'
            elif op in haslocal:
                argstr = '(' + co.co_varnames[oparg] + ')'
            elif op in hascompare:
                argstr = '(' + cmp_op[oparg] + ')'
            elif op in hasfree:
                if free is None:
                    free = co.co_cellvars + co.co_freevars
                argstr = '(' + free[oparg] + ')'
            else:
                argstr = ""
        else:
            oparg = None
            argstr = ""
        yield DisLine(lineno=lineno, first=first, target=target, offset=offset, opcode=opcode, oparg=oparg, argstr=argstr)


def byte_from_code(code, i):
    byte = code[i]
    if not isinstance(byte, int):
        byte = ord(byte)
    return byte

def findlabels(code):
    """Detect all offsets in a bytecode which are jump targets.

    Return the list of offsets.

    """
    labels = []
    n = len(code)
    i = 0
    while i < n:
        op = byte_from_code(code, i)
        i = i+1
        if op >= HAVE_ARGUMENT:
            oparg = byte_from_code(code, i) + byte_from_code(code, i+1)*256
            i = i+2
            label = -1
            if op in hasjrel:
                label = i+oparg
            elif op in hasjabs:
                label = oparg
            if label >= 0:
                if label not in labels:
                    labels.append(label)
    return labels

def findlinestarts(code):
    """Find the offsets in a bytecode which are start of lines in the source.

    Generate pairs (offset, lineno) as described in Python/compile.c.

    """
    byte_increments = [byte_from_code(code.co_lnotab, i) for i in range(0, len(code.co_lnotab), 2)]
    line_increments = [byte_from_code(code.co_lnotab, i) for i in range(1, len(code.co_lnotab), 2)]

    lastlineno = None
    lineno = code.co_firstlineno
    addr = 0
    for byte_incr, line_incr in zip(byte_increments, line_increments):
        if byte_incr:
            if lineno != lastlineno:
                yield (addr, lineno)
                lastlineno = lineno
            addr += byte_incr
        lineno += line_incr
    if lineno != lastlineno:
        yield (addr, lineno)

def _test():
    """Simple test program to disassemble a file."""
    if sys.argv[1:]:
        if sys.argv[2:]:
            sys.stderr.write("usage: python dis.py [-|file]\n")
            sys.exit(2)
        fn = sys.argv[1]
        if not fn or fn == "-":
            fn = None
    else:
        fn = None
    if fn is None:
        f = sys.stdin
    else:
        f = open(fn)
    source = f.read()
    if fn is not None:
        f.close()
    else:
        fn = "<stdin>"
    code = compile(source, fn, "exec")
    dis(code)

if __name__ == "__main__":
    _test()
