bl_info = {
    "name": "OsciStudio Client",
    "author": "Hansi Raber",
    "version": (1, 2),
    "blender": (2, 65, 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

#panel with per object settings
class myPanel(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 = "OStudio Params"           # name of the new panel

    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_freq")
            #self.layout.prop(bpy.context.active_object, "ostudio_tracefrom", slider=True)
            #self.layout.prop(bpy.context.active_object, "ostudio_tracerange",slider=True)
            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_symmetrize', text="Symmetrize Audio")
        #layout.prop(bpy.context.window_manager, 'ostudio_synthmode', text="Play all objects at once (add)")
        #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")
        #layout.separator()
        #layout.prop(bpy.context.window_manager, 'ostudio_wavname')
        #layout.operator("render.ostudio_exportwavetable", text="Wavetable for Frame")
        #layout.operator("render.ostudio_exportwavetables", text="Wavetables for Animation")
        #layout.operator("render.ostudio_exportwav", text="Wav for 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 ostudio"

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

    def execute(self, context):
        global sock
        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 ostudio"

    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 single wavetable operation
class OBJECT_OT_ostudio_exportwavetable(bpy.types.Operator):
    bl_label = "Export as Wavetable"

    bl_idname="render.ostudio_exportwavetable"

    def execute(self, context):
        filename = bpy.context.window_manager.ostudio_wavname + "-" + str(bpy.context.scene.frame_current)
        print( "save as " + filename)
        if sendMessage("S" +filename, waitForAck=True):
            self.report({'INFO'}, "Saved file")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}

# export wavetables operation
class OBJECT_OT_ostudio_exportwavetables(bpy.types.Operator):
    bl_label = "Run Wavetable Export Sequence"

    bl_idname="render.ostudio_exportwavetables"

    def execute(self, context):
        global sock
        global nextFrameInRecSequence
        global recordingType

        if sock is not None:
            bpy.context.scene.frame_current = bpy.context.scene.frame_start
            recordingType = RecordingType.wavetable
            nextFrameInRecSequence = bpy.context.scene.frame_start
            self.report({'INFO'}, "Sequence started ...")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}


# export wav operation
class OBJECT_OT_ostudio_exportwav(bpy.types.Operator):
    bl_label = "Run Wav Recording Sequence"

    bl_idname="render.ostudio_exportwav"

    def execute(self, context):
        global sock
        global nextFrameInRecSequence
        global recordingType
        if sock is not None:
            bpy.context.scene.frame_current = bpy.context.scene.frame_start
            bpy.ops.render.ostudio_record_begin()
            recordingType = RecordingType.wav
            nextFrameInRecSequence = bpy.context.scene.frame_start
            self.report({'INFO'}, "Sequence started ...")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}

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

    bl_idname="render.ostudio_sendanimation"

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

        return {'FINISHED'}

# export wavetable operation
class OBJECT_OT_ostudio_record_begin(bpy.types.Operator):
    bl_label = "Begin Recording Wav File"

    bl_idname="render.ostudio_record_begin"

    def execute(self, context):
        filename = bpy.context.window_manager.ostudio_wavname + "-full"
        if sendMessage( "R" + str(bpy.context.scene.render.fps) + "#" + filename):
            self.report({'INFO'}, "Started Recording")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}

# export wavetable operation
class OBJECT_OT_ostudio_record_append(bpy.types.Operator):
    bl_label = "Append to Wav File"

    bl_idname="render.ostudio_record_append"

    def execute(self, context):
        if sendMessage( "A", waitForAck=True ):
            self.report({'INFO'}, "Did append")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}

# export wavetable operation
class OBJECT_OT_ostudio_record_end(bpy.types.Operator):
    bl_label = "End Recording Wav File"

    bl_idname="render.ostudio_record_end"

    def execute(self, context):
        if sendMessage( "E" ):
            self.report({'INFO'}, "Ended Recording")
        else:
            self.report({'INFO'}, "Connection needed")

        return {'FINISHED'}


def listCurve(s,obj,max):
    curve = obj.data;
    mat = obj.matrix_world
    trans = worldToCameraTransformer()
    header = "freq:" + str(obj.ostudio_freq) + " tracefrom:" + str(obj.ostudio_tracefrom) + " tracerange:" + str(obj.ostudio_tracerange) + " group:" + str(obj.ostudio_midi_group);
    # vertex to string

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

    msg = "";
    if obj.ostudio_to_mesh:
        mesh = obj.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW')
        msg += 'POLY:' + header + ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in mesh.vertices])+']#';
        mesh.user_clear()
        bpy.data.meshes.remove(mesh)

    else:
        for spline in curve.splines:
            if spline.type == "POLY":
                msg += 'POLY:' + header + ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in spline.points])+']#';
            elif spline.type == "BEZIER":
                msg += 'BEZIER:' + header + ' vertices:[' + \
                    ''.join(["{v:" + vts(p.co) + " l:" + vts(p.handle_left)+ " r:" + vts(p.handle_right)+"}" for p in spline.bezier_points])+ \
                    ']#';

    count = 1
    return (msg, count)


