from __future__ import division

from itertools import product
from math import cos, pi, sin, sqrt
from random import choice, random, randint

from twisted.internet.defer import inlineCallbacks, returnValue
from zope.interface import Interface, implements

from bravo.blocks import blocks

PHI = (sqrt(5) - 1) * 0.5
IPHI = (sqrt(5) + 1) * 0.5
"""
Phi and inverse phi constants.
"""

# add lights in the middle of foliage clusters
# for those huge trees that get so dark underneath
# or for enchanted forests that should glow and stuff
# Only works if SHAPE is "round" or "cone" or "procedural"
# 0 makes just normal trees
# 1 adds one light inside the foliage clusters for a bit of light
# 2 adds two lights around the base of each cluster, for more light
# 4 adds lights all around the base of each cluster for lots of light
LIGHTTREE = 0

@inlineCallbacks
def dist_to_mat(cord, vec, matidxlist, world, invert=False, limit=None):
    """
    Find the distance from the given coordinates to any of a set of blocks
    along a certain vector.
    """

    curcord = [i + .5 for i in cord]
    iterations = 0
    on_map = True
    while on_map:
        x = int(curcord[0])
        y = int(curcord[1])
        z = int(curcord[2])
        if not 0 <= y < 128:
            break
        block = yield world.get_block((x, y, z))

        if block in matidxlist and not invert:
            break
        elif block not in matidxlist and invert:
            break
        else:
            curcord = [curcord[i] + vec[i] for i in range(3)]
            iterations += 1
        if limit and iterations > limit:
            break
    returnValue(iterations)

class ITree(Interface):
    """
    An ideal Platonic tree.

    Trees usually are made of some sort of wood, and are adorned with leaves.
    These trees also may have lanterns hanging in their branches.
    """

    def prepare(world):
        """
        Do any post-__init__() setup.
        """

    def make_trunk(world):
        """
        Write a trunk to the world.
        """

    def make_foliage(world):
        """
        Write foliage (leaves, lanterns) to the world.
        """

class Tree(object):
    """
    Set up the interface for tree objects.  Designed for subclassing.
    """

    implements(ITree)

    def __init__(self, pos, height=None):
        if height is None:
            self.height = randint(4, 7)
        else:
            self.height = height

        self.pos = pos

    def prepare(self, world):
        pass

    def make_trunk(self, world):
        pass

    def make_foliage(self, world):
        pass

class StickTree(Tree):
    """
    A large stick or log.

    Subclass this to build trees which only require a single-log trunk.
    """

    def make_trunk(self, world):
        x, y, z = self.pos
        for i in xrange(self.height):
            world.set_block((x, y, z), blocks["log"].slot)
            y += 1

class NormalTree(StickTree):
    """
    A Notchy tree.

    This tree will be a single bulb of foliage above a single width trunk.
    The shape is very similar to the default Minecraft tree.
    """

    def make_foliage(self, world):
        topy = self.pos[1] + self.height - 1
        start = topy - 2
        end = topy + 2
        for y in xrange(start, end):
            if y > start + 1:
                rad = 1
            else:
                rad = 2
            for xoff, zoff in product(xrange(-rad, rad + 1), repeat=2):
                if (random() > PHI and abs(xoff) == abs(zoff) == rad or
                       xoff == zoff == 0):
                    continue

                x = self.pos[0] + xoff
                z = self.pos[2] + zoff

                world.set_block((x, y, z), blocks["leaves"].slot)

class BambooTree(StickTree):
    """
    A bamboo-like tree.

    Bamboo foliage is sparse and always adjacent to the trunk.
    """

    def make_foliage(self, world):
        start = self.pos[1]
        end = start + self.height + 1
        for y in xrange(start, end):
            for i in (0, 1):
                xoff = choice([-1, 1])
                zoff = choice([-1, 1])
                x = self.pos[0] + xoff
                z = self.pos[2] + zoff
                world.set_block((x, y, z), blocks["leaves"].slot)

