#!/usr/bin/env python3

#      ELEGATO STREAM DECK - XPLANEL by Simfender
#
#	   Thanks to pyXPUDPServer and Python Stream Deck libraries

import json
import os
import struct
import sys
import asyncio
import time
from PIL import Image, ImageDraw, ImageFont
from StreamDeck.DeviceManager import DeviceManager
from StreamDeck.ImageHelpers import PILHelper
import pyxpudpserver as XPUDP
from jsonpath_ng import parse


def int_keys(ordered_pairs):
    result = {}
    for key, value in ordered_pairs:
        try:
            key = int(key)
        except ValueError:
            pass
        result[key] = value
    return result

config_file = 'config.json' # pylint: disable=C0103

if len(sys.argv) == 2:
    print("Loading:"+sys.argv[1])
    config_file = sys.argv[1]

with open(config_file, 'r') as f:
    config = json.load(f, object_pairs_hook=int_keys)

current_folder = config['default_folder']
drefcmds = {}
images_dir = config['img_folder']

debug = config['debug']

XPUDP.pyXPUDPServer.initialiseUDP((config['SERVER_IP'], config['SERVER_PORT']), (
    config['XPLANE_IP'], config['XPLANE_PORT']), 'XPLANEL')
XPUDP.pyXPUDPServer.start()

# Generates a custom tile with run-time generated text and custom image via the
# PIL module.


def render_key_image(deck, icon_filename, title, value):
    # Create new key image of the correct dimensions, black background

    image = PILHelper.create_image(deck)
    draw = ImageDraw.Draw(image)

    # Add image overlay, rescaling the image asset if it is too large to fit
    # the requested dimensions via a high quality Lanczos scaling algorithm
    if icon_filename:
        icon = Image.open(icon_filename).convert("RGBA")
        icon.thumbnail((image.width, image.height), Image.LANCZOS)
        image.paste(icon)
    if value:
        font = ImageFont.truetype('arial', 22)
        draw.text((image.width - 5, image.height / 2),
                  text=str(int(value)), font=font, anchor="rs", fill="white")
    if title:
        font = ImageFont.truetype('arial', 12)
        draw.text((image.width / 2, image.height - 5), text=title,
                  font=font, anchor="ms", fill="white")

    return PILHelper.to_native_format(deck, image)


# Creates a new key image based on the key index, style and current key state
# and updates the image on the StreamDeck.
def update_key_image(deck, key, image_in, title, value=None):
    # Generate the custom key with the requested image and label
    image = render_key_image(deck, image_in, title, value)

    # Update requested key with the generated image
    deck.set_key_image(key, image)



def change_current_folder(deck, folder):

    global drefcmds, current_folder, change_folder

    drefcmds = {}

    current_folder == folder
    drefcmds = config[folder]

    deck.reset()

    # Set initial screen brightness to 30%
    deck.set_brightness(100)

    for idx in drefcmds.keys():
        img_files = True
        value = None
        if drefcmds[idx]['type'] != "display":
            if drefcmds[idx]['type'] == "folder":
                if os.path.isfile(drefcmds[idx]["name"]):
                    drefcmds[idx]['off_img'] = images_dir+drefcmds[idx]['name']
                    drefcmds[idx]['on_img'] = images_dir+drefcmds[idx]['name']
                else:
                    drefcmds[idx]['off_img'] = 'images/folder.png'
                    drefcmds[idx]['on_img'] = 'images/folder.png'
            else:
                drefcmds[idx]['off_img'] = images_dir+drefcmds[idx]['name']+'.off.png'
                drefcmds[idx]['on_img'] = images_dir+drefcmds[idx]['name']+'.on.png'

            if not os.path.isfile(drefcmds[idx]['off_img']):
                print("Error: Off image for " + drefcmds[idx]['name'] + " not found")
                img_files = False
            if not os.path.isfile(drefcmds[idx]['on_img']):
                print("Error: On image for " + drefcmds[idx]['name'] + " not found")
                img_files = False
        else:
            drefcmds[idx]['off_img'] = None
            drefcmds[idx]['on_img'] = None
            value = XPUDP.pyXPUDPServer.getData(drefcmds[idx]['dataref'])
        if 'title' not in drefcmds[idx]:
            drefcmds[idx]['title'] = None
        drefcmds[idx]['value'] = value
        drefcmds[idx]['old_value'] = value
        update_key_image(deck, idx, drefcmds[idx]['off_img'], drefcmds[idx]['title'], drefcmds[idx]['value'])
        drefcmds[idx]['state'] = False
        drefcmds[idx]['old_state'] = False

