#
# Code under the MIT license by Alexander Pruss
#
#
# mandelbulb.py [size [power [half]]]
#
# Options for half: south, north, east, west, up, down

from mc import *
import mcpi.settings as settings
import cmath
import time
import sys
from random import uniform

# setting this to 0 makes for more isolated blocks because the connecting filigree was
# missed; increasing slows down rendering
AVOID_ISOLATES = 12 if not settings.isPE else 0

ESCAPE = 250
if len(sys.argv) < 2:
    size = 100
else:
    size = int(sys.argv[1])

if len(sys.argv) < 3:
    power = 8
else:
    power = int(sys.argv[2])

if power == 8:
    fractalSize = 2.16
else:
    fractalSize = 4.

if len(sys.argv) < 4:
    half = None
else:
    half = sys.argv[3].lower()[0]

palette = list(reversed([WOOL_WHITE,HARDENED_CLAY_STAINED_WHITE,WOOL_PINK,WOOL_LIGHT_GRAY,WOOL_LIGHT_BLUE,WOOL_MAGENTA,WOOL_PURPLE,HARDENED_CLAY_STAINED_LIGHT_BLUE,HARDENED_CLAY_STAINED_LIGHT_GRAY,HARDENED_CLAY_STAINED_MAGENTA,HARDENED_CLAY_STAINED_PINK,HARDENED_CLAY_STAINED_RED,WOOL_RED,REDSTONE_BLOCK,HARDENED_CLAY_STAINED_ORANGE,WOOL_ORANGE,HARDENED_CLAY_STAINED_YELLOW,WOOL_YELLOW,WOOL_LIME,HARDENED_CLAY_STAINED_LIME,HARDENED_CLAY_STAINED_PURPLE,HARDENED_CLAY_STAINED_CYAN,WOOL_CYAN,WOOL_BLUE,HARDENED_CLAY_STAINED_BLUE,WOOL_GRAY,HARDENED_CLAY_STAINED_GREEN,WOOL_GREEN,HARDENED_CLAY_STAINED_BROWN,WOOL_BROWN,HARDENED_CLAY_STAINED_GRAY,WOOL_BLACK]));

def positions(pos,scale):
    yield pos
    for i in range(AVOID_ISOLATES):
        yield (uniform(pos[0]-0.5*scale,pos[0]+0.5*scale),
               uniform(pos[1]-0.5*scale,pos[1]+0.5*scale),
               uniform(pos[2]-0.5*scale,pos[2]+0.5*scale))

def calculate0(pos):
    x,z,y = pos[0],pos[1],pos[2]
    cx,cy,cz = x,y,z

    i = 0
    try:
        wentOut = 0
        while i<ESCAPE:
            r = sqrt(x*x+y*y+z*z)
            if r > 2:
               return -1
            if r > wentOut:
               wentOut = r
            theta = acos(z/r)
            phi = atan2(y,x)
            zr = r**power
            theta *= power
            phi *= power

            x = cx+zr*sin(theta)*cos(phi)
            y = cy+zr*sin(phi)*sin(theta)
            z = cz+zr*cos(theta)

            i = i + 1
        return wentOut
    except:
        return 0

def calculate(pos0,scale):
    for pos in positions(pos0,scale):
        r = calculate0(pos)
        if r >= 0:
            return r
    return r

#
# we could of course just do for x in range(0,size): for y in range(0,size): yield(x,y)
# but it will make users happier if we start at the player
#


def pollZoom():
    global lastHitEvent,lastHitPos
    events = mc.events.pollBlockHits()
    if len(events) == 0:
        return lastHitEvent != None
    lastHitEvent = events[-1]
    lastHitPos = mc.player.getPos()
    return True

def toBulb(centerMC,centerBulb,scale,x,y,z):
    return ((x - centerMC.x) * scale  + centerBulb[0],
                    (y - centerMC.y) * scale  + centerBulb[1],
                    (z - centerMC.z) * scale + centerBulb[2])

def draw():
    count = 0
    
    rangeX = list(range(cornerMC.x, cornerMC.x+size))
    rangeY = list(range(cornerMC.y, cornerMC.y+size))
    rangeZ = list(range(cornerMC.z, cornerMC.z+size))
    
    if not half is None:
        if half == 'w':
            rangeX = list(range(cornerMC.x, cornerMC.x+size/2))
        elif half == 'e':
            rangeX = list(range(cornerMC.x+size/2, cornerMC.x+size))
        elif half == 'n':
            rangeZ = list(range(cornerMC.z, cornerMC.z+size/2))
        elif half == 's':
            rangeZ = list(range(cornerMC.z+size/2, cornerMC.z+size))
        elif half == 'u':
            rangeY = list(range(cornerMC.y+size/2, cornerMC.y+size))
        elif half == 'd':
            rangeY = list(range(cornerMC.y, cornerMC.y+size/2))

    for mcX in rangeX:
        for mcY in rangeY:
            for mcZ in rangeZ:
                radius = calculate(toBulb(centerMC,centerBulb,scale,mcX,mcY,mcZ),scale)
                if radius < 0:
                    mc.setBlock(mcX,mcY,mcZ,AIR)
                else:
                    i = int(len(palette) / (fractalSize/2) * radius)
                    if i >= len(palette):
                        i = len(palette) - 1
                    mc.setBlock(mcX,mcY,mcZ,palette[i])
        if pollZoom():
            return
    mc.postToChat("Rendered")

if __name__=='__main__':
    mc = Minecraft()
    startPos = mc.player.getTilePos()
    cornerMC = startPos + Vec3(1,0,1)
    centerMC = cornerMC + Vec3(size/2,size/2,size/2)
    centerBulb = (0,0,0)
    initial = True
    scale    = fractalSize / size
    lastHitEvent = None

    while True:
        mc.player.setPos(startPos)
        mc.postToChat("Scale: "+str(fractalSize/size/scale))
        draw()
        if not initial:
            mc.player.setPos(centerMC)
        while not pollZoom():
            time.sleep(0.25)
        if ( lastHitEvent.pos.x < cornerMC.x or
             lastHitEvent.pos.x >= cornerMC.x + size or
             lastHitEvent.pos.y < cornerMC.y or
             lastHitEvent.pos.y >= cornerMC.y + size or
             lastHitEvent.pos.z < cornerMC.z or
             lastHitEvent.pos.z >= cornerMC.z + size ):
                mc.postToChat("resetting")
                centerBulb = (0,0,0)
                scale = fractalSize / size
                initial = True
        else:
                mc.postToChat("zooming")
                centerBulb = toBulb(centerMC,centerBulb,scale,lastHitPos.x,lastHitPos.y,lastHitPos.z)
                scale /= 8
                initial = False
        lastHitEvent = None