class PalmTree(StickTree):
    """
    A traditional palm tree.

    This tree has four tufts of foliage at the top of the trunk.  No coconuts,
    though.
    """

    def make_foliage(self, world):
        y = self.pos[1] + self.height
        for xoff, zoff in product(xrange(-2, 3), repeat=2):
            if abs(xoff) == abs(zoff):
                x = self.pos[0] + xoff
                z = self.pos[2] + zoff
                world.set_block((x, y, z), blocks["leaves"].slot)

class ProceduralTree(Tree):
    """
    Base class for larger, complex, procedurally generated trees.

    This tree type has roots, a trunk, branches all of varying width, and many
    foliage clusters.

    This class needs to be subclassed to be useful. Specifically,
    foliage_shape must be set.  Subclass 'prepare' and 'shapefunc' to make
    different shaped trees.
    """

    def cross_section(self, center, radius, diraxis, matidx, world):
        """Create a round section of type matidx in world.

        Passed values:
        center = [x,y,z] for the coordinates of the center block
        radius = <number> as the radius of the section.  May be a float or int.
        diraxis: The list index for the axis to make the section
        perpendicular to.  0 indicates the x axis, 1 the y, 2 the z.  The
        section will extend along the other two axies.
        matidx = <int> the integer value to make the section out of.
        world = the array generated by make_mcmap
        matdata = <int> the integer value to make the block data value.
        """

        rad = int(radius + PHI)
        if rad <= 0:
            return None
        secidx1 = (diraxis - 1) % 3
        secidx2 = (1 + diraxis) % 3
        coord = [0] * 3
        for off1, off2 in product(xrange(-rad, rad + 1), repeat=2):
            thisdist = sqrt((abs(off1) + .5)**2 + (abs(off2) + .5)**2)
            if thisdist > radius:
                continue
            pri = center[diraxis]
            sec1 = center[secidx1] + off1
            sec2 = center[secidx2] + off2
            coord[diraxis] = pri
            coord[secidx1] = sec1
            coord[secidx2] = sec2
            world.set_block(coord, matidx)

    def shapefunc(self, y):
        """
        Obtain a radius for the given height.

        Subclass this method to customize tree design.

        If None is returned, no foliage cluster will be created.

        :returns: radius, or None
        """

        if random() < 100 / ((self.height)**2) and y < self.trunkheight:
            return self.height * .12
        return None

    def foliage_cluster(self, center, world):
        """
        Generate a round cluster of foliage at the location center.

        The shape of the cluster is defined by the list self.foliage_shape.
        This list must be set in a subclass of ProceduralTree.
        """

        x = center[0]
        y = center[1]
        z = center[2]
        for i in self.foliage_shape:
            self.cross_section([x, y, z], i, 1, blocks["leaves"].slot, world)
            y += 1

    def taperedcylinder(self, start, end, startsize, endsize, world, blockdata):
        """
        Create a tapered cylinder in world.

        start and end are the beginning and ending coordinates of form [x,y,z].
        startsize and endsize are the beginning and ending radius.
        The material of the cylinder is WOODMAT.
        """

        # delta is the coordinate vector for the difference between
        # start and end.
        delta = [int(e - s) for e, s in zip(end, start)]

        # primidx is the index (0,1,or 2 for x,y,z) for the coordinate
        # which has the largest overall delta.
        maxdist = max(delta, key=abs)
        if maxdist == 0:
            return None
        primidx = delta.index(maxdist)

        # secidx1 and secidx2 are the remaining indices out of [0,1,2].
        secidx1 = (primidx - 1) % 3
        secidx2 = (1 + primidx) % 3

        # primsign is the digit 1 or -1 depending on whether the limb is headed
        # along the positive or negative primidx axis.
        primsign = cmp(delta[primidx], 0) or 1

        # secdelta1 and ...2 are the amount the associated values change
        # for every step along the prime axis.
        secdelta1 = delta[secidx1]
        secfac1 = float(secdelta1)/delta[primidx]
        secdelta2 = delta[secidx2]
        secfac2 = float(secdelta2)/delta[primidx]
        # Initialize coord.  These values could be anything, since
        # they are overwritten.
        coord = [0] * 3
        # Loop through each crossection along the primary axis,
        # from start to end.
        endoffset = delta[primidx] + primsign
        for primoffset in xrange(0, endoffset, primsign):
            primloc = start[primidx] + primoffset
            secloc1 = int(start[secidx1] + primoffset*secfac1)
            secloc2 = int(start[secidx2] + primoffset*secfac2)
            coord[primidx] = primloc
            coord[secidx1] = secloc1
            coord[secidx2] = secloc2
            primdist = abs(delta[primidx])
            radius = endsize + (startsize-endsize) * abs(delta[primidx]
                                - primoffset) / primdist
            self.cross_section(coord, radius, primidx, blockdata, world)

    def make_foliage(self, world):
        """
        Generate the foliage for the tree in world.

        Also place lanterns.
        """

        foliage_coords = self.foliage_cords
        for coord in foliage_coords:
            self.foliage_cluster(coord,world)
        for x, y, z in foliage_coords:
            world.set_block((x, y, z), blocks["log"].slot)
            if LIGHTTREE == 1:
                world.set_block((x, y + 1, z), blocks["lightstone"].slot)
            elif LIGHTTREE in [2,4]:
                world.set_block((x + 1, y, z), blocks["lightstone"].slot)
                world.set_block((x - 1, y, z), blocks["lightstone"].slot)
                if LIGHTTREE == 4:
                    world.set_block((x, y, z + 1), blocks["lightstone"].slot)
                    world.set_block((x, y, z - 1), blocks["lightstone"].slot)

    def make_branches(self, world):
        """Generate the branches and enter them in world.
        """
        treeposition = self.pos
        height = self.height
        topy = treeposition[1] + int(self.trunkheight + 0.5)
        # endrad is the base radius of the branches at the trunk
        endrad = max(self.trunkradius * (1 - self.trunkheight/height), 1)
        for coord in self.foliage_cords:
            dist = (sqrt(float(coord[0]-treeposition[0])**2 +
                            float(coord[2]-treeposition[2])**2))
            ydist = coord[1]-treeposition[1]
            # value is a magic number that weights the probability
            # of generating branches properly so that
            # you get enough on small trees, but not too many
            # on larger trees.
            # Very difficult to get right... do not touch!
            value = (self.branchdensity * 220 * height)/((ydist + dist) ** 3)
            if value < random():
                continue

            posy = coord[1]
            slope = self.branchslope + (0.5 - random()) * .16
            if coord[1] - dist*slope > topy:
                # Another random rejection, for branches between
                # the top of the trunk and the crown of the tree
                threshhold = 1 / height
                if random() < threshhold:
                    continue
                branchy = topy
                basesize = endrad
            else:
                branchy = posy-dist*slope
                basesize = (endrad + (self.trunkradius-endrad) *
                         (topy - branchy) / self.trunkheight)
            startsize = (basesize * (1 + random()) * PHI *
                         (dist/height)**PHI)
            if startsize < 1.0:
                startsize = 1.0
            rndr = sqrt(random()) * basesize * PHI
            rndang = random() * 2 * pi
            rndx = int(rndr*sin(rndang) + 0.5)
            rndz = int(rndr*cos(rndang) + 0.5)
            startcoord = [treeposition[0]+rndx,
                          int(branchy),
                          treeposition[2]+rndz]
            endsize = 1.0
            self.taperedcylinder(startcoord, coord, startsize, endsize, world,
                blocks["log"].slot)

    def make_trunk(self, world):
        """
        Make the trunk, roots, buttresses, branches, etc.
        """

        # In this method, x and z are the position of the trunk.
        x, starty, z = self.pos
        midy = starty + int(self.trunkheight / (PHI + 1))
        topy = starty + int(self.trunkheight + 0.5)
        end_size_factor = self.trunkheight / self.height
        endrad = max(self.trunkradius * (1 - end_size_factor), 1)
        midrad = max(self.trunkradius * (1 - end_size_factor * .5), endrad)

        # Make the lower and upper sections of the trunk.
        self.taperedcylinder([x,starty,z], [x,midy,z], self.trunkradius,
            midrad, world, blocks["log"].slot)
        self.taperedcylinder([x,midy,z], [x,topy,z], midrad, endrad, world,
            blocks["log"].slot)

        #Make the branches.
        self.make_branches(world)

    def prepare(self, world):
        """
        Initialize the internal values for the Tree object.

        Primarily, sets up the foliage cluster locations.
        """

        treeposition = self.pos
        self.trunkradius = PHI * sqrt(self.height)
        if self.trunkradius < 1:
            self.trunkradius = 1
        self.trunkheight = self.height
        yend = int(treeposition[1] + self.height)
        self.branchdensity = 1.0
        foliage_coords = []
        ystart = treeposition[1]
        num_of_clusters_per_y = int(1.5 + (self.height / 19)**2)
        if num_of_clusters_per_y < 1:
            num_of_clusters_per_y = 1
        # make sure we don't spend too much time off the top of the map
        if yend > 127:
            yend = 127
        if ystart > 127:
            ystart = 127
        for y in xrange(yend, ystart, -1):
            for i in xrange(num_of_clusters_per_y):
                shapefac = self.shapefunc(y - ystart)
                if shapefac is None:
                    continue
                r = (sqrt(random()) + .328)*shapefac

                theta = random()*2*pi
                x = int(r*sin(theta)) + treeposition[0]
                z = int(r*cos(theta)) + treeposition[2]

                foliage_coords += [[x,y,z]]

        self.foliage_cords = foliage_coords


