#
# Code under the MIT license by Alexander Pruss
#

import mcpi.minecraft as minecraft
import mcpi.block as block
from mcpi.block import *
from mcpi.entity import *
import numbers
import copy
import time
from drawing import *
from operator import itemgetter
from math import *
import numbers



class Turtle:
    QUICK_SAVE = ( 'block', 'width', 'pen', 'matrix', 'nib', 'fan' )

    def __init__(self,mc=None):
        if mc:
             self.mc = mc
        else:
             self.mc = minecraft.Minecraft()
        self.block = GOLD_BLOCK
        self.width = 1
        self.pen = True
        self.directionIn()
        self.positionIn()
        self.delayTime = 0.05
        self.nib = [(0,0,0)]
        self.turtleType = PLAYER
        self.turtleId = None
        self.fan = None
        self.stack = []
        self.setEntityCommands()

    def setEntityCommands(self):
        if self.turtleId == None:
            self.setPos = self.mc.player.setPos
            self.setPitch = self.mc.player.setPitch
            self.setRotation = self.mc.player.setRotation
        else:
            self.setPos = lambda *pos: self.mc.entity.setPos(self.turtleId,*pos)
            self.setPitch = lambda angle: self.mc.entity.setPitch(self.turtleId,angle)
            self.setRotation = lambda angle: self.mc.entity.setRotation(self.turtleId,angle)

    def save(self):
        dict = {}
        for attribute in Turtle.QUICK_SAVE:
            dict[attribute] = copy.deepcopy(getattr(self, attribute))
        dict['position'] = (self.position.x, self.position.y, self.position.z)
        return dict

    def restore(self, dict):
        for attribute in Turtle.QUICK_SAVE:
            setattr(self, attribute, dict[attribute])
        p = dict['position']
        self.position = minecraft.Vec3(p[0], p[1], p[2])
        self.positionOut()
        self.directionOut()

    def push(self):
        """Save current drawing state to stack"""
        self.stack.append(self.save())

    def pop(self):
        """Restore current drawing state from stack"""
        self.restore(self.stack.pop())

    def turtle(self,turtleType):
        """Set turtle type. Use PLAYER for moving the player as the turtle and None for none"""
        if self.turtleType == turtleType:
            return
        if self.turtleType and self.turtleType != PLAYER:
            self.mc.removeEntity(self.turtleId)
        self.turtleType = turtleType
        if turtleType == PLAYER:
            self.turtleId = None
        elif turtleType:
            self.turtleId = self.mc.spawnEntity(turtleType,
                                                self.position.x,self.position.y,self.position.z,
                                                "{NoAI:1}")
        self.setEntityCommands()
        self.positionOut()
        self.directionOut()

    def follow(self): # deprecated
        self.turtle(PLAYER)
        
    def nofollow(self): # deprecated
        if self.turtleType == PLAYER:
            self.turtle(None)

    def penwidth(self,w):
        """Set pen stroke width (width:int)"""
        self.width = int(w)
        if self.width == 0:
            self.nib = []
        elif self.width == 1:
            self.nib = [(0,0,0)]
        elif self.width == 2:
            self.nib = []
            for x in range(-1,1):
                for y in range(0,2):
                    for z in range(-1,1):
                        self.nib.append((x,y,z))
        else:
            self.nib = []
            r2 = self.width * self.width / 4.
            for x in range(-self.width//2 - 1,self.width//2 + 1):
                for y in range(-self.width//2 - 1, self.width//2 + 1):
                    for z in range(-self.width//2 -1, self.width//2 + 1):
                        if x*x + y*y + z*z <= r2:
                            self.nib.append((x,y,z))

    def goto(self,x,y,z):
        """Teleport turtle to location (x:int, y:int, z:int)"""
        self.position.x = x
        self.position.y = y
        self.position.z = z
        self.positionOut()
        self.delay()

    def rollangle(self,angle):
        """Set roll angle of turtle (angle:float/int) in degrees: 0=up vector points up"""
        angles = self.getMinecraftAngles()
        m0 = matrixMultiply(yawMatrix(angles[0]), pitchMatrix(angles[1]))
        self.matrix = matrixMultiply(m0, rollMatrix(angle))

    def angles(self,compass=0,vertical=0,roll=0):
        """Set roll angle of turtle (compass, vertical, roll) in degrees"""
        self.matrix = makeMatrix(compass,vertical,roll)

    def verticalangle(self,angle):
        """Vertical angle of turtle (angle:float/int) in degrees: 0=horizontal, 90=directly up, -90=directly down"""
        angles = self.getMinecraftAngles();
        self.matrix = matrixMultiply(yawMatrix(angles[0]), pitchMatrix(angle))
        self.directionOut()

    def angle(self,angle):
        """Compass angle of turtle (angle:float/int) in degrees: 0=south, 90=west, 180=north, 270=west"""
        angles = self.getMinecraftAngles()
        self.matrix = matrixMultiply(yawMatrix(angle), pitchMatrix(angles[1]))
        self.directionOut()

    def penup(self):
        """Move without drawing"""
        self.pen = False

    def pendown(self):
        """Move with drawing"""
        self.pen = True

    def penblock(self, block):
        """Set material of pen block"""
        self.block = block

    def positionIn(self):
        pos = self.mc.player.getPos()
        self.position = minecraft.Vec3(int(round(pos.x)),int(round(pos.y)),int(round(pos.z)))

    def positionOut(self):
        if self.turtleType:
            self.setPos(self.position)

    def delay(self):
        if self.delayTime > 0:
            time.sleep(self.delayTime)

    def directionIn(self):
        rotation = self.mc.player.getRotation()
        pitch = 0 #self.mc.player.getPitch()
        self.matrix = matrixMultiply(yawMatrix(rotation), pitchMatrix(-pitch))

    def yaw(self,angleDegrees):
        self.matrix = matrixMultiply(self.matrix, yawMatrix(angleDegrees))
        self.directionOut()
        self.delay()

    def roll(self,angleDegrees):
        self.matrix = matrixMultiply(self.matrix, rollMatrix(angleDegrees))
        self.directionOut()
        self.delay()

    def pitch(self,angleDegrees):
        self.matrix = matrixMultiply(self.matrix, pitchMatrix(angleDegrees))
        self.directionOut()
        self.delay()

    def getHeading(self):
        return [self.matrix[0][2],self.matrix[1][2],self.matrix[2][2]]

    def getMinecraftAngles(self):
        heading = self.getHeading()

        if isinstance(heading[0], numbers.Integral) and isinstance(heading[1], numbers.Integral) and isinstance(heading[2], numbers.Integral):
            # the only way all coordinates of the heading can be integers is if we are
            # grid aligned

            # no need for square roots; could also use absolute value
            xz = abs(heading[0]) + abs(heading[2])
            if xz != 0:
                rotation = iatan2(-heading[0], heading[2])
            else:
                rotation = 0
            pitch = iatan2(-heading[1], xz)
        else:        
            xz = sqrt(heading[0]*heading[0] + heading[2]*heading[2])
            if xz >= 1e-9:
                rotation = atan2(-heading[0], heading[2]) * TO_DEGREES
            else:
                rotation = 0.
            pitch = atan2(-heading[1], xz) * TO_DEGREES
        return [rotation,pitch]

    def directionOut(self):
        if self.turtleType:
            heading = self.getHeading()
            xz = sqrt(heading[0]*heading[0] + heading[2]*heading[2])
            pitch = atan2(-heading[1], xz) * TO_DEGREES
            self.setPitch(pitch)
            if xz >= 1e-9:
                rotation = atan2(-heading[0], heading[2]) * TO_DEGREES
                self.setRotation(rotation)

    def pendelay(self, t):
        """Set pen delay in seconds (t: float)"""
        self.delayTime = t

    def left(self, angle):
        """Turn counterclockwise relative to compass heading"""
        self.right(-angle)

    def right(self, angle):
        """Turn clockwise relative to compass heading"""
        self.matrix = matrixMultiply(yawMatrix(angle), self.matrix)
        self.directionOut()
        self.delay()

    def up(self, angle):
        """Turn upwards (increase pitch)"""
        self.pitch(angle)

    def down(self, angle):
        """Turn downwards (decrease pitch)"""
        self.up(-angle)

    def go(self, distance):
        """Advance turtle, drawing as needed (distance: float)"""
#        pitch = self.pitch * pi/180.
#        rot = self.rotation * pi/180.
        # at pitch=0: rot=0 -> [0,0,1], rot=90 -> [-1,0,0]
#        dx = cos(-pitch) * sin(-rot)
#        dy = sin(-pitch)
#        dz = cos(-pitch) * cos(-rot)
        [dx,dy,dz] = self.getHeading()
        newX = self.position.x + dx * distance
        newY = self.position.y + dy * distance
        newZ = self.position.z + dz * distance
        self.drawLine(self.position.x, self.position.y, self.position.z,
                        newX, newY, newZ)
        self.position.x = newX
        self.position.y = newY
        self.position.z = newZ
        self.positionOut()
        self.delay()

    def back(self, distance):
        """Move turtle backwards, drawing as needed (distance: float), and keeping heading unchanged"""
#        pitch = self.pitch * pi/180.
#        rot = self.rotation * pi/180.
#        dx = - cos(-pitch) * sin(-rot)
#        dy = - sin(-pitch)
#        dz = - cos(-pitch) * cos(-rot)
        [dx,dy,dz] = self.getHeading()
        newX = self.position.x - dx * distance
        newY = self.position.y - dy * distance
        newZ = self.position.z - dz * distance
        self.drawLine(self.position.x, self.position.y, self.position.z,
                        newX, newY, newZ)
        self.position.x = newX
        self.position.y = newY
        self.position.z = newZ
        self.positionOut()
        self.delay()

    def startface(self):
        """Start drawing a convex polygon"""
        self.fan = (self.position.x,self.position.y,self.position.z)

    def endface(self):
        """Finish polygon"""
        self.fan = None

    def gridalign(self):
        """Align positions to grid"""
        self.position.x = int(round(self.position.x))
        self.position.y = int(round(self.position.y))
        self.position.z = int(round(self.position.z))

        if self.fan:
            self.fan = (int(round(self.fan[0])),int(round(self.fan[1])),int(round(self.fan[2])))

        bestDist = 2*9
        bestMatrix = makeMatrix(0,0,0)

        for compass in [0, 90, 180, 270]:
            for pitch in [0, 90, 180, 270]:
                for roll in [0, 90, 180, 270]:
                    m = makeMatrix(compass,pitch,roll)
                    dist = matrixDistanceSquared(self.matrix, m)
                    if dist < bestDist:
                        bestMatrix = m
                        bestDist = dist

        self.matrix = bestMatrix
        self.positionOut()
        self.directionOut()

    def drawLine(self, x1,y1,z1, x2,y2,z2):
        def drawPoint(p, fast=False):
            if self.pen:
                if self.width == 1 and not self.fan:
                    self.mc.setBlock(p[0],p[1],p[2],self.block)
                else:
                    for point in self.nib:
                        x0 = p[0]+point[0]
                        y0 = p[1]+point[1]
                        z0 = p[2]+point[2]
                        if (x0,y0,z0) not in done:
                            self.mc.setBlock(x0,y0,z0,self.block)
                            done.add((x0,y0,z0))

            if not fast and self.delayTime > 0:
                self.position.x = p[0]
                self.position.y = p[1]
                self.position.z = p[2]
                self.positionOut()
                self.delay()

        if not self.pen and self.delayTime == 0:
            return

        # dictinary to avoid duplicate drawing
        done = set()

        if self.pen and self.fan:
            if self.delayTime > 0:
                for a in getLine(x1,y1,z1, x2,y2,z2):
                    drawPoint(a)

            triangle = getTriangle(self.fan, (x1,y1,z1), (x2,y2,z2))
            for a in triangle:
                drawPoint(a, True)
        else:
            for a in getLine(x1,y1,z1, x2,y2,z2):
                drawPoint(a)


if __name__ == "__main__":
    t = Turtle()
    for i in range(7):
        t.back(80)
        t.right(180-180./7)
    t.turtle(None)
