"""
DeCliff filter contributed by Minecraft Forums user "DrRomz"

Originally posted here:
http://www.minecraftforum.net/topic/13807-mcedit-minecraft-world-editor-compatible-with-mc-beta-18/page__st__3940__p__7648793#entry7648793
"""
from numpy import zeros, array
import itertools
from pymclevel import alphaMaterials

am = alphaMaterials

# Consider below materials when determining terrain height
blocks = [
    am.Stone,
    am.Grass,
    am.Dirt,
    am.Bedrock,
    am.Sand,
    am.Sandstone,
    am.Clay,
    am.Gravel,
    am.GoldOre,
    am.IronOre,
    am.CoalOre,
    am.LapisLazuliOre,
    am.DiamondOre,
    am.RedstoneOre,
    am.RedstoneOreGlowing,
    am.Netherrack,
    am.SoulSand,
    am.Glowstone
]
terrainBlocktypes = [b.ID for b in blocks]
terrainBlockmask = zeros((256,), dtype='bool')

# Truth table used to calculate terrain height
# trees, leaves, etc. sit on top of terrain
terrainBlockmask[terrainBlocktypes] = True

inputs = (
    # Option to limit change to raise_cliff_floor / lower_cliff_top
    # Default is to adjust both and meet somewhere in the middle
    ("Raise/Lower", ("Both", "Lower Only", "Raise Only")),
)


#
# Calculate the maximum adjustment that can be made from
# cliff_pos in direction dir (-1/1) keeping terain at most
# maxstep blocks away from previous column
def maxadj(heightmap, slice_no, cliff_pos, dir, pushup, maxstep, slice_width):
    ret = 0
    if dir < 0:
        if cliff_pos < 2:
            return 0
        end = 0
    else:
        if cliff_pos > slice_width - 2:
            return 0
        end = slice_width - 1

    for cur_pos in range(cliff_pos, end, dir):
        if pushup:
            ret = ret + \
                  max([0, maxstep - dir * heightmap[slice_no, cur_pos] +
                       dir * heightmap[slice_no, cur_pos + dir]])
        else:
            ret = ret + \
                  min([0, -maxstep + dir * heightmap[slice_no, cur_pos] -
                       dir * heightmap[slice_no, cur_pos + dir]])

    return ret


#
# Raise/lower column at cliff face by adj and decrement change as we move away
# from the face. Each level will be at most maxstep blocks from those beside it.
#
# This function dosn't actually change anything, but just sets array 'new'
# with the desired height.
def adjheight(orig, new, slice_no, cliff_pos, dir, adj, can_adj, maxstep, slice_width):
    cur_adj = adj
    prev = 0
    done_adj = 0

    if dir < 0:
        end = 1
    else:
        end = slice_width - 1

    if adj == 0 or can_adj == 0:
        for cur_pos in range(cliff_pos, end, dir):
            new[slice_no, cur_pos] = orig[slice_no, cur_pos]
    else:

        for cur_pos in range(cliff_pos, end, dir):
            if adj > 0:
                done_adj = done_adj + \
                           max([0, maxstep - orig[slice_no, cur_pos] +
                                orig[slice_no, cur_pos + dir]])

                if orig[slice_no, cur_pos] - \
                        orig[slice_no, cur_pos + dir] > 0:
                    cur_adj = max([0, cur_adj - orig[slice_no, cur_pos] +
                                   orig[slice_no, cur_pos + dir]])
                    prev = adj - cur_adj
            else:
                done_adj = done_adj + \
                           min([0, -maxstep +
                                orig[slice_no, cur_pos] -
                                orig[slice_no, cur_pos + dir]])
                if orig[slice_no, cur_pos] - \
                        orig[slice_no, cur_pos + dir] > 0:
                    cur_adj = min([0, cur_adj + orig[slice_no, cur_pos] - orig[slice_no, cur_pos + dir]])
                    prev = adj - cur_adj
            new[slice_no, cur_pos] = max([0, orig[slice_no, cur_pos] + cur_adj])
            if cur_adj != 0 and \
                            abs(prev) < abs(int(adj * done_adj / can_adj)):
                cur_adj += prev - int(adj * done_adj / can_adj)
                prev = int(adj * done_adj / can_adj)

    new[slice_no, end] = orig[slice_no, end]