# Prints key state change information, updates rhe key image and performs any
# associated actions when a key is pressed.


async def key_change_callback(deck, key, state):

    global current_folder
    commands = []
    try:
        # Check if the key is changing to the pressed state
        if state:
            # Print new key state
            if debug:
                print(drefcmds[key]['name']+"|{}|{}".format(key, drefcmds[key]['state']))
            update_deck()

            if drefcmds[key]['type'] == "folder":
                current_folder = drefcmds[key]['name']
                change_current_folder(deck, drefcmds[key]['name'])
            else:
                # When an exit button is pressed, close the application
                if drefcmds[key]['name'] == "exit":
                    # Reset deck, clearing all button images
                    deck.reset()
                    # Close deck handle, terminating internal worker threads
                    deck.close()
                    inner_cycle = False
                else:
                    if drefcmds[key]['type'] == "single":
                        commands.append(drefcmds[key]['command'])
                    if drefcmds[key]['type'] == "toggle":
                        if drefcmds[key]['state']:
                            commands.append(drefcmds[key]['off_command'])
                        else:
                            commands.append(drefcmds[key]['on_command'])
                    if drefcmds[key]['type'] == "multiple":
                        if drefcmds[key]['state']:
                            commands = drefcmds[key]['off_commands']
                        else:
                            commands = drefcmds[key]['on_commands']
                    for command in commands:
                        XPUDP.pyXPUDPServer.sendXPCmd(command)
    except KeyError:
        print('NA')


def update_deck():

    global drefcmds
    for index, deck in enumerate(streamdecks):
        old_states_expression = parse('$..old_state')
        old_states = [match.value for match in old_states_expression.find(drefcmds)]
        states_expression = parse('$..state')
        states = [match.value for match in states_expression.find(drefcmds)]
        if old_states != states:
            for idx in drefcmds.keys():
                if drefcmds[idx]['state'] != drefcmds[idx]['old_state']:
                    if drefcmds[idx]['state']:
                        update_key_image(deck, idx, drefcmds[idx]['on_img'], drefcmds[idx]['title'])
                    else:
                        update_key_image(deck, idx, drefcmds[idx]['off_img'], drefcmds[idx]['title'])
                    drefcmds[idx]['old_state'] = drefcmds[idx]['state']
        old_values_expression = parse('$..old_value')
        old_values = [match.value for match in old_values_expression.find(drefcmds)]
        values_expression = parse('$..state')
        values = [match.value for match in values_expression.find(drefcmds)]
        if old_values != values:
            for idx in drefcmds.keys():
                if drefcmds[idx]['value'] != drefcmds[idx]['old_value']:
                    update_key_image(deck, idx, None, drefcmds[idx]['title'], drefcmds[idx]['value'])
                drefcmds[idx]['value_state'] = drefcmds[idx]['value']


async def main_function(lock):

    global streamdecks
    streamdecks = DeviceManager().enumerate()

    global inner_cycle
    inner_cycle = True

    if debug:
        print("Found {} Stream Deck(s).\n".format(len(streamdecks)))

    for index, deck in enumerate(streamdecks):
        deck.open()
        change_current_folder(deck, current_folder)
        deck.set_key_callback_async(key_change_callback, loop)


async def sub_function(lock):

    while True:
        await asyncio.sleep(0.1)
        async with lock:
            try:
                for idx in drefcmds.keys():
                    if drefcmds[idx]['type'] != 'folder':
                        val = XPUDP.pyXPUDPServer.getData(drefcmds[idx]['dataref'])
                        if debug:
                            print(drefcmds[idx]['name'], val)
                        if drefcmds[idx]['type'] != 'display':
                            if val == drefcmds[idx]['dataref_value']:
                                drefcmds[idx]['state'] = True
                                update_deck()
                            else:
                                drefcmds[idx]['state'] = False
                                update_deck()
                        else:
                            if drefcmds[idx]['value'] != val:
                                drefcmds[idx]['value'] = val
                                update_deck()
            except Exception as e:
                print('NA')

if __name__ == "__main__":

    lock = asyncio.Lock()
    # this is the event loop
    loop = asyncio.get_event_loop()
    # schedule both the coroutines to run on the event loop
    loop.run_until_complete(asyncio.gather(main_function(lock), sub_function(lock)))
