bl_info = {
    "name": "OsciStudio A6.0 Client",
    "author": "Hansi Raber",
    "version": (1, 5),
    "blender": (2, 80, 0),
    "location": "ostudio",
    "description": "Sends object data to OsciStudio",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"}

import textwrap
import bpy
import struct
import select
from bpy.props import StringProperty, IntProperty, BoolProperty
import socket
import sys
from bpy.app.handlers import persistent
import time
from threading import Timer
from threading import Lock
from enum import Enum

class RecordingType(Enum):
    none = 0
    wavetable = 1
    wav = 2
    animation = 3

lock = Lock()
sock = None
nextFrameInRecSequence = -1
recordingType = RecordingType.none
animationFrames = []
lastFrameVisible = -1
lastFrameMessage = ''

#panel with per object settings
class OBJECT_PT_ostudio_props(bpy.types.Panel):     # panel to display new property
    bl_space_type = "VIEW_3D"       # show up in: 3d-window
    bl_region_type = "UI"           # show up in: properties panel
    bl_label = "OsciStudio"           # name of the new panel
    bl_category = 'OsciStudio'
    
    def draw(self, context):
        if bpy.context.active_object != None:
            # display value of "foo", of the active object
            self.layout.prop(bpy.context.active_object, "ostudio_name")
            self.layout.prop(bpy.context.active_object, "ostudio_midi_group")
            self.layout.prop(bpy.context.active_object, "ostudio_enabled")
            self.layout.prop(bpy.context.active_object, "ostudio_to_mesh")

#panel with global settings and connect/disconnect buttons
class OBJECT_PT_ostudio(bpy.types.Panel):
    bl_label = "OsciStudio Global Settings"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "render"

    def draw_header(self, context):
        layout = self.layout
        layout.label(text="", icon="SPEAKER")

    def draw(self, context):
        global sock
        layout = self.layout
        layout.prop(bpy.context.window_manager, 'ostudio_maxvertices', text="Max Vertices")
        layout.prop(bpy.context.window_manager, 'ostudio_port', text="Port Number")
        #layout.prop(bpy.context.window_manager, 'ostudio_usecam', text="Use active camera")
        layout.prop(bpy.context.window_manager, 'ostudio_autosend')

        if sock is None:
            layout.operator("render.ostudio_connect", text="Connect")
        else:
            layout.operator("render.ostudio_disconnect", text="Disconnect")

        layout.separator()
        layout.operator("render.ostudio_sendanimation", text="Send Animation")


def sendMessage(msg, waitForAck=False):
    global sock
    global lock
    with lock:
        if sock is not None:
            readable, writable, exceptional = select.select([sock], [sock], [sock], 0)
            if len(writable) == 0: 
                print("No valid connection:")
                print("R=" + str(len(readable)) + ", W=" + str(len(writable)) + "E=" + str(len(exceptional)))
                bpy.ops.render.ostudio_disconnect()
                return
            try:
                #print("-------------------------")
                #print("SENDING:" + msg)
                #print("-------------------------")

                n = 1024
                for i in range(0,len(msg),n): 
                    sock.sendall(bytes(msg[i:(i+n)],'UTF-8'))
                sock.sendall(bytes('\n','UTF-8'))
                
                if waitForAck:
                    data = sock.recv(1);
                    while data != b"K":
                        if data == '':
                            bpy.ops.render.ostudio_disconnect()
                            sock.close()
                            return
                        data = sock.recv(1);
                    sock.recv(1)
                return True
            except:
                bpy.ops.render.ostudio_disconnect()
                return False
        else:
            return False


# connect operation
class OBJECT_OT_ostudio_connect(bpy.types.Operator):
    bl_label = "Connect to OsciStudio"

    bl_idname = "render.ostudio_connect"
    bl_description = "Connect to OsciStudio"

    def execute(self, context):
        global sock
        global lastFrameMessage
        
        lastFrameMessage = ''

        TCP_IP = '127.0.0.1'
        TCP_PORT = bpy.context.window_manager.ostudio_port

        if sock is None:
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

                #timeval = struct.pack('ll', 2, 0)
                #sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, timeval)
                #sock.setblocking( False )
                sock.settimeout(2.0)
                
                sock.connect((TCP_IP, TCP_PORT))
                
                
                #sock.send( bytes("Z\n", 'UTF-8' ))
                self.report({'INFO'}, "Connected!")
                scene_updated(bpy.context.scene,False)
            except Exception as inst:
                print( inst )
                print( type(inst) )
                print( inst.args )
                sock.close()
                sock = None
                self.report({'ERROR'}, "Connection failed")
        else:
            self.report({'INFO'}, "Was Already connected")

        return {'FINISHED'}

# disconnect operation
class OBJECT_OT_ostudio_disconnect(bpy.types.Operator):
    bl_label = "Disconnect from OsciStudio"

    bl_idname="render.ostudio_disconnect"

    def execute(self, context):
        global sock
        if sock is not None:
            try:
                #sock.send( bytes("Z\n", 'UTF-8' ))
                sock.close()
                self.report({'INFO'}, "Connection closed")
            except:
                self.report({'ERROR'}, "Couldn't close connection")

            sock = None
        else:
            self.report({'INFO'}, "Connection was already closed")

        return {'FINISHED'}


# export wav operation
class OBJECT_OT_ostudio_sendanimation(bpy.types.Operator):
    bl_label = "Send Animation to OsciStudio"

    bl_idname="render.ostudio_sendanimation"

    def execute(self, context):
        global sock
        global nextFrameInRecSequence
        global recordingType
        global animationFrames
        global lastFrameMessage    
        if sock is not None:
            bpy.context.window_manager.ostudio_autosend = False
            bpy.context.scene.frame_current = bpy.context.scene.frame_start
            lastFrameMessage = ''
            animationFrames = []
            recordingType = RecordingType.animation
            nextFrameInRecSequence = bpy.context.scene.frame_start
            self.report({'INFO'}, "Sequence started ...")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}