class RoundTree(ProceduralTree):
    """
    A rounded deciduous tree.
    """

    branchslope = 1 / (PHI + 1)
    foliage_shape = [2, 3, 3, 2.5, 1.6]

    def prepare(self, world):
        ProceduralTree.prepare(self, world)
        self.trunkradius *= 0.8
        self.trunkheight *= 0.7

    def shapefunc(self, y):
        twigs = ProceduralTree.shapefunc(self, y)
        if twigs is not None:
            return twigs
        if y < self.height * (.282 + .1 * sqrt(random())) :
            return None
        radius = self.height / 2
        adj = self.height / 2 - y
        if adj == 0 :
            dist = radius
        elif abs(adj) >= radius:
            dist = 0
        else:
            dist = sqrt((radius**2) - (adj**2))
        dist *= PHI
        return dist


class ConeTree(ProceduralTree):
    """
    A conifer.
    """

    branchslope = 0.15
    foliage_shape = [3, 2.6, 2, 1]

    def prepare(self, world):
        ProceduralTree.prepare(self, world)
        self.trunkradius *= 0.5

    def shapefunc(self, y):
        twigs = ProceduralTree.shapefunc(self, y)
        if twigs is not None:
            return twigs
        if y < self.height * (.25 + .05 * sqrt(random())):
            return None

        # Radius.
        return max((self.height - y) / (PHI + 1), 0)

