import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *
from direct.task import Task
from direct.interval.IntervalGlobal import *
from math import *

from constants import *

MAX_SPIN = 2160
TURBO_SPIN = 4320
#4320
#2160

class Ball(DirectObject):
    def __init__(self):
        self.turnLeft = False
        self.turnRight = False
        self.brake = False
        self.turbo = False
        
        self.radius = 1.0
        self.elasticity = 0.4 # % of speed retained on bounce
        
        # how quickly does the actual velocity change to the
        # theoretical driven velocity?
        # 1.0 = 100% per second
        self.traction = 4.0
        self.lastNormal = Vec3(0,0,1) # last direction from ground
        
        self.turnRate = 90 # degrees per second
        
        self.tilt=0.0
        self.spinrate = 0.0
        self.oldSpinrateTime=0.0
        self.oldSpinTime=0.0
        self.velocity = Vec3(0,0,0)
        
        self.outerMechRate = 0
        self.innerMechRate = 0
        self.initSound()
        taskMgr.doMethodLater(.25,self.soundUpdateTask,"sound update")
        
        # moved to enable, called extrnally
        ##taskMgr.add(self.ballUpdateTask,"ball updater")
        
        #self.shell=loader.loadModel("Models/Test_Outer_Ball.egg")
        self.shell=loader.loadModel("BallEggs/OuterBall.egg")
        self.shell.setScale(MODEL_SCALE)
        #self.core=loader.loadModel("Models/Test_Inner_Ball.egg")
        self.core=loader.loadModel("BallEggs/InnerBall.egg")
        self.core.setScale(MODEL_SCALE)
        self.rollGimbal=render.attachNewNode("ball roll gimbal")
        self.coreRollGimbal=render.attachNewNode("ball core roll gimbal")
        self.root = render.attachNewNode("ball model root")
        self.shell.reparentTo(self.rollGimbal)
        self.core.reparentTo(self.coreRollGimbal)
        self.rollGimbal.reparentTo(self.root)
        self.coreRollGimbal.reparentTo(self.root)
        
        # to hopefully save time over doing a bunch of trig based on heading
        self.right = NodePath("ball right marker")
        self.right.reparentTo(self.root)
        self.right.setPos(1,0,0)
        self.forwards = NodePath("ball forwards marker")
        self.forwards.reparentTo(self.root)
        self.forwards.setPos(0,1,0)
        self.up = NodePath("ball up marker")
        self.up.reparentTo(self.root)
        self.up.setPos(0,0,1)
        
        self.root.setPos(0,5,5)
        self.root.setTransparency(TransparencyAttrib.MAlpha)
        self.grounded = False
        #taskMgr.add(self.setSpinTask,"model update task")
        
        self.maxAccel = 0.0 # peaks at ~5.4
        
    def initSound(self):
        self.sfx_steam = loader.loadSfx("sounds/steam.wav")
        self.sfx_steam.setVolume(0)
        self.sfx_steam.setLoop(True)
        self.sfx_steam.play()
        
        self.sfx_brake = loader.loadSfx("sounds/applyBrake.wav")
        self.sfx_brake.setVolume(.25)
        
        self.sfx_mechOuter = loader.loadSfx("sounds/outerMechanical.wav")
        self.sfx_mechOuter.setVolume(0.5)
        self.sfx_mechOuter.setPlayRate(0.0)
        self.sfx_mechOuter.setLoop(True)
        self.sfx_mechOuter.play()
        self.sfx_mechInner = loader.loadSfx("sounds/innerMechanical.wav")
        self.sfx_mechInner.setVolume(0.25)
        self.sfx_mechInner.setPlayRate(0.0)
        self.sfx_mechInner.setLoop(True)
        self.sfx_mechInner.play()
        
    def enable(self):
        self.tilt=0.0
        self.spinrate = 0.0
        self.oldSpinrateTime=0.0
        self.oldSpinTime=0.0
        self.velocity = Vec3(0,0,0)
        
        self.outerMechRate = 0
        self.innerMechRate = 0
        
        self.turnLeft = False
        self.turnRight = False
        self.brake = False
        self.turbo = False
        
        if not taskMgr.hasTaskNamed("ball updater"):
            taskMgr.add(self.ballUpdateTask,"ball updater")
    
    def disable(self):
        taskMgr.remove("ball updater")
        self.turnLeft = False
        self.turnRight = False
        self.brake = False
        self.turbo = False
        
        outerSpindownInt = LerpFunctionInterval(self.setOuterMechRate,duration=0.5, fromData=self.outerMechRate, toData=0.0)
        innerSpindownInt = LerpFunctionInterval(self.setInnerMechRate,duration=0.5, fromData=self.innerMechRate, toData=0.0)
        steamKillInt = LerpFunctionInterval(self.sfx_steam.setVolume,duration=0.5, fromData=self.sfx_steam.getVolume(), toData=0.0)
        combinedInt = Parallel(outerSpindownInt,innerSpindownInt,steamKillInt)
        combinedInt.start()
        
    def ballUpdateTask(self,task):
        t = task.time
        dt = t-self.oldSpinrateTime
        self.oldSpinrateTime=t
        decel = 1000
        minSpeed=0
        maxSpeed = MAX_SPIN
        if self.turbo:
            maxSpeed = TURBO_SPIN
        if self.brake:
            decel = 5000
            minSpeed=-maxSpeed/8.0
            if self.spinrate>minSpeed:
                self.spinrate=max(minSpeed,self.spinrate-dt*decel)
            #print "braking\n"
        elif self.spinrate!=0:
            if self.spinrate>0:
                self.spinrate=max(0,self.spinrate-dt*decel)
            else:
                self.spinrate=min(0,self.spinrate+dt*decel)
        self.spinrate = min(self.spinrate,maxSpeed)
        #print self.spinrate
        
        # spinrate is in degrees per second...
        # so divide by 360 for RPS
        rps = self.spinrate/360.0
        # theoretical velocity is circumference*RPS
        targetVel = PI*2*self.radius*rps
        #print targetVel,rps,self.velocity.length()
        
        # calculate the direction we should move based on heading
        # and ground angle
        
        # using the self.right nodepath to get ballRight is faster,
        # but may introduce innacuracy as the ball tilts.
        ballRight = self.right.getPos(render)-self.root.getPos(render)#Vec3(cos(radians(self.root.getH())),sin(radians(self.root.getH())),0) # ball's right-vector
        ballRight.normalize()
        ballMoveDir = self.lastNormal.cross(ballRight) # up x right = forward
        ballMoveDir.normalize()
        # cheap hack- decrease motive power the further normal and right get from 90 degrees off
        # as angle approaches 0 or 180, the abs term approaches 90, the division approaches 1 and directness approaches 0
        # as angle approaches 90, tyhe term approaches 0, division 0, and directness 1
        directness = 1.0-(abs(ballRight.angleDeg(self.lastNormal)-90)/90.0)
        
        if self.grounded:
            ## heading = self.root.getH()
            ## velX = -targetVel*sin(radians(heading))
            ## velY = targetVel*cos(radians(heading))
            ## difX = (velX - self.velocity.getX())*self.traction*dt
            ## difY = (velY - self.velocity.getY())*self.traction*dt
            ## self.velocity = self.velocity + Vec3(difX,difY,0)
            targetVelVec = ballMoveDir * targetVel
            dif = targetVelVec - self.velocity
            self.velocity = self.velocity + dif*self.traction*dt * directness
            
        
        if self.turnRight:
            self.tilt=min(35,self.tilt+30*dt)
            self.root.setH(self.root.getH()-self.turnRate*dt)
        else:
            if self.tilt>0:
                self.tilt=max(0,self.tilt-40*dt)
        if self.turnLeft:
            self.tilt=max(-35,self.tilt-30*dt)
            self.root.setH(self.root.getH()+self.turnRate*dt)
        else:
            if self.tilt<0:
                self.tilt=min(0,self.tilt+40*dt)
        
        if not self.grounded:
            self.velocity+=Vec3(0,0,-1)*GRAVITY*dt
            #print "not grounded", self.velocity
        else:
            gravVec = Vec3(0,0,-1)*GRAVITY*dt
            # after gravity is applied, the surface pushes back outwards until the ball is even with it
            normResponse = self.lastNormal*(gravVec.dot(self.lastNormal))*-1
            resultVec = gravVec+normResponse
            self.velocity+=resultVec
        self.root.setPos(self.root.getPos()+self.velocity*dt)
        
        self.root.setR(self.tilt)
        outerSpin = self.spinrate*dt
        self.rollGimbal.setP(self.rollGimbal.getP()-outerSpin)
        # base core speed is half of roll speed
        # left turn slows core to 0, right turn speeds to full
        tiltfactor = (self.tilt+35)/70.0 # result in 0-1 range
        innerSpin = self.spinrate*dt*tiltfactor
        self.coreRollGimbal.setP(self.coreRollGimbal.getP()+innerSpin)
        
        #print outerSpin, innerSpin
        self.outerMechRate = abs(outerSpin/10.0) # outerSpin peaks at ~44
        self.innerMechRate = abs(innerSpin/5.0) # inner peaks at ~24
        steamVolume = (self.outerMechRate+self.innerMechRate)/32.0
        
        # volume can be changed while playing,
        # the speed we'll want to do in a side task
        self.sfx_steam.setVolume(steamVolume)
            
        return Task.cont
    
    # changes brake application, plays brake sfx if brakes just applied
    def setBrake(self,newBrake):
        if newBrake and not self.brake and self.spinrate>0:
            self.sfx_brake.play()
        self.brake = newBrake
        
    # method to allow delayed ungrounding when ball rolls off an edge
    # (prevents constant 'hovering' along surface)
    def unground(self):
        self.grounded=False
    
    # updates the play rate of the mechanical sounds based on ball speed
    # since play() must be called to apply the new rate, and spamming
    # stop/play every frame kills audio quality
    def soundUpdateTask(self,task):
        time = self.sfx_mechOuter.getTime()
        self.sfx_mechOuter.setPlayRate(self.outerMechRate)
        self.sfx_mechOuter.setTime(time)
        self.sfx_mechOuter.play()
        
        time = self.sfx_mechInner.getTime()
        self.sfx_mechInner.setPlayRate(self.innerMechRate)
        self.sfx_mechInner.setTime(time)
        self.sfx_mechInner.play()
        return Task.again
    
    # for winding the ball down in an interval
    def setOuterMechRate(self,rate):
        self.outerMechRate = rate
    def setInnerMechRate(self,rate):
        self.innerMechRate = rate
    
    # makes the ball pitch and spin-down when not under power
    def setSpinTask(self,task):
        t = task.time
        dt = t-self.oldSpinTime
        self.oldSpinTime=t
        self.root.setR(self.tilt)
        self.rollGimbal.setP(self.rollGimbal.getP()-self.spinrate*dt)
        return Task.cont