def getNameQuoted(obj): 
    name = obj.name_full
    if obj.ostudio_name != '': 
        name = obj.ostudio_name

    return name.replace('"','\\"')

def serializeObject(s,obj,max):
    dg = bpy.context.evaluated_depsgraph_get()
    
    mesh = obj.evaluated_get(dg).to_mesh()
    mat = obj.matrix_world
    trans = worldToCameraTransformer()
    count = len(mesh.vertices)
    
    if count >= max:
        return ("", 0)

    def vts(x):
        p = trans(mat@x)
        return str(p.x)+","+str(p.y)+","+str(p.z)

    header = "group:" + str(obj.ostudio_midi_group) + ' name:"' + getNameQuoted(obj) + '" ';
    verts = ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in mesh.vertices])+']';
    edges = ' edges:[' + ''.join(["{" + str(e.vertices[0]) + " " + str(e.vertices[1]) + "}" for e in mesh.edges]) + ']'
    faces = ' faces:[' + ''.join("[" + ' '.join(str(i) for i in p.vertices) + "]" for p in mesh.polygons.values()) + ']'; 
    
    mesh.user_clear()
    obj.to_mesh_clear()
    
    return ("{mesh: " + header + verts + edges + faces + "}", count)


def serializeCurve(s,obj,max):
    curve = obj.data;
    mat = obj.matrix_world
    trans = worldToCameraTransformer()
    header = 'group:' + str(obj.ostudio_midi_group) + ' name:"' + getNameQuoted(obj) + '" ';
    # vertex to string

    def vts(x):
        p = trans(mat@x)
        return str(p.x)+","+str(p.y)+","+str(p.z)

    curves = []
    if obj.ostudio_to_mesh:
        mesh = obj.to_mesh()
        
        curves.append('{poly: ' + header + ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in mesh.vertices])+']}');
        mesh.user_clear()
        obj.to_mesh_clear()

    else:
        #header = header + " cyclic:" + ("0" if len(obj.splines)==0 else ("1" if obj.splines[0].use_cyclic_u else "0")); 
        header = header + " cyclic:0"; 
        for spline in curve.splines:
            if spline.type == "POLY":
                pts = list(spline.points)
                if spline.use_cyclic_u and len(pts)>0: 
                    pts.append(pts[0])
                curves.append('{poly: ' + header + ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in pts])+']}');
            elif spline.type == "BEZIER":
                pts = list(spline.bezier_points)
                if spline.use_cyclic_u and len(pts)>0: 
                    pts.append(pts[0])
                curves.append('{bezier: ' + header + ' vertices:[' + \
                    ''.join(["{v:" + vts(p.co) + " l:" + vts(p.handle_left)+ " r:" + vts(p.handle_right)+"}" for p in pts])+ \
                    ']}');

    return ("".join(curves), len(curves))


