#!/usr/bin/env python3

import datetime
import getpass
import logging
import os
import platform
import sys

from typing import Any, Union

import uno
import unohelper
from com.sun.star.beans import PropertyValue, NamedValue
from com.sun.star.datatransfer import XTransferable, DataFlavor


__all__ = [
    'DESKTOP',
    'INFO_DEBUG',
    'IS_MAC',
    'IS_WIN',
    'LANG',
    'LANGUAGE',
    'NAME',
    'OS',
    'PC',
    'USER',
    'VERSION',
    'ClipBoard',
    'LOMain',
    # ~ 'create_instance',
    'data_to_dict',
    'dict_to_property',
    'get_app_config',
    ]


CTX = uno.getComponentContext()
SM = CTX.getServiceManager()


# Global variables
OS = platform.system()
DESKTOP = os.environ.get('DESKTOP_SESSION', '')
PC = platform.node()
USER = getpass.getuser()
IS_WIN = OS == 'Windows'
IS_MAC = OS == 'Darwin'


LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
if IS_WIN:
    logging.addLevelName(logging.ERROR, 'ERROR')
    logging.addLevelName(logging.DEBUG, 'DEBUG')
    logging.addLevelName(logging.INFO, 'INFO')
else:
    logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
    logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
    logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)


def create_instance(name: str, with_context: bool=False, arguments: Any=None) -> Any:
    """Create a service instance

    :param name: Name of service
    :type name: str
    :param with_context: If used context
    :type with_context: bool
    :param argument: If needed some argument
    :type argument: Any
    :return: PyUno instance
    :rtype: PyUno Object
    """

    if with_context:
        instance = SM.createInstanceWithContext(name, CTX)
    elif arguments:
        instance = SM.createInstanceWithArguments(name, (arguments,))
    else:
        instance = SM.createInstance(name)

    return instance


def get_app_config(node_name: str, key: str='') -> Any:
    """Get any key from any node from LibreOffice configuration.

    :param node_name: Name of node
    :type name: str
    :param key: Name of key
    :type key: str
    :return: Any value
    :rtype: Any

    `See Api ConfigurationProvider <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1configuration_1_1ConfigurationProvider.html>`_
    """

    name = 'com.sun.star.configuration.ConfigurationProvider'
    service = 'com.sun.star.configuration.ConfigurationAccess'
    cp = create_instance(name, True)
    node = PropertyValue(Name='nodepath', Value=node_name)
    value = ''

    try:
        value = cp.createInstanceWithArguments(service, (node,))
        if value and value.hasByName(key):
            value = value.getPropertyValue(key)
    except Exception as e:
        log.error(e)
        value = ''

    return value


# Get info LibO
NAME = TITLE = get_app_config('/org.openoffice.Setup/Product', 'ooName')
VERSION = get_app_config('/org.openoffice.Setup/Product','ooSetupVersion')
LANGUAGE = get_app_config('/org.openoffice.Setup/L10N/', 'ooLocale')
LANG = LANGUAGE.split('-')[0]

# Get start date from Calc configuration
node = '/org.openoffice.Office.Calc/Calculate/Other/Date'
year = get_app_config(node, 'YY')
month = get_app_config(node, 'MM')
day = get_app_config(node, 'DD')
DATE_OFFSET = datetime.date(year, month, day).toordinal()

_info_debug = f"Python: {sys.version}\n\n{platform.platform()}\n\n" + '\n'.join(sys.path)
INFO_DEBUG = f"{NAME} v{VERSION} {LANGUAGE}\n\n{_info_debug}"


def dict_to_property(values: dict, uno_any: bool=False):
    """Convert dictionary to array of PropertyValue

    :param values: Dictionary of values
    :type values: dict
    :param uno_any: If return like array uno.Any
    :type uno_any: bool
    :return: Tuple of PropertyValue or array uno.Any
    :rtype: tuples or uno.Any
    """
    ps = tuple([PropertyValue(Name=n, Value=v) for n, v in values.items()])
    if uno_any:
        ps = uno.Any('[]com.sun.star.beans.PropertyValue', ps)
    return ps


def data_to_dict(data: Union[tuple, list]) -> dict:
    """Convert tuples, list, PropertyValue, NamedValue to dictionary

    :param data: Iterator of values
    :type data: array of tuples, list, PropertyValue or NamedValue
    :return: Dictionary
    :rtype: dict
    """
    d = {}

    if isinstance(data[0], (tuple, list)):
        d = {r[0]: r[1] for r in data}
    elif isinstance(data[0], (PropertyValue, NamedValue)):
        d = {r.Name: r.Value for r in data}

    return d


# ~ https://github.com/django/django/blob/main/django/utils/functional.py#L61
class classproperty:

    def __init__(self, method=None):
        self.fget = method

    def __get__(self, instance, cls=None):
        return self.fget(cls)

    def getter(self, method):
        self.fget = method
        return self


