import struct

from .attributes import Attribute
from .constants import Classref, ConstantPool
from .fields import Field
from .methods import Method


# From: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html

##########################################################################
# Chapter 4: The class File Format
##########################################################################

# This chapter describes the Java Virtual Machine class file format. Each class
# file contains the definition of a single class or interface. Although a class
# or interface need not have an external representation literally contained in a
# file (for instance, because the class is generated by a class loader), we will
# colloquially refer to any valid representation of a class or interface as
# being in the class file format.

# A class file consists of a stream of 8-bit bytes. All 16-bit, 32-bit, and
# 64-bit quantities are constructed by reading in two, four, and eight
# consecutive 8-bit bytes, respectively. Multibyte data items are always stored
# in big-endian order, where the high bytes come first. In the Java SE platform,
# this format is supported by interfaces java.io.DataInput and
# java.io.DataOutput and classes such as java.io.DataInputStream and
# java.io.DataOutputStream.

# This chapter defines its own set of data types representing class file data:
# The types u1, u2, and u4 represent an unsigned one-, two-, or four-byte
# quantity, respectively. In the Java SE platform, these types may be read by
# methods such as readUnsignedByte, readUnsignedShort, and readInt of the
# interface java.io.DataInput.

# This chapter presents the class file format using pseudostructures written in
# a C-like structure notation. To avoid confusion with the fields of classes and
# class instances, etc., the contents of the structures describing the class
# file format are referred to as items. Successive items are stored in the class
# file sequentially, without padding or alignment.

# Tables, consisting of zero or more variable-sized items, are used in several
# class file structures. Although we use C-like array syntax to refer to table
# items, the fact that tables are streams of varying-sized structures means that
# it is not possible to translate a table index directly to a byte offset into
# the table.

# Where we refer to a data structure as an array, it consists of zero or more
# contiguous fixed-sized items and can be indexed like an array.

# Reference to an ASCII character in this chapter should be interpreted to mean
# the Unicode code point corresponding to the ASCII character.


class ClassFileWriter:
    def __init__(self, outfile, constant_pool):
        self._outfile = outfile
        self.constant_pool = constant_pool

    def write_bytes(self, b):
        self._outfile.write(b)

    def write_s1(self, u1):
        self._outfile.write(struct.pack('b', u1))

    def write_u1(self, u1):
        self._outfile.write(struct.pack('B', u1))

    def write_s2(self, u2):
        self._outfile.write(struct.pack('>h', u2))

    def write_u2(self, u2):
        self._outfile.write(struct.pack('>H', u2))

    def write_s4(self, u4):
        self._outfile.write(struct.pack('>i', u4))

    def write_u4(self, u4):
        self._outfile.write(struct.pack('>I', u4))

    def write_f(self, f):
        self._outfile.write(struct.pack('>f', f))

    def write_s8(self, u8):
        self._outfile.write(struct.pack('>q', u8))

    def write_u8(self, u8):
        self._outfile.write(struct.pack('>Q', u8))

    def write_d(self, d):
        self._outfile.write(struct.pack('>d', d))


class ClassFileReader:
    def __init__(self, infile, constant_pool, debug=None):
        self._infile = infile
        self.constant_pool = constant_pool
        if debug is None:
            self.debug = lambda *msg: print(*msg)
        else:
            self.debug = lambda *msg: debug.write(' '.join(str(m) for m in msg) + '\n')

    def read_bytes(self, count):
        return self._infile.read(count)

    def read_u1(self):
        return struct.unpack('B', self._infile.read(1))[0]

    def read_s2(self):
        return struct.unpack('>h', self._infile.read(2))[0]

    def read_u2(self):
        return struct.unpack('>H', self._infile.read(2))[0]

    def read_u4(self):
        return struct.unpack('>I', self._infile.read(4))[0]

    def read_f(self):
        return struct.unpack('>f', self._infile.read(4))[0]

    def read_u8(self):
        return struct.unpack('>Q', self._infile.read(8))[0]

    def read_d(self):
        return struct.unpack('>d', self._infile.read(8))[0]


# ------------------------------------------------------------------------
# 4.1. The ClassFile Structure
# ------------------------------------------------------------------------

