#!/usr/bin/env python

import argparse
import asyncio
import importlib
import json
import os
import socket
import sys
import traceback

import paho.mqtt.client as mqtt

from xknx import XKNX
from xknx.dpt.dpt import DPTArray, DPTBinary
from xknx.io.knxip_interface import ConnectionType, ConnectionConfig
from xknx.telegram.address import GroupAddress, IndividualAddress
from xknx.telegram.apci import GroupValueWrite
from xknx.telegram import Telegram, TelegramDirection


XKNX_DPT_MODULE_STR = "xknx.dpt"


knx_tunnel = None
mqtt_client = None
daemon_args = None
item_states = None


def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)


def get_dpt_type_for_address(group_address):
    global daemon_args

    dpt_type = None

    if group_address in daemon_args.dpt_types:
        dpt_type = daemon_args.dpt_types[group_address]

    return dpt_type


def extract_payload_from_telegram(group_address, telegram):
    dpt_type = get_dpt_type_for_address(group_address)
    payload = telegram.payload

    print("Address: {}, DPT Type: {}, Payload: {}".format(group_address, dpt_type, payload))

    value = None

    try:
        if dpt_type == 'DPTBinary':
            value = str(bool(payload.value.value)).lower()
        else:
            if dpt_type is None:
                dpt_type = payload.value.__class__.__name__
            dpt_class = getattr(importlib.import_module(XKNX_DPT_MODULE_STR), dpt_type)
            value = dpt_class.from_knx(payload.value)
    except Exception as e:
        eprint(traceback.format_exc())

    return value


def create_payload_for_telegram(group_address, value):
    dpt_type = get_dpt_type_for_address(group_address)

    print("Address: {}, DPT Type: {}, Value: {}".format(group_address, dpt_type, value))

    payload = None

    try:
        if dpt_type == 'DPTBinary' and type(value) == str:
            payload = DPTBinary(int(value.lower() == 'true'))
        elif dpt_type == 'DPTBinary' and type(value) == bool:
            payload = DPTBinary(int(value == True))
        else:
            dpt_class = getattr(importlib.import_module(XKNX_DPT_MODULE_STR), dpt_type)
            payload = DPTArray(dpt_class.to_knx(value))
    except Exception as e:
        eprint(traceback.format_exc())

    return payload


def publish_to_knx(address, payload):
    global daemon_args, knx_tunnel

    print("Address: {}, Payload: {}".format(address, payload))

    source_address = IndividualAddress(daemon_args.knx_individual_address)
    group_address = GroupAddress(address)
    group_value = GroupValueWrite(payload)

    telegram = Telegram(
        destination_address=group_address,
        direction=TelegramDirection.OUTGOING,
        payload=group_value,
        source_address=source_address
    )

    if knx_tunnel.started:
        if daemon_args.knx_no_queue:
            asyncio.run(knx_tunnel.telegram_queue.process_telegram_outgoing(telegram))
        else:
            asyncio.run(knx_tunnel.telegrams.put(telegram))


def publish_to_mqtt(address, value):
    global mqtt_client, daemon_args, item_states

    topic = "{}/{}".format(daemon_args.mqtt_topic, address)

    item_states[address] = str(value)

    print("Topic: {}, Payload: {}".format(topic, value))
    mqtt_client.publish(topic, value)


async def on_telegram_received(telegram):
    try:
        if telegram.direction != TelegramDirection.INCOMING:
            return
        group_address = str(telegram.destination_address)
        payload = extract_payload_from_telegram(group_address, telegram)

        publish_to_mqtt(group_address, payload)
    except Exception as e:
        eprint(traceback.format_exc())


def on_mqtt_connect(client, userdata, flags, rc):
    global daemon_args

    for address in daemon_args.mqtt_subscribe:
        topic = "{}/{}".format(daemon_args.mqtt_topic, address)
        print("Subscribe: {}".format(topic))
        client.subscribe(topic)


def on_mqtt_received(client, userdata, message):
    global daemon_args, item_states

    try:
        group_address = message.topic.replace("{}/".format(daemon_args.mqtt_topic), '')
        value = str(message.payload.decode())

        if item_states[group_address] == value:
            print("Received value for {} is last state sent: {}".format(group_address, value))
            return

        payload = create_payload_for_telegram(group_address, value)

        publish_to_knx(group_address, payload)
    except Exception as e:
        eprint(traceback.format_exc())


def start_mqtt():
    global daemon_args
    mqtt_client = mqtt.Client(daemon_args.mqtt_clientid)
    if daemon_args.verbose:
        mqtt_client.enable_logger()
    if daemon_args.mqtt_user is not None and daemon_args.mqtt_password is not None:
        mqtt_client.username_pw_set(daemon_args.mqtt_user, daemon_args.mqtt_password)
    mqtt_client.on_connect = on_mqtt_connect
    mqtt_client.on_message = on_mqtt_received
    mqtt_client.connect(daemon_args.mqtt_host, daemon_args.mqtt_port, daemon_args.mqtt_keepalive)
    mqtt_client.loop_start()
    return mqtt_client