def serializeFrame():
    global sock
    max = bpy.context.window_manager.ostudio_maxvertices
    msg = ""
    numObjects = 0
    for obj in bpy.context.view_layer.objects:
        count_ = 0;
        if obj.ostudio_enabled and obj.visible_get():
            if obj.type == "MESH":
                (msg_,count_) = serializeObject(sock,obj,max)
            elif obj.type == "CURVE":
                (msg_,count_) = serializeCurve(sock,obj,max)

            if count_ > 0:
                numObjects = numObjects + 1
                msg = msg + ("" if len(msg) == 0 else ",") + msg_
                max = max - count_
            if max <= 0:
                break
    
    return ("{" + \
        " frameNum:" + str(bpy.context.scene.frame_current-bpy.context.scene.frame_start) + \
        " shapes:[" + msg + "]" + \
    "}", numObjects); 

def worldToCameraTransformer():
    camObj = bpy.context.scene.camera
    if camObj is None:
        #self.report({'ERROR'}, "None camera available ...")
        def trans(co): 
            return co
        return trans

    from mathutils import Vector
    inv = camObj.matrix_world.normalized().inverted()
    scene = bpy.context.scene
    camera = camObj.data
    frame = [-v for v in camera.view_frame(scene=scene)[:3]]
    if not(bpy.context.window_manager.ostudio_usecam): 
        return lambda co:co

    def trans(co,frame=frame,camera=camera):
        co_local = inv @ co
        z = -co_local.z

        if camera.type != 'ORTHO':
            if z == 0.0:
                return Vector((0.5, 0.5, 0.0))
            else:
                frame = [(v / (v.z / z)) for v in frame]

        min_x, max_x = frame[1].x, frame[2].x
        min_y, max_y = frame[0].y, frame[1].y
        dx = (max_x - min_x)
        dy = (max_y - min_y)
        if dx > dy:
            x = (co_local.x - min_x) / dx
            y = (co_local.y - min_y - (dx-dy)/2) / dx
        else: 
            x = (co_local.x - min_x - (dy-dx)/2) / dy
            y = (co_local.y - min_y) / dy

        return Vector((x, z, y))

    return trans


@persistent
def scene_updated(scene,forceUpdate=False):
    global sock
    global lastFrameMessage    
    
    if bpy.app.timers.is_registered(checkSendAnimation) == False: 
        print("re-registering ostudio update timer...")
        bpy.app.timers.register(checkSendAnimation)

    
    if forceUpdate == True or (bpy.context.window_manager.ostudio_autosend):
        (msg, numObjects) = serializeFrame()
        if numObjects > 0: 
            idx_new = msg.find('shapes:')
            idx_old = lastFrameMessage.find('shapes:')
            
            if lastFrameMessage[idx_old:] != msg[idx_new:]:
                sendMessage('animation frames:[' + msg + ']')
            lastFrameMessage = msg
        else: 
            lastFrameMessage = ''
            sendMessage('Z')