def perform(level, box, options):
    if box.volume > 16000000:
        raise ValueError("Volume too big for this filter method!")

    RLOption = options["Raise/Lower"]
    schema = level.extractSchematic(box)
    schema.removeEntitiesInBox(schema.bounds)
    schema.removeTileEntitiesInBox(schema.bounds)

    terrainBlocks = terrainBlockmask[schema.Blocks]

    coords = terrainBlocks.nonzero()

    # Swap values around so long edge of selected rectangle is first
    # - the long edge is assumed to run parallel to the cliff face
    # and we want to process slices perpendicular to the face
    #  heightmap will have x,z (or z,x) index with highest ground level
    if schema.Width > schema.Length:
        heightmap = zeros((schema.Width, schema.Length), dtype='float32')
        heightmap[coords[0], coords[1]] = coords[2]
        newHeightmap = zeros((schema.Width, schema.Length), dtype='uint16')
        slice_count = schema.Width
        slice_width = schema.Length
    else:
        heightmap = zeros((schema.Length, schema.Width), dtype='float32')
        heightmap[coords[1], coords[0]] = coords[2]
        newHeightmap = zeros((schema.Length, schema.Width), dtype='uint16')
        slice_count = schema.Length
        slice_width = schema.Width

    nonTerrainBlocks = ~terrainBlocks
    nonTerrainBlocks &= schema.Blocks != 0

    for slice_no in range(0, slice_count):

        cliff_height = 0
        # determine pos and height of cliff in this slice
        for cur_pos in range(0, slice_width - 1):
            if abs(heightmap[slice_no, cur_pos] -
                    heightmap[slice_no, cur_pos + 1]) > abs(cliff_height):
                cliff_height = \
                    heightmap[slice_no, cur_pos] - \
                    heightmap[slice_no, cur_pos + 1]
                cliff_pos = cur_pos

        if abs(cliff_height) < 2:
            # nothing to adjust - just copy heightmap to newHightmap
            adjheight(heightmap, newHeightmap, slice_no, 0, 1, 0, 1, 1, slice_width)
            continue

        # Try to keep adjusted columns within 1 column of their neighbours
        # but ramp up to 4 blocks up/down on each column when needed
        for max_step in range(1, 4):

            can_left = maxadj(heightmap, slice_no, cliff_pos, -1, cliff_height < 0, max_step, slice_width)
            can_right = maxadj(heightmap, slice_no, cliff_pos + 1, 1, cliff_height > 0, max_step, slice_width)

            if can_right < 0 and RLOption == "Raise Only":
                can_right = 0
            if can_right > 0 and RLOption == "Lower Only":
                can_right = 0
            if can_left < 0 and RLOption == "Raise Only":
                can_left = 0
            if can_left > 0 and RLOption == "Lower Only":
                can_left = 0

            if 0 > cliff_height > can_right - can_left:
                if abs(can_left) > abs(can_right):
                    adj_left = -1 * (cliff_height - max([int(cliff_height / 2), can_right]))
                    adj_right = cliff_height + adj_left
                else:
                    adj_right = cliff_height - max([int(cliff_height / 2), -can_left])
                    adj_left = -1 * (cliff_height - adj_right + 1)
            else:
                if 0 < cliff_height < can_right - can_left:
                    if abs(can_left) > abs(can_right):
                        adj_left = -1 * (cliff_height - min([int(cliff_height / 2), can_right]))
                        adj_right = cliff_height + adj_left
                    else:
                        adj_right = cliff_height - min([int(cliff_height / 2), -can_left]) - 1
                        adj_left = -1 * (cliff_height - adj_right)
                else:
                    adj_right = 0
                    adj_left = 0
                    continue
            break

        adjheight(heightmap, newHeightmap, slice_no, cliff_pos, -1, adj_left, can_left, max_step, slice_width)
        adjheight(heightmap, newHeightmap, slice_no, cliff_pos + 1, 1, adj_right, can_right, max_step, slice_width)

    # OK, newHeightMap has new height for each column
    # so it's just a matter of moving everything up/down
    for x, z in itertools.product(xrange(1, schema.Width - 1), xrange(1, schema.Length - 1)):

        if schema.Width > schema.Length:
            oh = heightmap[x, z]
            nh = newHeightmap[x, z]
        else:
            oh = heightmap[z, x]
            nh = newHeightmap[z, x]

        delta = nh - oh

        column = array(schema.Blocks[x, z])
        # Keep bottom 5 blocks, so we don't loose bedrock
        keep = min([5, nh])

        Waterdepth = 0
        # Detect Water on top
        if column[oh + 1:oh + 2] == am.Water.ID or \
                        column[oh + 1:oh + 2] == am.Ice.ID:
            for cur_pos in range(int(oh) + 1, schema.Height):
                if column[cur_pos:cur_pos + 1] != am.Water.ID and \
                                column[cur_pos:cur_pos + 1] != am.Ice.ID:
                    break
                Waterdepth += 1

        if delta == 0:
            column[oh:] = schema.Blocks[x, z, oh:]

        if delta < 0:
            # Moving column down
            column[keep:delta] = schema.Blocks[x, z, keep - delta:]
            column[delta:] = am.Air.ID
            if Waterdepth > 0:
                # Avoid steping small lakes, etc on cliff top
                # replace with dirt 'n grass
                column[nh:nh + 1] = am.Grass.ID
                column[nh + 1:nh + 1 + delta] = am.Air.ID
        if delta > 0:
            # Moving column up
            column[keep + delta:] = schema.Blocks[x, z, keep:-delta]
            # Put stone in gap at the bottom
            column[keep:keep + delta] = am.Stone.ID

            if Waterdepth > 0:
                if Waterdepth > delta:
                    # Retain Ice
                    if column[nh + Waterdepth:nh + Waterdepth + 1] == am.Ice.ID:
                        column[nh + Waterdepth - delta:nh + 1 + Waterdepth - delta] = \
                            am.Ice.ID
                    column[nh + 1 + Waterdepth - delta:nh + 1 + Waterdepth] = am.Air.ID
                else:
                    if Waterdepth < delta - 2:
                        column[nh:nh + 1] = am.Grass.ID
                        column[nh + 1:nh + 1 + Waterdepth] = am.Air.ID
                    else:
                        # Beach at the edge
                        column[nh - 4:nh - 2] = am.Sandstone.ID
                        column[nh - 2:nh + 1] = am.Sand.ID
                        column[nh + 1:nh + 1 + Waterdepth] = am.Air.ID

        schema.Blocks[x, z] = column

    level.copyBlocksFrom(schema, schema.bounds, box.origin)