class BaseObject():

    def __init__(self, obj):
        self._obj = obj

    def __enter__(self):
        return self

    @property
    def obj(self):
        """Return original pyUno object"""
        return self._obj


class LOMain():
    """Classe for LibreOffice"""

    class commands():
        """Class for disable and enable commands

        `See DispatchCommands <https://wiki.documentfoundation.org/Development/DispatchCommands>`_
        """
        @classmethod
        def _set_app_command(cls, command: str, disable: bool) -> bool:
            """Disable or enabled UNO command

            :param command: UNO command to disable or enabled
            :type command: str
            :param disable: True if disable, False if active
            :type disable: bool
            :return: True if correctly update, False if not.
            :rtype: bool
            """
            NEW_NODE_NAME = f'zaz_disable_command_{command.lower()}'
            name = 'com.sun.star.configuration.ConfigurationProvider'
            service = 'com.sun.star.configuration.ConfigurationUpdateAccess'
            node_name = '/org.openoffice.Office.Commands/Execute/Disabled'

            cp = create_instance(name, True)
            node = PropertyValue(Name='nodepath', Value=node_name)
            update = cp.createInstanceWithArguments(service, (node,))

            result = True
            try:
                if disable:
                    new_node = update.createInstanceWithArguments(())
                    new_node.setPropertyValue('Command', command)
                    update.insertByName(NEW_NODE_NAME, new_node)
                else:
                    update.removeByName(NEW_NODE_NAME)
                update.commitChanges()
            except Exception as e:
                result = False

            return result

        @classmethod
        def disable(cls, command: str) -> bool:
            """Disable UNO command

            :param command: UNO command to disable
            :type command: str
            :return: True if correctly disable, False if not.
            :rtype: bool
            """
            return cls._set_app_command(command, True)

        @classmethod
        def enabled(cls, command) -> bool:
            """Enabled UNO command

            :param command: UNO command to enabled
            :type command: str
            :return: True if correctly disable, False if not.
            :rtype: bool
            """
            return cls._set_app_command(command, False)

    @classmethod
    def fonts(cls):
        """Get all font visibles in LibreOffice

        :return: tuple of FontDescriptors
        :rtype: tuple

        `See API FontDescriptor <https://api.libreoffice.org/docs/idl/ref/structcom_1_1sun_1_1star_1_1awt_1_1FontDescriptor.html>`_
        """
        toolkit = create_instance('com.sun.star.awt.Toolkit')
        device = toolkit.createScreenCompatibleDevice(0, 0)
        return device.FontDescriptors

    @classmethod
    def filters(cls):
        """Get all support filters

        `See Help ConvertFilters <https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html>`_
        `See API FilterFactory <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1FilterFactory.html>`_
        """
        factory = create_instance('com.sun.star.document.FilterFactory')
        rows = [data_to_dict(factory[name]) for name in factory]
        for row in rows:
            row['UINames'] = data_to_dict(row['UINames'])
        return rows

    @classmethod
    def dispatch(cls, frame: Any, command: str, args: dict={}) -> None:
        """Call dispatch, used only if not exists directly in API

        :param frame: doc or frame instance
        :type frame: pyUno
        :param command: Command to execute
        :type command: str
        :param args: Extra argument for command
        :type args: dict

        `See DispatchCommands <`See DispatchCommands <https://wiki.documentfoundation.org/Development/DispatchCommands>`_>`_
        """
        dispatch = create_instance('com.sun.star.frame.DispatchHelper')
        if hasattr(frame, 'frame'):
            frame = frame.frame
        url = command
        if not command.startswith('.uno:'):
            url = f'.uno:{command}'
        opt = dict_to_property(args)
        dispatch.executeDispatch(frame, url, '', 0, opt)
        return


class ClipBoard(object):
    SERVICE = 'com.sun.star.datatransfer.clipboard.SystemClipboard'
    CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16'

    class TextTransferable(unohelper.Base, XTransferable):

        def __init__(self, text):
            df = DataFlavor()
            df.MimeType = ClipBoard.CLIPBOARD_FORMAT_TEXT
            df.HumanPresentableName = 'encoded text utf-16'
            self.flavors = (df,)
            self._data = text

        def getTransferData(self, flavor):
            return self._data

        def getTransferDataFlavors(self):
            return self.flavors

    @classmethod
    def set(cls, value):
        ts = cls.TextTransferable(value)
        sc = create_instance(cls.SERVICE)
        sc.setContents(ts, None)
        return

    @classproperty
    def contents(cls):
        df = None
        text = ''
        sc = create_instance(cls.SERVICE)
        transferable = sc.getContents()
        data = transferable.getTransferDataFlavors()
        for df in data:
            if df.MimeType == cls.CLIPBOARD_FORMAT_TEXT:
                break
        if df:
            text = transferable.getTransferData(df)
        return text

    @classmethod
    def get(cls):
        return cls.contents