# A class file consists of a single ClassFile structure:

class BaseClass:
    # u4             magic;
    # u2             minor_version;
    # u2             major_version;
    # u2             constant_pool_count;
    # cp_info        constant_pool[constant_pool_count-1];
    # u2             access_flags;
    # u2             this_class;
    # u2             super_class;
    # u2             interfaces_count;
    # u2             interfaces[interfaces_count];
    # u2             fields_count;
    # field_info     fields[fields_count];
    # u2             methods_count;
    # method_info    methods[methods_count];
    # u2             attributes_count;
    # attribute_info attributes[attributes_count];

    # Table 4.1. Class access and property modifiers

    ACC_PUBLIC = 0x0001  # Declared public; may be accessed from outside its package.
    ACC_FINAL = 0x0010  # Declared final; no subclasses allowed.
    ACC_SUPER = 0x0020  # Treat superclass methods specially when invoked by the invokespecial instruction.
    ACC_INTERFACE = 0x0200  # Is an interface, not a class.
    ACC_ABSTRACT = 0x0400  # Declared abstract; must not be instantiated.
    ACC_SYNTHETIC = 0x1000  # Declared synthetic; not present in the source code.
    ACC_ANNOTATION = 0x2000  # Declared as an annotation type.
    ACC_ENUM = 0x4000  # Declared as an enum type.

    def __init__(
                self, name, extends=None,
                public=True, final=False, interface=False,
                abstract=False, synthetic=False, annotation=False, enum=False,
                implements=None
            ):

        # Constructor properties.
        self.public = public
        self.final = final
        self.super = True
        self.interface = interface
        self.abstract = abstract
        self.synthetic = synthetic
        self.annotation = annotation
        self.enum = enum

        # The magic item supplies the magic number identifying the class file
        # format; it has the value 0xCAFEBABE.
        self.magic = 0xCAFEBABE

        # The values of the minor_version and major_version items are the minor
        # and major version numbers of this class file. Together, a major and a
        # minor version number determine the version of the class file format.
        # If a class file has major version number M and minor version number m,
        # we denote the version of its class file format as M.m. Thus, class
        # file format versions may be ordered lexicographically, for example,
        # 1.5 < 2.0 < 2.1.

        # A Java Virtual Machine implementation can support a class file format
        # of version v if and only if v lies in some contiguous range Mi.0 ≤ v ≤
        # Mj.m. The release level of the Java SE platform to which a Java
        # Virtual Machine implementation conforms is responsible for determining
        # the range.

        # Oracle's Java Virtual Machine implementation in JDK release 1.0.2
        # supports class file format versions 45.0 through 45.3 inclusive. JDK
        # releases 1.1.* support class file format versions in the range 45.0
        # through 45.65535 inclusive. For k ≥ 2, JDK release 1.k supports class
        # file format versions in the range 45.0 through 44+k.0 inclusive.
        #
        # i.e.,
        #   J2SE 8 = 52 (0x34 hex),
        #   J2SE 7 = 51 (0x33 hex),
        #   J2SE 6.0 = 50 (0x32 hex),
        #   J2SE 5.0 = 49 (0x31 hex),
        #   JDK 1.4 = 48 (0x30 hex),
        #   JDK 1.3 = 47 (0x2F hex),
        #   JDK 1.2 = 46 (0x2E hex),
        #   JDK 1.1 = 45 (0x2D hex).
        #
        # If major_version is set to 51 or higher, and you're using a Java 7
        # virtual machine, you'll need to use the ``-XX:-UseSplitVerifier``
        # on the command line. This disables the use of the StackMapFrame
        # verifier. If you're using a Java 8 or higher VM, you'll need to
        # use -noverify, because StackMapFrame are no longer optional.
        self.major_version = 50
        self.minor_version = 0

        # Each value in the interfaces array must be a valid index into the
        # constant_pool table. The constant_pool entry at each value of interfaces[i],
        # where 0 ≤ i < interfaces_count, must be a CONSTANT_Class_info structure
        # (§4.4.1) representing an interface that is a direct superinterface of this
        # class or interface type, in the left-to-right order given in the source for
        # the type.
        if implements is None:
            self.interfaces = []
        else:
            self.interfaces = [Classref(iface) for iface in implements]

        # Each value in the fields table must be a field_info (§4.5) structure giving a
        # complete description of a field in this class or interface. The fields table
        # includes only those fields that are declared by this class or interface. It
        # does not include items representing fields that are inherited from
        # superclasses or superinterfaces.
        self.fields = []

        # Each value in the methods table must be a method_info (§4.6) structure giving
        # a complete description of a method in this class or interface. If neither of
        # the ACC_NATIVE and ACC_ABSTRACT flags are set in the access_flags item of a
        # method_info structure, the Java Virtual Machine instructions implementing the
        # method are also supplied.

        # The method_info structures represent all methods declared by this class or
        # interface type, including instance methods, class methods, instance
        # initialization methods (§2.9), and any class or interface initialization
        # method (§2.9). The methods table does not include items representing methods
        # that are inherited from superclasses or superinterfaces.
        self.methods = []

        # Each value of the attributes table must be an attribute_info (§4.7) structure.

        # The attributes defined by this specification as appearing in the attributes
        # table of a ClassFile structure are the InnerClasses (§4.7.6), EnclosingMethod
        # (§4.7.7), Synthetic (§4.7.8), Signature (§4.7.9), SourceFile (§4.7.10),
        # SourceDebugExtension (§4.7.11), Deprecated (§4.7.15),
        # RuntimeVisibleAnnotations (§4.7.16), RuntimeInvisibleAnnotations (§4.7.17),
        # and BootstrapMethods (§4.7.21) attributes.

        # If a Java Virtual Machine implementation recognizes class files whose version
        # number is 49.0 or above, it must recognize and correctly read Signature
        # (§4.7.9), RuntimeVisibleAnnotations (§4.7.16), and RuntimeInvisibleAnnotations
        # (§4.7.17) attributes found in the attributes table of a ClassFile structure of
        # a class file whose version number is 49.0 or above.

        # If a Java Virtual Machine implementation recognizes class files whose version
        # number is 51.0 or above, it must recognize and correctly read BootstrapMethods
        # (§4.7.21) attributes found in the attributes table of a ClassFile structure of
        # a class file whose version number is 51.0 or above.

        # A Java Virtual Machine implementation is required to silently ignore any or
        # all attributes in the attributes table of a ClassFile structure that it does
        # not recognize. Attributes not defined in this specification are not allowed to
        # affect the semantics of the class file, but only to provide additional
        # descriptive information (§4.7.1).
        self.attributes = []

        # The value of the this_class item must be a valid index into the constant_pool
        # table. The constant_pool entry at that index must be a CONSTANT_Class_info
        # structure (§4.4.1) representing the class or interface defined by this class
        # file.
        self.this_class = Classref(name)

        # For a class, the value of the super_class item either must be zero or must be
        # a valid index into the constant_pool table. If the value of the super_class
        # item is nonzero, the constant_pool entry at that index must be a
        # CONSTANT_Class_info structure (§4.4.1) representing the direct superclass of
        # the class defined by this class file. Neither the direct superclass nor any of
        # its superclasses may have the ACC_FINAL flag set in the access_flags item of
        # its ClassFile structure.

        # If the value of the super_class item is zero, then this class file must
        # represent the class Object, the only class or interface without a direct
        # superclass.

        # For an interface, the value of the super_class item must always be a valid
        # index into the constant_pool table. The constant_pool entry at that index must
        # be a CONSTANT_Class_info structure representing the class Object.
        self.super_class = Classref(extends if extends else 'java/lang/Object')

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, self.this_class.name)

    @staticmethod
    def read(infile, debug=None):
        constant_pool = ConstantPool()
        reader = ClassFileReader(infile, constant_pool, debug=debug)
        dump = 0 if debug else None

        magic = reader.read_u4()
        minor_version = reader.read_u2()
        major_version = reader.read_u2()

        if dump is not None:
            reader.debug("    " * dump, 'Magic: %x' % (magic))
            reader.debug("    " * dump, 'Version: %s.%s' % (major_version, minor_version))

        reader.constant_pool.read(reader, dump)

        access_flags = reader.read_u2()
        val = reader.read_u2()
        this_class = reader.constant_pool[val].name.bytes.decode('mutf-8')
        val = reader.read_u2()
        super_class = reader.constant_pool[val].name.bytes.decode('mutf-8')

        if dump is not None:
            reader.debug("    " * dump, 'Class %s' % this_class)
            reader.debug("    " * dump, '    Extends %s' % super_class)

            access_description = ', '.join(f for f in [
                    flag if access_flags & mask else None
                    for flag, mask in [
                        ('public', Class.ACC_PUBLIC),
                        ('final', Class.ACC_FINAL),
                        ('super', Class.ACC_SUPER),
                        ('interface', Class.ACC_INTERFACE),
                        ('abstract', Class.ACC_ABSTRACT),
                        ('synthetic', Class.ACC_SYNTHETIC),
                        ('annotation', Class.ACC_ANNOTATION),
                        ('enum', Class.ACC_ENUM),
                    ]
                ] if f)
            reader.debug("    " * dump, '    Flags: 0x%04x%s' % (access_flags, ' (%s)') % (
                access_description if access_description else ''
            ))

        interfaces_count = reader.read_u2()
        if dump is not None:
            reader.debug("    " * (dump + 1), 'Interfaces: (%s)' % interfaces_count)
        for i in range(0, interfaces_count):
            val = reader.read_u2()
            interface = reader.constant_pool[val]
            reader.debug("    " * (dump + 2), interface.name)

        fields_count = reader.read_u2()
        if dump is not None:
            reader.debug("    " * (dump + 1), 'Fields: (%s)' % fields_count)
        for i in range(0, fields_count):
            Field.read(reader, dump=dump + 2 if dump is not None else dump)

        methods_count = reader.read_u2()
        if dump is not None:
            reader.debug("    " * (dump + 1), 'Methods: (%s)' % methods_count)
        for i in range(0, methods_count):
            Method.read(reader, dump=dump + 2 if dump is not None else dump)

        attributes_count = reader.read_u2()
        if dump is not None:
            reader.debug("    " * (dump + 1), 'Attributes: (%s)' % attributes_count)
        for i in range(0, attributes_count):
            Attribute.read(reader, dump=dump + 2 if dump is not None else dump)

        if access_flags & BaseClass.ACC_ENUM:
            klass = Enum
        elif access_flags & BaseClass.ACC_INTERFACE:
            klass = Interface
        else:
            klass = Class

        return klass(
            this_class,
            extends=super_class,
            public=bool(access_flags & BaseClass.ACC_PUBLIC),
            final=bool(access_flags & BaseClass.ACC_FINAL),
            abstract=bool(access_flags & BaseClass.ACC_ABSTRACT),
            # synthetic=bool(access_flags & self.ACC_SYNTHETIC),
            # annotation=bool(access_flags & self.ACC_ANNOTATION),
        )

    def write(self, outfile):
        """Output the classfile to the output stream provided"""
        # First, create a writer.
        constant_pool = ConstantPool()
        writer = ClassFileWriter(outfile, constant_pool)

        self.this_class.resolve(constant_pool)
        self.super_class.resolve(constant_pool)

        for interface in self.interfaces:
            interface.resolve(constant_pool)

        for field in self.fields:
            field.resolve(constant_pool)

        for method in self.methods:
            method.resolve(constant_pool)

        for attribute in self.attributes:
            attribute.resolve(constant_pool)

        # import pprint
        # pprint.pprint(constant_pool._constants)
        # pprint.pprint(constant_pool._constant_pool)

        # Now, write everything out.
        writer.write_u4(self.magic)
        writer.write_u2(self.minor_version)
        writer.write_u2(self.major_version)

        constant_pool.write(writer)

        writer.write_u2(self.access_flags)
        writer.write_u2(writer.constant_pool.index(self.this_class))
        writer.write_u2(writer.constant_pool.index(self.super_class))

        writer.write_u2(self.interfaces_count)
        for interface in self.interfaces:
            writer.write_u2(writer.constant_pool.index(interface))

        writer.write_u2(self.fields_count)
        for field in self.fields:
            field.write(writer)

        writer.write_u2(self.methods_count)
        for method in self.methods:
            method.write(writer)

        writer.write_u2(self.attributes_count)
        for attribute in self.attributes:
            attribute.write(writer)

    @property
    def access_flags(self):
        """The value of the access_flags item is a mask of flags used to denote access
        permissions to and properties of this class or interface. The interpretation
        of each flag, when set, is as shown in Table 4.1.

        A class may be marked with the ACC_SYNTHETIC flag to indicate that it was
        generated by a compiler and does not appear in source code.

        The ACC_ENUM flag indicates that this class or its superclass is declared as
        an enumerated type.

        An interface is distinguished by its ACC_INTERFACE flag being set. If its
        ACC_INTERFACE flag is not set, this class file defines a class, not an
        interface.

        If the ACC_INTERFACE flag of this class file is set, its ACC_ABSTRACT flag
        must also be set (JLS §9.1.1.1). Such a class file must not have its
        ACC_FINAL, ACC_SUPER or ACC_ENUM flags set.

        An annotation type must have its ACC_ANNOTATION flag set. If the
        ACC_ANNOTATION flag is set, the ACC_INTERFACE flag must be set as well. If the
        ACC_INTERFACE flag of this class file is not set, it may have any of the other
        flags in Table 4.1 set, except the ACC_ANNOTATION flag. However, such a class
        file cannot have both its ACC_FINAL and ACC_ABSTRACT flags set (JLS §8.1.1.2).

        The ACC_SUPER flag indicates which of two alternative semantics is to be
        expressed by the invokespecial instruction (§invokespecial) if it appears in
        this class. Compilers to the instruction set of the Java Virtual Machine
        should set the ACC_SUPER flag.

        The ACC_SUPER flag exists for backward compatibility with code compiled by
        older compilers for the Java programming language. In Oracle’s JDK prior to
        release 1.0.2, the compiler generated ClassFile access_flags in which the flag
        now representing ACC_SUPER had no assigned meaning, and Oracle's Java Virtual
        Machine implementation ignored the flag if it was set.

        All bits of the access_flags item not assigned in Table 4.1 are reserved for
        future use. They should be set to zero in generated class files and should be
        ignored by Java Virtual Machine implementations.
        """
        return (
            (self.ACC_PUBLIC if self.public else 0) |
            (self.ACC_FINAL if self.final else 0) |
            (self.ACC_SUPER if self.super else 0) |
            (self.ACC_INTERFACE if self.interface else 0) |
            (self.ACC_ABSTRACT if self.abstract else 0) |
            (self.ACC_SYNTHETIC if self.synthetic else 0) |
            (self.ACC_ANNOTATION if self.annotation else 0) |
            (self.ACC_ENUM if self.enum else 0)
        )

    @property
    def interfaces_count(self):
        """The value of the interfaces_count item gives the number of direct
        superinterfaces of this class or interface type.
        """
        return len(self.interfaces)

    @property
    def fields_count(self):
        """The value of the fields_count item gives the number of field_info structures
        in the fields table. The field_info structures (§4.5) represent all fields,
        both class variables and instance variables, declared by this class or
        interface type.
        """
        return len(self.fields)

    @property
    def methods_count(self):
        """The value of the methods_count item gives the number of method_info structures
        in the methods table.
        """
        return len(self.methods)

    @property
    def attributes_count(self):
        """The value of the attributes_count item gives the number of attributes (§4.7)
        in the attributes table of this class.
        """
        return len(self.attributes)


class Class(BaseClass):
    def __init__(self, name, extends=None, implements=None, public=True, final=False, abstract=False):
        super(Class, self).__init__(
            name, extends, implements=implements, public=public, final=final, abstract=abstract
        )


class Interface(BaseClass):
    def __init__(self, name, extends=None, public=True, final=False, abstract=False):
        super(Interface, self).__init__(name, extends, public=public, final=final, abstract=False, interface=True)


class Enum(BaseClass):
    def __init__(self, name, extends=None, public=True, final=False):
        super(Enum, self).__init__(name, extends, public=public, final=final, enum=True)