class RainforestTree(ProceduralTree):
    """
    A big rainforest tree.
    """

    branchslope = 1
    foliage_shape = [3.4, 2.6]

    def prepare(self, world):
        ProceduralTree.prepare(self, world)
        self.trunkradius /= PHI + 1
        self.trunkheight *= .9

    def shapefunc(self, y):
        # Bottom 4/5 of the tree is probably branch-free.
        if y < self.height * 0.8:
            if self.edgeheight < self.height:
                twigs = ProceduralTree.shapefunc(self,y)
                if twigs is not None and random() < 0.07:
                    return twigs
            return None
        else:
            width = self.height * 1 / (IPHI + 1)
            topdist = (self.height - y) / (self.height * 0.2)
            dist = width * (PHI + topdist) * (PHI + random()) * 1 / (IPHI + 1)
            return dist

class MangroveTree(RoundTree):
    """
    A mangrove tree.

    Like the round deciduous tree, but bigger, taller, and generally more
    awesome.
    """

    branchslope = 1

    def prepare(self, world):
        RoundTree.prepare(self, world)
        self.trunkradius *= PHI

    def shapefunc(self, y):
        val = RoundTree.shapefunc(self, y)
        if val is not None:
            val *= IPHI
        return val

    @inlineCallbacks
    def make_roots(self, rootbases, world):
        """generate the roots and enter them in world.

        rootbases = [[x,z,base_radius], ...] and is the list of locations
        the roots can originate from, and the size of that location.
        """
        treeposition = self.pos
        height = self.height
        for coord in self.foliage_cords:
            # First, set the threshhold for randomly selecting this
            # coordinate for root creation.
            dist = (sqrt(float(coord[0]-treeposition[0])**2 +
                            float(coord[2]-treeposition[2])**2))
            ydist = coord[1]-treeposition[1]
            value = (self.branchdensity * 220 * height)/((ydist + dist) ** 3)
            # Randomly skip roots, based on the above threshold
            if value < random():
                continue
            # initialize the internal variables from a selection of
            # starting locations.
            rootbase = choice(rootbases)
            rootx = rootbase[0]
            rootz = rootbase[1]
            rootbaseradius = rootbase[2]
            # Offset the root origin location by a random amount
            # (radialy) from the starting location.
            rndr = sqrt(random()) * rootbaseradius * PHI
            rndang = random()*2*pi
            rndx = int(rndr*sin(rndang) + 0.5)
            rndz = int(rndr*cos(rndang) + 0.5)
            rndy = int(random()*rootbaseradius*0.5)
            startcoord = [rootx+rndx,treeposition[1]+rndy,rootz+rndz]
            # offset is the distance from the root base to the root tip.
            offset = [startcoord[i]-coord[i] for i in xrange(3)]
            # If this is a mangrove tree, make the roots longer.
            offset = [int(val * IPHI - 1.5) for val in offset]
            rootstartsize = (rootbaseradius * IPHI * abs(offset[1])/
                             (height * IPHI))
            rootstartsize = max(rootstartsize, 1.0)

    def make_trunk(self, world):
        """
        Make the trunk, roots, buttresses, branches, etc.
        """

        height = self.height
        trunkheight = self.trunkheight
        trunkradius = self.trunkradius
        treeposition = self.pos
        starty = treeposition[1]
        midy = treeposition[1]+int(trunkheight * 1 / (PHI + 1))
        topy = treeposition[1]+int(trunkheight + 0.5)
        # In this method, x and z are the position of the trunk.
        x = treeposition[0]
        z = treeposition[2]
        end_size_factor = trunkheight/height
        endrad = max(trunkradius * (1 - end_size_factor), 1)
        midrad = max(trunkradius * (1 - end_size_factor * .5), endrad)

        # The start radius of the trunk should be a little smaller if we
        # are using root buttresses.
        startrad = trunkradius * .8
        # rootbases is used later in self.makeroots(...) as
        # starting locations for the roots.
        rootbases = [[x,z,startrad]]
        buttress_radius = trunkradius * 0.382
        # posradius is how far the root buttresses should be offset
        # from the trunk.
        posradius = trunkradius
        # In mangroves, the root buttresses are much more extended.
        posradius = posradius * (IPHI + 1)
        num_of_buttresses = int(sqrt(trunkradius) + 3.5)
        for i in xrange(num_of_buttresses):
            rndang = random()*2*pi
            thisposradius = posradius * (0.9 + random()*.2)
            # thisx and thisz are the x and z position for the base of
            # the root buttress.
            thisx = x + int(thisposradius * sin(rndang))
            thisz = z + int(thisposradius * cos(rndang))
            # thisbuttressradius is the radius of the buttress.
            # Currently, root buttresses do not taper.
            thisbuttressradius = max(buttress_radius * (PHI + random()),
                1)
            # Make the root buttress.
            self.taperedcylinder([thisx, starty, thisz], [x, midy, z],
                thisbuttressradius, thisbuttressradius, world,
                blocks["log"].slot)
            # Add this root buttress as a possible location at
            # which roots can spawn.
            rootbases += [[thisx,thisz,thisbuttressradius]]

        # Make the lower and upper sections of the trunk.
        self.taperedcylinder([x,starty,z], [x,midy,z], startrad, midrad,
            world, blocks["log"].slot)
        self.taperedcylinder([x,midy,z], [x,topy,z], midrad, endrad, world,
            blocks["log"].slot)

        #Make the branches
        self.make_branches(world)
