#!/usr/bin/env python
# -*- coding: utf8 -*-

from .topping import Topping

from jawa.constants import *
from jawa.util.descriptor import method_descriptor, field_descriptor

import traceback
import six
import six.moves

# EnumFacing.Plane.  Needed because this is also a predicate, which is used
# to get certain facings
class Plane:
    def __init__(self, directions):
        self.directions = directions
HORIZONTAL = Plane(["NORTH", "EAST", "SOUTH", "WEST"])
VERTICAL = Plane(["UP", "DOWN"])
PLANES = { "HORIZONTAL": HORIZONTAL, "VERTICAL": VERTICAL }

# Classes that represent predicates in various versions
PREDICATE_CLASSES = ("com/google/common/base/Predicate", "java/util/function/Predicate")

class BlockStateTopping(Topping):
    """Gets tile entity (block entity) types."""

    PROVIDES = [
        "blocks.states"
    ]

    DEPENDS = [
        "blocks",
        "version.data",
        "version.is_flattened",
        "identify.blockstatecontainer",
        "identify.sounds.list",
        "identify.enumfacing.plane"
    ]

    @staticmethod
    def act(aggregate, classloader, verbose=False):
        if "blockstatecontainer" not in aggregate["classes"]:
            if verbose:
                print("blockstatecontainer not found; skipping blockstates")
            return

        is_flattened = aggregate["version"]["is_flattened"]

        blockstatecontainer = aggregate["classes"]["blockstatecontainer"]
        block_cf = classloader[aggregate["classes"]["block.superclass"]]
        plane = aggregate["classes"]["enumfacing.plane"]

        # Part 1: build up a list of property fields, by class.  Also build a set of property types.
        # 18w19a and above use a builder to register states; before that they just directly returned a container.
        # Note that blockstatecontainer is the builder class in 18w19a.
        is_18w19a = aggregate["version"]["data"] >= 1484
        is_protected = lambda m: m.access_flags.acc_protected
        if is_18w19a:
            base_method = block_cf.methods.find_one(returns="V", args="L" + blockstatecontainer + ";", f=is_protected)
        else:
            base_method = block_cf.methods.find_one(returns="L" + blockstatecontainer + ";", args="",  f=is_protected)

        def matches(other_method):
            return (other_method.name.value == base_method.name.value and
                    other_method.descriptor.value == base_method.descriptor.value)

        _property_types = set()
        # Properties that are used by each block class
        properties_by_class = {}
        def process_class(name):
            """
            Gets the properties for the given block class, checking the parent
            class if none are defined.  Returns the properties, and also adds
            them to properties_by_class
            """
            if name in properties_by_class:
                # Caching - avoid reading the same class multiple times
                return properties_by_class[name]

            cf = classloader[name]
            method = cf.methods.find_one(f=matches)

            if not method:
                properties = process_class(cf.super_.name.value)
                properties_by_class[name] = properties
                return properties

            properties = None
            if_pos = None
            stack = []
            for ins in method.code.disassemble():
                # This could _almost_ just be checking for getstatic, but
                # brewing stands use an array of properties as the field,
                # so we need some stupid extra logic.
                if ins == "new":
                    assert not is_18w19a # In 18w19a this should be a parameter
                    const = ins.operands[0]
                    type_name = const.name.value
                    assert type_name == blockstatecontainer
                    stack.append(object())
                elif ins == "aload" and ins.operands[0].value == 1:
                    assert is_18w19a # The parameter is only used in 18w19a and above
                    stack.append(object())
                elif ins in ("sipush", "bipush"):
                    stack.append(ins.operands[0].value)
                elif ins in ("anewarray", "newarray"):
                    length = stack.pop()
                    val = [None] * length
                    stack.append(val)
                elif ins == "getstatic":
                    const = ins.operands[0]
                    prop = {
                        "field_name": const.name_and_type.name.value
                    }
                    desc = field_descriptor(const.name_and_type.descriptor.value)
                    _property_types.add(desc.name)
                    stack.append(prop)
                elif ins == "aaload":
                    index = stack.pop()
                    array = stack.pop()
                    prop = array.copy()
                    prop["array_index"] = index
                    stack.append(prop)
                elif ins == "aastore":
                    value = stack.pop()
                    index = stack.pop()
                    array = stack.pop()
                    array[index] = value
                elif ins == "dup":
                    stack.append(stack[-1])
                elif ins == "invokespecial":
                    const = ins.operands[0]
                    assert const.name_and_type.name == "<init>"
                    desc = method_descriptor(const.name_and_type.descriptor.value)
                    assert len(desc.args) == 2

                    # Normally this constructor call would return nothing, but
                    # in this case we'd rather remove the object it's called on
                    # and keep the properties array (its parameter)
                    arg = stack.pop()
                    stack.pop() # Block
                    stack.pop() # Invocation target
                    stack.append(arg)
                elif ins == "invokevirtual":
                    # Two possibilities (both only present pre-flattening):
                    # 1. It's isDouble() for a slab.  Two different sets of
                    #    properties in that case.
                    # 2. It's getTypeProperty() for flowers.  Only one
                    #    set of properties, but other hacking is needed.
                    # We can differentiate these cases based off of the return
                    # type.
                    # There is a third option post 18w19a:
                    # 3. It's calling the state container's register method.
                    # We can check this just by the type.
                    const = ins.operands[0]
                    desc = method_descriptor(const.name_and_type.descriptor.value)

                    if const.class_.name == blockstatecontainer:
                        # Case 3.
                        # Note that the register method actually adds multiple
                        # states. The only block that calls it multiple times
                        # is Chain in 1.16.2-pre1+ though, with everything else
                        # using only 1 varargs call.  (There also are no calls
                        # to the superclass' register states method.)
                        if properties is None:
                            properties = stack.pop()
                        else:
                            properties.extend(stack.pop())
                        assert desc.returns.name == blockstatecontainer
                        # Don't pop anything, since we'd just pop and re-add the builder
                    elif desc.returns.name == "boolean":
                        # Case 2.
                        properties = [None, None]
                        stack.pop() # Target object
                        # XXX shouldn't something be returned here?
                    else:
                        # Case 1.
                        # Assume that the return type is the base interface
                        # for properties
                        stack.pop() # Target object
                        stack.append(None)
                elif ins == "ifeq":
                    assert if_pos is None
                    if_pos = ins.pos + ins.operands[0].value
                elif ins == "pop":
                    stack.pop()
                elif ins == "areturn":
                    assert not is_18w19a # In 18w19a we don't return a container
                    if if_pos == None:
                        assert properties == None
                        properties = stack.pop()
                    else:
                        assert isinstance(properties, list)
                        index = 0 if ins.pos < if_pos else 1
                        assert properties[index] == None
                        properties[index] = stack.pop()
                elif ins == "return":
                    assert is_18w19a # We only return void in 18w19a
                elif ins == "aload":
                    assert ins.operands[0].value == 0 # Should be aload_0 (this)
                    stack.append(object())
                elif verbose: