#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -*- mode: Python -*-
'''
LJ Laser Server v0.8.2

Inspiration for a future WebUI icon menu :
https://codepen.io/AlbertFeynman/pen/mjXeMV

Laser server + webUI servers (ws + OSC)

- get point list to draw : /pl/lasernumber
- for report /lstt/lasernumber /lack/lasernumber /cap/lasernumber
- a nice ws debug tool : websocat 
- a "plugin" is a generator that send points to LJ. Plugins if they have an open OSC port can be checked and restart if in the same computer. 


All used ports: 

8002 OSC incoming
9001 Websocket communication with WebGUI 
Plugins OSC Ports (see LJ.conf)

'''
#import pdb

from libs3 import log
print("")
print("")
log.infog("LJ Laser Server")
log.infog("v0.8.2")
print("")
print("-h will display help")
print("")

import redis
import os
ljpath = r'%s' % os.getcwd().replace('\\','/')
import sys

#sys.path.append('libs3/')
from libs3 import gstt, settings
gstt.ljpath = ljpath

log.info("Reading " + gstt.ConfigName + " setup file...")
settings.Read()

# Arguments may alter .conf file so import settings first then cli
from libs3 import cli
settings.Write()

from multiprocessing import Process, set_start_method
import random, ast
from libs3 import plugins


#from libs3 import lasytracer as tracer 
from libs3 import tracer3 as tracer 


from libs3 import homographyp, commands, font1
#import subprocess
import os
#import midi
from libs3 import OSC3 
from websocket_server import WebsocketServer
#import socket
import types, _thread, time



r = redis.StrictRedis(host=gstt.LjayServerIP , port=6379, db=0)
# r = redis.StrictRedis(host=gstt.LjayServerIP , port=6379, db=0, password='-+F816Y+-')
args =[0,0]


def dac_process(number, pl):

    import sys
    from libs3 import gstt

    print("Starting dac process", number)

    while True:
        try:
            d = tracer.DAC(number,pl)
            d.play_stream()

        except Exception as e:

            import sys
            import traceback

            if gstt.debug > 0:
                log.err('\n---------------------')
                log.err('Exception: %s' % e)
                log.err('- - - - - - - - - - -')
                traceback.print_tb(sys.exc_info()[2])
                print("\n")
            pass

        except KeyboardInterrupt:
            sys.exit(0)
 


#
# Servers init variables
#

print("Start Scene number :",gstt.SceneNumber)

print("WebUI connect to :", gstt.wwwIP)

serverIP = gstt.LjayServerIP
print("Redis IP :", serverIP)

oscserverIP = gstt.oscIPin
print("OSCserver IP :", oscserverIP)

nozoscIP = gstt.nozoscip
print("Nozosc IP :", nozoscIP)

debug = gstt.debug 
print("Debug :", debug)


# Websocket listening port
wsPORT = 9001

# oscserver
# OSC Server : accept OSC message on port 8002 
#oscIPin = "192.168.1.10"s
oscserverIPin = serverIP

print("oscserverIPin", oscserverIPin)
oscserverPORTin = 8002

# OSC Client : to send OSC message to an IP port 8001
oscserverIPout = oscserverIP 
oscserverPORTout = 8001


# Nozoid OSC Client : to send OSC message to Nozoid inport 8003
NozoscIPout = nozoscIP
NozoscPORTout = plugins.Port("nozoid")


# Planetarium OSC Client : to send OSC message to planetarium inport 8005
planetIPout = nozoscIP
planetPORTout = plugins.Port("planet")


import socket

#retry = 1
#delay = 1


#
# OSC
#

oscserver = OSC3.OSCServer( (oscserverIPin, oscserverPORTin) )
oscserver.timeout = 0
OSCRunning = True


def handle_timeout(self):
    self.timed_out = True

oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver)


# OSC default path handler : send incoming OSC message to UI via websocket 9001
def handler(path, tags, args, source):

    oscpath = path.split("/")
    if gstt.debug > 0:
        print("")
        print("OSC handler in main said : path", path," oscpath ", oscpath," args", args)

    if oscpath[1] != "pong":
        sendWSall(path + " " + str(args[0]))
    commands.handler(oscpath,args)


# RAW OSC Frame available ? 
def osc_frame():
    #print 'oscframe'
    # clear timed_out flag
    oscserver.timed_out = False
    # handle all pending requests then return
    while not oscserver.timed_out:
        oscserver.handle_request()

def PingAll():

    if gstt.debug > 0:
        print("Pinging all plugins...")
        
    for plugin in list(gstt.plugins.keys()):
        if gstt.debug > 0:
            print("pinging", plugin)
        #sendWSall("/"+ plugin + "/start 0")
        plugins.Ping(plugin)



# OSC server Thread : handler, dacs reports and simulator points sender to UI.
def osc_thread():

    #while True:
        try:
            while True:

                time.sleep(0.1)
                osc_frame()
                for laserid in range(0,gstt.LaserNumber):           # Laser not used -> led is not lit

                    lstate = {'0': 'IDLE', '1': 'PREPARE', '2': "PLAYING", '64': "NOCONNECTION ?" }
                    lstt = r.get('/lstt/'+ str(laserid)).decode('ascii')
                    #print ("laserid", laserid,"lstt",lstt, type(lstt))
                    if gstt.debug >1:
                        print("DAC", laserid, "is in (lstt) :", lstt , lstate[str(lstt)])
                    if lstt == "0":                                 # Dac IDLE state(0) -> led is blue (3)
                        sendWSall("/lstt/" + str(laserid) + " 3")

                    if lstt == "1":                                 # Dac PREPARE state (1) -> led is cyan (2)
                        sendWSall("/lstt/" + str(laserid) + " 2")

                    if lstt == "2":                                 # Dac PLAYING (2) -> led is green (1)
                        sendWSall("/lstt/" + str(laserid) + " 1")
                

                    ackstate = {'61': 'ACK', '46': 'FULL', '49': "INVALID", '21': 'STOP', '64': "NOCONNECTION ?", '35': "NOCONNECTION ?" , '97': 'ACK', '70': 'FULL', '73': "INVALID", '33': 'STOP', '100': "NOCONNECTION", '48': "NOCONNECTION", 'a': 'ACK', 'F': 'FULL', 'I': "INVALID", '!': 'STOP', 'd': "NOCONNECTION", '0': "NOCONNECTION"}
                    lack= r.get('/lack/'+str(laserid)).decode('ascii')

                    if gstt.debug >1:
                        print("DAC", laserid, "answered (lack):", lack, chr(int(lack)), ackstate[str(lack)])

                    if chr(int(lack)) == 'a':                       # Dac sent ACK ("a") -> led is green (1)
                        sendWSall("/lack/" + str(laserid) +" 1")

                    if chr(int(lack)) == 'F':                       # Dac sent FULL ("F") -> led is orange (5)
                        sendWSall("/lack/" + str(laserid) +" 5")

                    if chr(int(lack)) == 'I':                       # Dac sent INVALID ("I") -> led is yellow (4)
                        sendWSall("/lack/" + str(laserid)+" 4")
                    #print lack
                    
                    if lack == "64" or lack =="35":                 # no connection to dac -> leds are red (6)
                        sendWSall("/lack/" + str(laserid) + " 6")   
                        sendWSall("/lstt/" + str(laserid) + " 6")  
                        #sendWSall("/lstt/" + str(laserid) + " 0")  
                        sendWSall("/points/" + str(laserid) + " 6")
                        
                    else:
                        # last number of points sent to etherdream buffer
                        sendWSall("/points/" + str(laserid) + " " + str(r.get('/cap/'+str(laserid)).decode('ascii')))

                #print("Sending simu frame from",'/pl/'+str(gstt.SceneNumber)+'/'+str(gstt.Laser))
                #print(r.get('/pl/'+str(gstt.SceneNumber)+'/'+str(gstt.Laser)))
                sendWSall("/simul" +" "+ str(r.get('/po/'+str(gstt.SceneNumber)+'/'+str(gstt.Laser)).decode('ascii')))
                if random.randint(0,100)>95:
                    plugins.sendbroadcast()


        except Exception as e:
            import sys, traceback
            print('\n--------------------------')
            print('OSC Thread Exception: %s' % e)
            print('- - - - - - - - - - - - - - ')
            traceback.print_tb(sys.exc_info()[2])
            print("\n")

#
# Websocket part
# 

# Called for every WS client connecting (after handshake)
def new_client(client, wserver):

    print("New WS client connected and was given id %d" % client['id'])
    sendWSall("/status Hello " + str(client['id']))

    for laserid in range(0,gstt.LaserNumber):    

        sendWSall("/ip/" + str(laserid) + " " + str(gstt.lasersIPS[laserid]))
        sendWSall("/kpps/" + str(laserid)+ " " + str(gstt.kpps[laserid]))
        #sendWSall("/laser"+str(laserid)+"/start 1")
        sendWSall("/laser "+str(laserid))
        #print("/laser "+str(laserid))
        sendWSall("/lack/" + str(laserid) + " 6")
        #print("/lack/" + str(laserid) + " 6") 
        sendWSall("/lstt/" + str(laserid) + " 6")  
        #print("/lstt/" + str(laserid) + " 6")
        sendWSall("/points/" + str(laserid) + " 0")
        #print("/points/" + str(laserid) + " 0")

        if gstt.swapX[laserid] == 1:
            sendWSall("/swap/X/" + str(laserid)+ " 1")
        else:
            sendWSall("/swap/X/" + str(laserid)+ " 0")

        if gstt.swapY[laserid] == 1:
            sendWSall("/swap/Y/" + str(laserid)+ " 1")
        else:
            sendWSall("/swap/Y/" + str(laserid)+ " 0")

# Called for every WS client disconnecting
def client_left(client, wserver):
    print("WS Client(%d) disconnected" % client['id'])


# Called for each WS received message.
def message_received(client, wserver, message):

    #if len(message) > 200:
    #    message = message[:200]+'..'    

    #if gstt.debug >0:
    #    print ("")
    #    print("WS Client(%d) said: %s" % (client['id'], message))
    
    oscpath = message.split(" ")
    #print "WS Client", client['id'], "said :", message, "splitted in an oscpath :", oscpath
    if gstt.debug > 0:
        print("WS Client", client['id'], "said :", message, "splitted in an oscpath :", oscpath)
    
    PingAll()
    message4plugin = False

    # WS received Message is for a plugin ?

    for plugin in list(gstt.plugins.keys()):
    
        if oscpath[0].find(plugin) != -1:
    
            message4plugin = True
            #print(oscpath)
            if plugins.Send(plugin, oscpath):
                print("plugins sent incoming WS correctly to", plugin)
            else:
                print("plugins detected", plugin, "offline.")


    # WS received message is an LJ command 

    if message4plugin == False:

        if len(oscpath) == 1:
            args[0] = "noargs"
            #print "noargs command"
    
        elif len(oscpath) > 1:
            args[0] = str(oscpath[1]) 
            #print "arg",oscpath[1]
        
        commands.handler(oscpath[0].split("/"),args)
    

    # if needed a loop back : WS Client -> server -> WS Client
    #sendWSall("ws"+message)


def handle_timeout(self):
    self.timed_out = True


def sendWSall(message):
    #if gstt.debug >0:
        #print("WS sending %s" % (message))
    wserver.send_message_to_all(message)
 
'''   
print ""
print "Midi Configuration"
midi.InConfig()
midi.OutConfig()
'''

def fff(name):
        print()
        print('HELLO', name ) #indent
        print()


#
# Create a startup point list for each laser : 0,1,2,...
#

print("")
log.info("Creating startup point lists...")


if r.set("/clientkey","/pl/"+str(gstt.SceneNumber)+"/")==True:
    print("sent clientkey : /pl/"+str(gstt.SceneNumber)+"/")
    
#pdb.set_trace()
for sceneid in range(0,gstt.MaxScenes+1):
    print("Scene "+ str(sceneid))
    #digit_points = font1.DigitsDots(sceneid,65280)

    # Order all lasers to show its number at startup -> tell all 4 laser process to USER PLs
    for laserid in range(0,gstt.LaserNumber):

        digit_points = font1.DigitsDots(laserid,65280)
        if r.set('/pl/'+str(sceneid)+'/'+str(laserid), str(digit_points)) == True:
            pass
            #print( ast.literal_eval(r.get('/pl/'+str(sceneid)+'/'+str(laserid)).decode('ascii')))
            #print("/pl/"+str(sceneid)+"/"+str(laserid)+" "+str(ast.literal_eval(r.get('/pl/'+str(sceneid)+'/'+str(laserid)).decode('ascii'))))

        r.set('/order/'+str(laserid), 0)


#
# Starts one DAC process per requested Laser
#

if __name__ == '__main__':

    # Bug in 3.8.4 MacOS default multiprocessing start method is spawn. Spawn doesn't work properly
    set_start_method('fork')

    print("gstt.LaserNumber", gstt.LaserNumber)
    if gstt.LaserNumber == -1:
        log.infog("Autodetected DACs mode")
        commands.DAChecks()
        print("dacs", gstt.dacs)

    else: 
        log.infog("Resquested DACs mode")

    lasernumber = gstt.LaserNumber -1
    print("LaserNumber = ", gstt.LaserNumber)

    
    log.info("Starting "+str(gstt.LaserNumber) + " DACs process...")
    
    # Launch one process (a tracer3 instance) by etherdream
    dac_worker0= Process(target=dac_process, args=(0,0,))
    dac_worker0.start()
    commands.worker0 = dac_worker0
    print("Tracer 0 : name", dac_worker0.name , "pid", dac_worker0.pid )
    
    if lasernumber >0:
        dac_worker1= Process(target=dac_process, args=(1,0,))
        commands.worker1 = dac_worker1
        print("Tracer 1 : name", dac_worker1.name , "pid", dac_worker1.pid )
        dac_worker1.start()
    
    if lasernumber >1:
        dac_worker2= Process(target=dac_process, args=(2,0,))
        dac_worker2.start()
        commands.worker2 = dac_worker2
        print("Tracer 2 : name", dac_worker2.name , "pid", dac_worker2.pid )
    
    if lasernumber >2:
        dac_worker3= Process(target=dac_process, args=(3,0,))
        print("Tracer 3 : name", dac_worker3.name , "pid", dac_worker3.pid )
        commands.worker3 = dac_worker3
        dac_worker3.start()
    print("")
    
    #
    #  start WS and OSC servers
    #
    
    try:
        
        # Websocket startup
        wserver = WebsocketServer(wsPORT,host=serverIP)
        plugins.Init(wserver)
        
        log.info("Starting servers...")
        # Launch OSC thread listening to oscserver
        print("Launching OSC server...")
        print("at", oscserverIPin, "port",str(oscserverPORTin))
        oscserver.addMsgHandler( "/noteon", commands.NoteOn)
        oscserver.addMsgHandler( "/scim", commands.Scim)
        oscserver.addMsgHandler( "/line1", commands.Line1)
        oscserver.addMsgHandler( "/forwardui", commands.ForwardUI)
        # Default OSC handler for all OSC incoming message
        oscserver.addMsgHandler("default", handler)
        _thread.start_new_thread(osc_thread, ())
    
        print("Launching webUI Websocket server...")
        print("at", serverIP, "port",wsPORT)
        wserver.set_fn_new_client(new_client)
        wserver.set_fn_client_left(client_left)
        wserver.set_fn_message_received(message_received)
        print("")
        log.info("Resetting all Homographies...")
        for laserid in range(0,gstt.LaserNumber):  
            homographyp.newEDH(laserid)
    
        # plugins autostart
        print("")
        log.info("Plugins startup...")
    
        if gstt.autostart != "":
         
            for pluginname in gstt.autostart.split(","):
                print("Autostarting", pluginname, "...")
                plugins.Start(pluginname)
        
        print("")
        log.infog("LJ server running...")
        
        # websocket loop

        wserver.run_forever()
        
    
    except Exception:
        log.err("Exception")
        traceback.print_exc()
    
    # Gently stop on CTRL C
    
    finally:

        commands.LJautokill()

    '''
    Some code previously used, for reference :
    
    random_points = [(300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 0), (500.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280), (500.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 400.0+random.randint(-100, 100), 65280), (300.0+random.randint(-100, 100), 200.0+random.randint(-100, 100), 65280)]
    '''