def checkSendAnimation(): 
    global nextFrameInRecSequence
    global recordingType
    global animationFrames
    global lastFrameVisible
    if sock is None:
        nextFrameInRecSequence = -1
        return 0.1
    
    frameVisible = bpy.context.scene.frame_current
    if nextFrameInRecSequence >= 0 and nextFrameInRecSequence == bpy.context.scene.frame_current and lastFrameVisible == bpy.context.scene.frame_current:
        nextFrameInRecSequence = nextFrameInRecSequence + bpy.context.scene.frame_step
        if recordingType == RecordingType.animation:
            (msg,numObjects) = serializeFrame() 
            animationFrames.append(msg)
        elif recordingType == RecordingType.wavetable:
            bpy.ops.render.ostudio_exportwavetable()
        else:
            bpy.ops.render.ostudio_record_append()

        if bpy.context.scene.frame_current == bpy.context.scene.frame_end:
            nextFrameInRecSequence = -1
            if recordingType == RecordingType.wav:
                bpy.ops.render.ostudio_record_end()
            elif recordingType == RecordingType.animation: 
                print("Done Recording. Result: \n")
                sendMessage('animation frames:[' + ",".join(animationFrames) + ']')
        
        bpy.context.scene.frame_current = nextFrameInRecSequence
        
    lastFrameVisible = frameVisible
    return 0.05
    

def forceUpdate(self, context):
    scene_updated(context.scene, True)


def initostudio():
    bpy.types.Object.ostudio_name = StringProperty(
            name="Name",
            default=""
            )
            
    bpy.types.Object.ostudio_freq = bpy.props.FloatProperty(
        name="Frequency",
        default=50.0,
        min=0.01,
        max=15000.0
    )

    bpy.types.Object.ostudio_tracefrom = bpy.props.FloatProperty(
        name="Trace from",
        default=0,
        min=0.00,
        max=1.00
    )

    bpy.types.Object.ostudio_tracerange = bpy.props.FloatProperty(
        name="Trace range",
        default=1.0,
        min=0.00,
        max=1.0
    )

    bpy.types.Object.ostudio_midi_group = bpy.props.IntProperty(
        name="Midi Group",
        default=0,
        min=0,
        max=3
    )

    bpy.types.Object.ostudio_to_mesh = bpy.props.BoolProperty(
        name="To Mesh",
        default=False
    )

    bpy.types.Object.ostudio_enabled = bpy.props.BoolProperty(
        name="Send",
        default=True
    )


    bpy.types.WindowManager.ostudio_maxvertices = IntProperty(
            name="Maximum vertices per frame (higher means more cpu)",
            default=2000,
            min=2
            )

    bpy.types.WindowManager.ostudio_synthmode = BoolProperty(
            name="Play all objects at once (ADD)",
            default=False,
            update=forceUpdate
            )

    bpy.types.WindowManager.ostudio_usecam = BoolProperty(
            name="Use active camera",
            default=False,
            update=forceUpdate
            )

    bpy.types.WindowManager.ostudio_port = IntProperty(
            name="Port Number",
            default=11995,
            min=1,
            max=65536
            )

    bpy.types.WindowManager.ostudio_autosend = BoolProperty(
            name="Automatically send when blend file changes",
            default=True,
            )

    bpy.types.WindowManager.ostudio_symmetrize = BoolProperty(
            name="Symmetrize Audio",
            default=False,
            update=forceUpdate
            )

    bpy.types.WindowManager.ostudio_wavname = StringProperty(
            name="Filename when exporting Wavetable",
            default="testomat"
            )


classes = (OBJECT_PT_ostudio,OBJECT_OT_ostudio_connect,OBJECT_OT_ostudio_sendanimation,OBJECT_OT_ostudio_disconnect,OBJECT_PT_ostudio_props)

def register():
    bpy.app.handlers.depsgraph_update_post.append(scene_updated)
    bpy.app.handlers.frame_change_post.append(scene_updated)
    for cls in classes:
        bpy.utils.register_class(cls)
        
    initostudio()
    # i _should_ be allowed to call register(checkSendAnimation,0,True)
    # to install a persistent timer, but this doesn't seem to work. 
    # so instead, in scene_updated we check if the timer still works!
    bpy.app.timers.register(checkSendAnimation)

def unregister():
    bpy.app.handlers.depsgraph_update_post.remove(scene_updated)
    bpy.app.handlers.frame_change_post.remove(scene_updated)
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
        
    bpy.app.timers.unregister(checkSendAnimation)
        
if __name__ == "__main__":
    register()