def listObject(s,obj,max):
    me = obj.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW')
    count = len(me.vertices)
    if count >= max:
        return ("", 0)

    msg = str(obj.ostudio_freq) + ":" + str(obj.ostudio_tracefrom) + ":" + str(obj.ostudio_tracerange) + ":" + str(obj.ostudio_midi_group)
    msg = msg + ":" + listEdges(obj,me)

    me.user_clear()
    bpy.data.meshes.remove(me)

    return (msg, count)

def listPolygons(obj,me):
    msg = ""
    mat = obj.matrix_world
    trans = worldToCameraTransformer()

    first = True
    for vec in me.vertices:
        v = vec.co
        v = trans(mat*v)
        msg = msg + ("" if first else "&") + str(v[0])+" " + str(v[1]) + " " + str(v[2])
        first = False

    msg = msg + ":"

    first = True
    for poly in me.polygons:
        p = ""
        for v in poly.vertices:
            p = p + ("" if len(p)==0 else " ") + str(v)

        msg = msg + ("" if first else "&" ) + p
        first = False

    return msg


def listEdges(obj,me):
    msg = ""
    vertices = me.vertices;
    mat = obj.matrix_world
    trans = worldToCameraTransformer()

    #val = 0 if obj.active_shape_key == None else obj.active_shape_key.value

    first = True
    for vec in me.vertices:
        v = vec.co
        v = trans(mat*v)
        msg = msg + ("" if first else "&") + str(v[0])+" " + str(v[1]) + " " + str(v[2])
        first = False

    msg = msg + ":"
    print(msg)
    first = True
    for edge in me.edges:
        i1 = edge.vertices[0]
        i2 = edge.vertices[1]
        msg = msg + ("" if first else "&" ) + str(i1) + " " + str(i2)
        first = False

    return msg

def serializeObject(s,obj,max):
    mesh = obj.to_mesh(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW')
    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:"' + obj.ostudio_name.replace('"','\\"') + '" ';
    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]) + ']'

    mesh.user_clear()
    bpy.data.meshes.remove(mesh)

    return ("{mesh: " + header + verts + edges + "}", count)


def serializeCurve(s,obj,max):
    curve = obj.data;
    mat = obj.matrix_world
    trans = worldToCameraTransformer()
    header = 'group:' + str(obj.ostudio_midi_group) + ' name:"' + obj.ostudio_name.replace('"','\\"') + '" ';
    # 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(scene=bpy.context.scene, apply_modifiers=True, settings='PREVIEW')
        
        curves.append('{poly: ' + header + ' vertices:[' + ''.join(["{" + vts(p.co)+"}" for p in mesh.vertices])+']}');
        mesh.user_clear()
        bpy.data.meshes.remove(mesh)

    else:
        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.selectable_objects:
        count_ = 0;
        if obj.ostudio_enabled:
            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 nextFrameInRecSequence
    global recordingType
    global animationFrames
    global lastFrameVisible
    
    frameVisible = bpy.context.scene.frame_current
    
    if sock is None:
        nextFrameInRecSequence = -1
        return
        
    max = bpy.context.window_manager.ostudio_maxvertices
    if forceUpdate or (bpy.data.objects.is_updated and bpy.context.window_manager.ostudio_autosend):
        (msg, numObjects) = serializeFrame()
        if numObjects > 0: 
            sendMessage('animation frames:[' + msg + ']')
        else: 
            sendMessage('Z')
        #msg = ""
        #
        #if bpy.context.window_manager.ostudio_synthmode:
        #    msg = "add#"
        #else:
        #    msg = "serial#"
        #    
        #if bpy.context.window_manager.ostudio_symmetrize:
        #    msg = msg + "S#";
        #else:
        #    msg = msg + "A#";
        #    
        #for obj in bpy.context.selectable_objects:
        #    count_ = 0;
        #    if obj.ostudio_enabled:
        #        if obj.type == "MESH":
        #            (msg_,count_) = listObject(sock,obj,max)
        #        elif obj.type == "CURVE":
        #            (msg_,count_) = listCurve(sock,obj,max);
        #
        #        if count_ > 0:
        #            msg = msg + msg_ + "#"
        #            max = max - count_
        #        if max <= 0:
        #            break
        #
        #if msg == "":
        #    msg = "Z"
        #
        #sendMessage(msg)

    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

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


def initostudio():
    bpy.types.Object.ostudio_name = StringProperty(
            name="Name",
            default="Unnamed"
            )
            
    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"
            )


    #bpy.app.handlers.scene_update_post.clear()
    #bpy.utils.register_class(myPanel)   # register panel

#@persistent
#def load_handler(somethingsomething):
#    print( "nothing" )


def register():
    bpy.app.handlers.scene_update_post.append(scene_updated)
    #bpy.app.handlers.load_post.append(load_handler)
    bpy.utils.register_module(__name__)

    initostudio()

def unregister():
   #bpy.utils.unregister_class(myPanel)
   bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()