def start_knx():
    global daemon_args

    config = ConnectionConfig(
            connection_type=ConnectionType.TUNNELING,
            local_ip=daemon_args.knx_local_ip,
            gateway_ip=socket.gethostbyname(daemon_args.knx_host),
            gateway_port=daemon_args.knx_port
        )

    async def run():
        global knx_tunnel
        knx_tunnel = XKNX(daemon_mode=True, connection_config=config)
        knx_tunnel.telegram_queue.register_telegram_received_cb(
            telegram_received_cb=on_telegram_received
        )
        await knx_tunnel.start()
        await knx_tunnel.stop()

    asyncio.run(run())


def parse_args():
    parser = argparse.ArgumentParser(
            prog='knx2mqtt',
            description='A KNX to MQTT bridge',
            epilog='Have a lot of fun!')
    parser.add_argument('-m', '--mqtt_host', type=str,
            default='localhost',
            help='The hostname of the MQTT server. Default is localhost')
    parser.add_argument('--mqtt_port', type=int,
            default=1883,
            help='The port of the MQTT server. Default is 1883')
    parser.add_argument('--mqtt_keepalive', type=int,
            default=30,
            help='The keep alive interval for the MQTT server connection in seconds. Default is 30')
    parser.add_argument('--mqtt_clientid', type=str,
            default='knx2mqtt',
            help='The clientid to send to the MQTT server. Default is knx2mqtt')
    parser.add_argument('-u', '--mqtt_user', type=str,
            help='The username for the MQTT server connection.')
    parser.add_argument('-p', '--mqtt_password', type=str,
            help='The password for the MQTT server connection.')
    parser.add_argument('-t', '--mqtt_topic', type=str,
            default='bus/knx',
            help='The topic to publish MQTT message. Default is bus/knx')
    parser.add_argument('-k', '--knx_host', type=str,
            default='localhost',
            help='The hostname of the KNX gateway. Default is localhost')
    parser.add_argument('--knx_port', type=int,
            default=3671,
            help='The port of the KNX gateway. Default is 3671')
    parser.add_argument('--knx_local_ip', type=str,
            help='The local ip address to connect the KNX gateway.')
    parser.add_argument('--knx_individual_address', type=str,
            default='15.15.248',
            help='The group address to send KNX telegrams from. Default is 15.15.248')
    parser.add_argument('--knx_no_queue',
            default=False,
            action='store_true',
            help='Disable XKNX telegram queuing.')
    parser.add_argument('-c', '--config', type=str,
            default='/etc/knx2mqtt.conf',
            help='The path to the config file. Default is /etc/knx2mqtt.conf')
    parser.add_argument('-v', '--verbose',
            default=False,
            action='store_true',
            help='Be verbose while running.')
    args = parser.parse_args()
    return args


def parse_config():
    global daemon_args

    daemon_args.items = []

    if not os.path.isfile(daemon_args.config):
        return

    with open(daemon_args.config, "r") as config_file:
        data = json.load(config_file)
        if 'mqtt_host' in data:
            daemon_args.mqtt_host = data['mqtt_host']
        if 'mqtt_port' in data:
            daemon_args.mqtt_port = int(data['mqtt_port'])
        if 'mqtt_keepalive' in data:
            daemon_args.mqtt_keepalive = int(data['mqtt_keepalive'])
        if 'mqtt_clientid' in data:
            daemon_args.mqtt_clientid = data['mqtt_clientid']
        if 'mqtt_user' in data:
            daemon_args.mqtt_user = data['mqtt_user']
        if 'mqtt_password' in data:
            daemon_args.mqtt_password = data['mqtt_password']
        if 'mqtt_topic' in data:
            daemon_args.mqtt_topic = data['mqtt_topic']
        if 'knx_host' in data:
            daemon_args.knx_host = data['knx_host']
        if 'knx_port' in data:
            daemon_args.knx_port = int(data['knx_port'])
        if 'knx_local_ip' in data:
            daemon_args.knx_local_ip = data['knx_local_ip']
        if 'knx_individual_address' in data:
            daemon_args.knx_individual_address = data['knx_individual_address']
        if 'knx_no_queue' in data:
            daemon_args.knx_no_queue = data['knx_no_queue']
        if 'items' in data:
            daemon_args.items = data['items']


def init_items():
    global daemon_args, item_states

    daemon_args.dpt_types = {}
    daemon_args.mqtt_subscribe = []
    item_states = {}

    for item in daemon_args.items:
        if 'address' in item:
            item_states[item['address']] = None
            if 'type' in item:
                daemon_args.dpt_types[item['address']] = item['type']
            if 'mqtt_subscribe' in item and item['mqtt_subscribe']:
                daemon_args.mqtt_subscribe.append(item['address'])


def main():
    global daemon_args, mqtt_client
    daemon_args = parse_args()
    parse_config()
    init_items()
    mqtt_client = start_mqtt()
    start_knx()


if __name__ == "__main__":
    main()
