#!/usr/bin/env python3

# == Rapid Develop Macros in LibreOffice ==

# ~ This file is part of ZAZ.

# ~ ZAZ is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.

# ~ ZAZ is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# ~ GNU General Public License for more details.

# ~ You should have received a copy of the GNU General Public License
# ~ along with ZAZ.  If not, see <https://www.gnu.org/licenses/>.


import ctypes
import datetime
import errno
import getpass
import json
import logging
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import threading
import time
import zipfile

from collections import OrderedDict
from collections.abc import MutableMapping
from datetime import datetime
from functools import wraps
from pathlib import Path, PurePath
from pprint import pprint
from subprocess import PIPE

import uno
import unohelper
from com.sun.star.beans import PropertyValue
from com.sun.star.awt import MessageBoxButtons as MSG_BUTTONS
from com.sun.star.awt.MessageBoxResults import YES
from com.sun.star.awt.PosSize import POSSIZE, SIZE
from com.sun.star.awt import Size, Point
from com.sun.star.datatransfer import XTransferable, DataFlavor
from com.sun.star.table.CellContentType import EMPTY, VALUE, TEXT, FORMULA

from com.sun.star.text.TextContentAnchorType import AS_CHARACTER

from com.sun.star.lang import XEventListener
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XMouseListener


MSG_LANG = {
    'es': {
        'OK': 'Aceptar',
        'Cancel': 'Cancelar',
        'Select file': 'Seleccionar archivo',
    }
}


OS = platform.system()
USER = getpass.getuser()
PC = platform.node()
DESKTOP = os.environ.get('DESKTOP_SESSION', '')
INFO_DEBUG = '{}\n\n{}\n\n{}'.format(sys.version, platform.platform(), '\n'.join(sys.path))


IS_WIN = OS == 'Windows'
LOG_NAME = 'ZAZ'
CLIPBOARD_FORMAT_TEXT = 'text/plain;charset=utf-16'


CALC = 'calc'
WRITER = 'writer'
OBJ_CELL = 'ScCellObj'
OBJ_RANGE = 'ScCellRangeObj'
OBJ_RANGES = 'ScCellRangesObj'
OBJ_TYPE_RANGES = (OBJ_CELL, OBJ_RANGE, OBJ_RANGES)

TYPE_DOC = {
    'calc': 'com.sun.star.sheet.SpreadsheetDocument',
    'writer': 'com.sun.star.text.TextDocument',
    'impress': 'com.sun.star.presentation.PresentationDocument',
    'draw': 'com.sun.star.drawing.DrawingDocument',
    'base': 'com.sun.star.sdb.OfficeDatabaseDocument',
    'math': 'com.sun.star.formula.FormulaProperties',
    'basic': 'com.sun.star.script.BasicIDE',
}

NODE_MENUBAR = 'private:resource/menubar/menubar'
MENUS_CALC = {
    'file': '.uno:PickList',
    'edit': '.uno:EditMenu',
    'view': '.uno:ViewMenu',
    'insert': '.uno:InsertMenu',
    'format': '.uno:FormatMenu',
    'styles': '.uno:FormatStylesMenu',
    'sheet': '.uno:SheetMenu',
    'data': '.uno:DataMenu',
    'tools': '.uno:ToolsMenu',
    'windows': '.uno:WindowList',
    'help': '.uno:HelpMenu',
}

MENUS_APP = {
    'calc': MENUS_CALC,
}


FILE_NAME_DEBUG = 'zaz-debug.log'
FILE_NAME_CONFIG = 'zaz-config.json'
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
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__)


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


def create_instance(name, with_context=False):
    if with_context:
        instance = SM.createInstanceWithContext(name, CTX)
    else:
        instance = SM.createInstance(name)
    return instance


def _get_app_config(key, node_name):
    name = 'com.sun.star.configuration.ConfigurationProvider'
    service = 'com.sun.star.configuration.ConfigurationAccess'
    cp = create_instance(name, True)
    node = PropertyValue(Name='nodepath', Value=node_name)
    try:
        ca = cp.createInstanceWithArguments(service, (node,))
        if ca and (ca.hasByName(key)):
            data = ca.getPropertyValue(key)
        return data
    except Exception as e:
        log.error(e)
        return ''


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


def mri(obj):
    m = create_instance('mytools.Mri')
    if m is None:
        msg = 'Extension MRI not found'
        error(msg)
        return

    m.inspect(obj)
    return


def catch_exception(f):
    @wraps(f)
    def func(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception as e:
            log.error(f.__name__, exc_info=True)
    return func


class LogWin(object):

    def __init__(self, doc):
        self.doc = doc

    def write(self, info):
        text = self.doc.Text
        cursor = text.createTextCursor()
        cursor.gotoEnd(False)
        text.insertString(cursor, str(info), 0)
        return


def info(data):
    log.info(data)
    return


def debug(info):
    if IS_WIN:
        # ~ app = LOApp(self.ctx, self.sm, self.desktop, self.toolkit)
        # ~ doc = app.getDoc(FILE_NAME_DEBUG)
        # ~ if not doc:
            # ~ doc = app.newDoc(WRITER)
        # ~ out = OutputDoc(doc)
        # ~ sys.stdout = out
        # ~ pprint(info)
        doc = LogWin(new_doc('writer').obj)
        doc.write(info)
        return

    log.debug(str(info))
    return


def error(info):
    log.error(info)
    return


def save_log(path, data):
    with open(path, 'a') as out:
        out.write('{} -{}- '.format(str(datetime.now())[:19], LOG_NAME))
        pprint(data, stream=out)
    return


def run_in_thread(fn):
    def run(*k, **kw):
        t = threading.Thread(target=fn, args=k, kwargs=kw)
        t.start()
        return t
    return run


def get_config(key=''):
    values = {}
    path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG)
    if not exists_path(path):
        return values

    with open(path, 'r', encoding='utf-8') as fh:
        data = fh.read()
        if data:
            values = json.loads(data)

    if key:
        return values.get(key, None)

    return values


def set_config(key, value):
    path = join(get_config_path('UserConfig'), FILE_NAME_CONFIG)
    values = get_config()
    values[key] = value
    with open(path, 'w', encoding='utf-8') as fh:
        json.dump(values, fh, ensure_ascii=False, sort_keys=True, indent=4)
    return True


def sleep(seconds):
    time.sleep(seconds)
    return


def _(msg):
    L = LANGUAGE.split('-')[0]
    if L == 'en':
        return msg

    if not L in MSG_LANG:
        return msg

    return MSG_LANG[L][msg]


def msgbox(message, title=TITLE, buttons=MSG_BUTTONS.BUTTONS_OK, type_msg='infobox'):
    """ Create message box
        type_msg: infobox, warningbox, errorbox, querybox, messbox
        http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1XMessageBoxFactory.html
    """
    toolkit = create_instance('com.sun.star.awt.Toolkit')
    parent = toolkit.getDesktopWindow()
    mb = toolkit.createMessageBox(parent, type_msg, buttons, title, str(message))
    return mb.execute()


def question(message, title=TITLE):
    res = msgbox(message, title, MSG_BUTTONS.BUTTONS_YES_NO, 'querybox')
    return res == YES


def warning(message, title=TITLE):
    return msgbox(message, title, type_msg='warningbox')


def errorbox(message, title=TITLE):
    return msgbox(message, title, type_msg='errorbox')


def get_desktop():
    return create_instance('com.sun.star.frame.Desktop', True)


def get_dispatch():
    return create_instance('com.sun.star.frame.DispatchHelper')


def get_temp_file():
    delete = True
    if IS_WIN:
        delete = False
    return tempfile.NamedTemporaryFile(delete=delete)


def _path_url(path):
    if path.startswith('file://'):
        return path
    return uno.systemPathToFileUrl(path)


def _path_system(path):
    if path.startswith('file://'):
        return os.path.abspath(uno.fileUrlToSystemPath(path))
    return path


def exists_app(name):
    try:
        dn = subprocess.DEVNULL
        subprocess.Popen([name, ''], stdout=dn, stderr=dn).terminate()
    except OSError as e:
        if e.errno == errno.ENOENT:
            return False
    return True

# ~ Delete
def exists(path):
    return Path(path).exists()
def exists_path(path):
    return Path(path).exists()


def get_type_doc(obj):
    # ~ services = {
        # ~ 'calc': 'com.sun.star.sheet.SpreadsheetDocument',
        # ~ 'writer': 'com.sun.star.text.TextDocument',
        # ~ 'impress': 'com.sun.star.presentation.PresentationDocument',
        # ~ 'draw': 'com.sun.star.drawing.DrawingDocument',
        # ~ 'base': 'com.sun.star.sdb.OfficeDatabaseDocument',
        # ~ 'math': 'com.sun.star.formula.FormulaProperties',
        # ~ 'basic': 'com.sun.star.script.BasicIDE',
    # ~ }
    for k, v in TYPE_DOC.items():
        if obj.supportsService(v):
            return k
    return ''


# ~ def _properties(values):
    # ~ p = [PropertyValue(Name=n, Value=v) for n, v in values.items()]
    # ~ return tuple(p)

def dict_to_property(values, uno_any=False):
    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 property_to_dict(values):
    d = {i.Name: i.Value for i in values}
    return d


# ~ Third classes


# ~ https://github.com/psf/requests/blob/v2.22.0/requests/structures.py
class CaseInsensitiveDict(MutableMapping):
    """A case-insensitive ``dict``-like object.
    Implements all methods and operations of
    ``MutableMapping`` as well as dict's ``copy``. Also
    provides ``lower_items``.
    All keys are expected to be strings. The structure remembers the
    case of the last key to be set, and ``iter(instance)``,
    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
    will contain case-sensitive keys. However, querying and contains
    testing is case insensitive::
        cid = CaseInsensitiveDict()
        cid['Accept'] = 'application/json'
        cid['aCCEPT'] == 'application/json'  # True
        list(cid) == ['Accept']  # True
    For example, ``headers['content-encoding']`` will return the
    value of a ``'Content-Encoding'`` response header, regardless
    of how the header name was originally stored.
    If the constructor, ``.update``, or equality comparison
    operations are given keys that have equal ``.lower()``s, the
    behavior is undefined.
    """

    def __init__(self, data=None, **kwargs):
        self._store = OrderedDict()
        if data is None:
            data = {}
        self.update(data, **kwargs)

    def __setitem__(self, key, value):
        # Use the lowercased key for lookups, but store the actual
        # key alongside the value.
        self._store[key.lower()] = (key, value)

    def __getitem__(self, key):
        return self._store[key.lower()][1]

    def __delitem__(self, key):
        del self._store[key.lower()]

    def __iter__(self):
        return (casedkey for casedkey, mappedvalue in self._store.values())

    def __len__(self):
        return len(self._store)

    def lower_items(self):
        """Like iteritems(), but with all lowercase keys."""
        return (
            (lowerkey, keyval[1])
            for (lowerkey, keyval)
            in self._store.items()
        )

    def __eq__(self, other):
        if isinstance(other, Mapping):
            other = CaseInsensitiveDict(other)
        else:
            return NotImplemented
        # Compare insensitively
        return dict(self.lower_items()) == dict(other.lower_items())

    # Copy is required
    def copy(self):
        return CaseInsensitiveDict(self._store.values())

    def __repr__(self):
        return str(dict(self.items()))


# ~ Custom classes


class LODocument(object):

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

    def _init_values(self):
        self._type_doc = get_type_doc(self.obj)
        self._cc = self.obj.getCurrentController()
        return

    @property
    def obj(self):
        return self._obj

    @property
    def type(self):
        return self._type_doc

    @property
    def title(self):
        return self.obj.getTitle()

    @property
    def frame(self):
        return self._cc.getFrame()

    @property
    def is_saved(self):
        return self.obj.hasLocation()

    @property
    def is_modified(self):
        return self.obj.isModified()

    @property
    def is_read_only(self):
        return self.obj.isReadOnly()

    @property
    def path(self):
        return _path_system(self.obj.getURL())

    @property
    def visible(self):
        w = self._cc.getFrame().getContainerWindow()
        return w.Visible
    @visible.setter
    def visible(self, value):
        w = self._cc.getFrame().getContainerWindow()
        w.setVisible(value)

    @property
    def zoom(self):
        return self._cc.ZoomValue
    @zoom.setter
    def zoom(self, value):
        self._cc.ZoomValue = value

    def create_instance(self, name):
        obj = self.obj.createInstance(name)
        return obj

    def save(self, path='', **kwargs):
        # ~ opt = _properties(kwargs)
        opt = dict_to_property(kwargs)
        if path:
            self._obj.storeAsURL(_path_url(path), opt)
        else:
            self._obj.store()
        return True

    def close(self):
        self.obj.close(True)
        return

    def focus(self):
        w = self._cc.getFrame().getComponentWindow()
        w.setFocus()
        return

    def paste(self):
        sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard')
        transferable = sc.getContents()
        self._cc.insertTransferable(transferable)
        return self.obj.getCurrentSelection()


class LOCalc(LODocument):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def obj(self):
        return self._obj

    @property
    def active(self):
        return LOCalcSheet(self._cc.getActiveSheet(), self)

    @property
    def selection(self):
        sel = self.obj.getCurrentSelection()
        if sel.ImplementationName in OBJ_TYPE_RANGES:
            sel = LOCellRange(sel, self)
        return sel

    def get_cell(self, index=None):
        """
            index is str 'A1'
            index is tuple (row, col)
        """
        if index is None:
            cell = self.selection.first
        else:
            cell = LOCellRange(self.active[index].obj, self)
        return cell

    def select(self, rango):
        r = rango
        if hasattr(rango, 'obj'):
            r = rango.obj
        elif isinstance(rango, str):
            r = self.get_cell(rango).obj
        self._cc.select(r)
        return


class LOCalcSheet(object):

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc
        self._init_values()

    def __getitem__(self, index):
        return LOCellRange(self.obj[index], self.doc)

    def _init_values(self):
        return

    @property
    def obj(self):
        return self._obj

    @property
    def doc(self):
        return self._doc


class LOWriter(LODocument):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def obj(self):
        return self._obj

    @property
    def string(self):
        return self._obj.getText().String

    @property
    def text(self):
        return self._obj.getText()

    @property
    def cursor(self):
        return self.text.createTextCursor()

    @property
    def selection(self):
        sel = self._cc.getSelection()
        return LOTextRange(sel[0])

    def insert_content(self, cursor, data, replace=False):
        self.text.insertTextContent(cursor, data, replace)
        return

    # ~ tt = doc.createInstance('com.sun.star.text.TextTable')
    # ~ tt.initialize(5, 2)

    # ~ f = doc.createInstance('com.sun.star.text.TextFrame')
    # ~ f.setSize(Size(10000, 500))

    def insert_image(self, path, **kwargs):
        cursor = kwargs.get('cursor', self.selection.cursor.getEnd())
        w = kwargs.get('width', 1000)
        h = kwargs.get('Height', 1000)
        image = self.create_instance('com.sun.star.text.GraphicObject')
        image.GraphicURL = _path_url(path)
        image.AnchorType = AS_CHARACTER
        image.Width = w
        image.Height = h
        self.insert_content(cursor, image)
        return


class LOTextRange(object):

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

    @property
    def obj(self):
        return self._obj

    @property
    def string(self):
        return self.obj.String

    @property
    def text(self):
        return self.obj.getText()

    @property
    def cursor(self):
        return self.text.createTextCursorByRange(self.obj)


class LOBase(LODocument):

    def __init__(self, obj):
        super().__init__(obj)


class LODrawImpress(LODocument):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def draw_page(self):
        return self._cc.getCurrentPage()

    @catch_exception
    def insert_image(self, path, **kwargs):
        w = kwargs.get('width', 3000)
        h = kwargs.get('Height', 1000)
        x = kwargs.get('X', 1000)
        y = kwargs.get('Y', 1000)

        image = self.create_instance('com.sun.star.drawing.GraphicObjectShape')
        image.GraphicURL = _path_url(path)
        image.Size = Size(w, h)
        image.Position = Point(x, y)
        self.draw_page.add(image)
        return


class LOImpress(LODrawImpress):

    def __init__(self, obj):
        super().__init__(obj)


class LODraw(LODrawImpress):

    def __init__(self, obj):
        super().__init__(obj)


class LOMath(LODocument):

    def __init__(self, obj):
        super().__init__(obj)


class LOBasicIde(LODocument):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def selection(self):
        sel = self._cc.getSelection()
        return sel


class LOCellRange(object):

    def __init__(self, obj, doc):
        self._obj = obj
        self._doc = doc
        self._init_values()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass

    def __getitem__(self, index):
        return LOCellRange(self.obj[index], self.doc)

    def _init_values(self):
        self._type_obj = self.obj.ImplementationName
        self._type_content = EMPTY

        if self._type_obj == OBJ_CELL:
            self._type_content = self.obj.getType()
        return

    @property
    def obj(self):
        return self._obj

    @property
    def doc(self):
        return self._doc

    @property
    def type(self):
        return self._type_obj

    @property
    def type_content(self):
        return self._type_content

    @property
    def first(self):
        if self.type == OBJ_RANGES:
            obj = LOCellRange(self.obj[0][0,0], self.doc)
        else:
            obj = LOCellRange(self.obj[0,0], self.doc)
        return obj

    @property
    def value(self):
        v = None
        if self._type_content == VALUE:
            v = self.obj.getValue()
        elif self._type_content == TEXT:
            v = self.obj.getString()
        elif self._type_content == FORMULA:
            v = self.obj.getFormula()
        return v
    @value.setter
    def value(self, data):
        if isinstance(data, str):
            if data.startswith('='):
                self.obj.setFormula(data)
            else:
                self.obj.setString(data)
        elif isinstance(data, (int, float)):
            self.obj.setValue(data)

    @property
    def data(self):
        return self.obj.getDataArray()
    @data.setter
    def data(self, values):
        if isinstance(values, list):
            values = tuple(values)
        self.obj.setDataArray(values)

    def offset(self, col=1, row=0):
        a = self.address
        col = a.Column + col
        row = a.Row + row
        return LOCellRange(self.sheet[row,col], self.doc)

    @property
    def sheet(self):
        return self.obj.Spreadsheet

    @property
    def draw_page(self):
        return self.sheet.getDrawPage()

    @property
    def name(self):
        return self.obj.AbsoluteName

    @property
    def address(self):
        if self._type_obj == OBJ_CELL:
            a = self.obj.getCellAddress()
        elif self._type_obj == OBJ_RANGE:
            a = self.obj.getRangeAddress()
        else:
            a = self.obj.getRangeAddressesAsString()
        return a

    @property
    def current_region(self):
        cursor = self.sheet.createCursorByRange(self.obj[0,0])
        cursor.collapseToCurrentRegion()
        return LOCellRange(self.sheet[cursor.AbsoluteName], self.doc)

    def insert_image(self, path, **kwargs):
        s = self.obj.Size
        w = kwargs.get('width', s.Width)
        h = kwargs.get('Height', s.Height)
        img = self.doc.create_instance('com.sun.star.drawing.GraphicObjectShape')
        img.GraphicURL = _path_url(path)
        self.draw_page.add(img)
        img.Anchor = self.obj
        img.setSize(Size(w, h))
        return

    def select(self):
        self.doc._cc.select(self.obj)
        return


class EventsListenerBase(unohelper.Base, XEventListener):

    def __init__(self, controller, window=None):
        self._controller = controller
        self._window = window

    def disposing(self, event):
        self._controller = None
        if not self._window is None:
            self._window.setMenuBar(None)


class EventsButton(EventsListenerBase, XActionListener):

    def __init__(self, controller):
        super().__init__(controller)

    def actionPerformed(self, event):
        name = event.Source.Model.Name
        event_name = '{}_action'.format(name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return


class EventsMouse(EventsListenerBase, XMouseListener):

    def __init__(self, controller):
        super().__init__(controller)

    def mousePressed(self, event):
        name = event.Source.Model.Name
        event_name = '{}_click'.format(name)
        if event.ClickCount == 2:
            event_name = '{}_double_click'.format(name)
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(event)
        return

    def mouseReleased(self, event):
        pass

    def mouseEntered(self, event):
        pass

    def mouseExited(self, event):
        pass


class UnoBaseObject(object):

    def __init__(self, obj):
        self._obj = obj
        self._model = self.obj.Model
        self._rules = {}

    @property
    def obj(self):
        return self._obj

    @property
    def model(self):
        return self._model

    @property
    def name(self):
        return self.model.Name

    @property
    def parent(self):
        return self.obj.getContext()

    @property
    def x(self):
        return self.model.PositionX
    @x.setter
    def x(self, value):
        self.model.PositionX = value

    @property
    def y(self):
        return self.model.PositionY
    @y.setter
    def y(self, value):
        self.model.PositionY = value

    @property
    def width(self):
        return self._model.Width
    @width.setter
    def width(self, value):
        self._model.Width = value

    @property
    def height(self):
        return self._model.Height
    @height.setter
    def height(self, value):
        self._model.Height = value

    @property
    def tag(self):
        return self.model.Tag
    @tag.setter
    def tag(self, value):
        self.model.Tag = value

    @property
    def step(self):
        return self.model.Step
    @step.setter
    def step(self, value):
        self.model.Step = value

    @property
    def rules(self):
        return self._rules
    @rules.setter
    def rules(self, value):
        self._rules = value

    def set_focus(self):
        self.obj.setFocus()
        return

    def center(self, horizontal=True, vertical=False):
        p = self.parent.Model
        w = p.Width
        h = p.Height
        if horizontal:
            x = w / 2 - self.width / 2
            self.x = x
        if vertical:
            y = h / 2 - self.height / 2
            self.y = y
        return

    def move(self, origin, x=0, y=5):
        w = 0
        h = 0
        if x:
            w = origin.width
        if y:
            h = origin.height
        x = origin.x + x + w
        y = origin.y + y + h
        self.x = x
        self.y = y
        return


class UnoLabel(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def value(self):
        return self.model.Label
    @value.setter
    def value(self, value):
        self.model.Label = value


class UnoButton(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        # ~ self._set_icon()

    def _set_icon(self):
        icon_name = self.tag.strip()
        if icon_name:
            path_icon = _file_url('{}/img/{}'.format(CURRENT_PATH, icon_name))
            self._model.ImageURL = path_icon
            if self.value:
                self._model.ImageAlign = 0
        return

    @property
    def value(self):
        return self.model.Label
    @value.setter
    def value(self, value):
        self.model.Label = value


class UnoText(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)

    @property
    def value(self):
        return self.model.Text
    @value.setter
    def value(self, value):
        self.model.Text = value

    def validate(self):

        return


class UnoListBox(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._data = []

    @property
    def value(self):
        return self.obj.SelectedItem

    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, values):
        self._data = list(sorted(values))
        self.model.StringItemList = self.data
        return


class LODialog(object):

    def __init__(self, properties):
        self._obj = self._create(properties)
        self._init_values()

    def _init_values(self):
        self._model = self._obj.Model
        self._init_controls()
        self._events = None
        # ~ self._response = None
        return

    def _create(self, properties):
        path = properties.pop('Path', '')
        if path:
            dp = create_instance('com.sun.star.awt.DialogProvider2', True)
            return dp.createDialog(_path_url(path))

        if 'Library' in properties:
            location = properties['Location']
            if location == 'user':
                location = 'application'
            dp = create_instance('com.sun.star.awt.DialogProvider2', True)
            path = 'vnd.sun.star.script:{}.{}?location={}'.format(
                properties['Library'], properties['Name'], location)
            return dp.createDialog(path)

        dlg = create_instance('com.sun.star.awt.UnoControlDialog', True)
        model = create_instance('com.sun.star.awt.UnoControlDialogModel', True)
        toolkit = create_instance('com.sun.star.awt.Toolkit', True)
        set_properties(model, properties)
        dlg.setModel(model)
        dlg.setVisible(False)
        dlg.createPeer(toolkit, None)

        return dlg

    def _init_controls(self):

        return

    @property
    def obj(self):
        return self._obj

    @property
    def model(self):
        return self._model

    @property
    def events(self):
        return self._events
    @events.setter
    def events(self, controllers):
        self._events = controllers
        self._connect_listeners()

    def _connect_listeners(self):

        return

    def _add_listeners(self, control):
        if self.events is None:
            return

        listeners = {
            'addActionListener': EventsButton,
            'addMouseListener': EventsMouse,
        }
        for key, value in listeners.items():
            if hasattr(control.obj, key):
                getattr(control.obj, key)(listeners[key](self.events))
        return

    def open(self):
        return self.obj.execute()

    def close(self, value=0):
        return self.obj.endDialog(value)

    def _get_control_model(self, control):
        services = {
            'label': 'com.sun.star.awt.UnoControlFixedTextModel',
            'button': 'com.sun.star.awt.UnoControlButtonModel',
            'text': 'com.sun.star.awt.UnoControlEditModel',
            'listbox': 'com.sun.star.awt.UnoControlListBoxModel',
            'link': 'com.sun.star.awt.UnoControlFixedHyperlinkModel',
            'roadmap': 'com.sun.star.awt.UnoControlRoadmapModel',
            'image': 'com.sun.star.awt.UnoControlImageControlModel',
            'groupbox': 'com.sun.star.awt.UnoControlGroupBoxModel',
            'radio': 'com.sun.star.awt.UnoControlRadioButtonModel',
            'tree': 'com.sun.star.awt.tree.TreeControlModel',
            'grid': 'com.sun.star.awt.grid.UnoControlGridModel',
        }
        return services[control]

    def _get_custom_class(self, tipo, obj):
        classes = {
            'label': UnoLabel,
            'button': UnoButton,
            'text': UnoText,
            'listbox': UnoListBox,
            # ~ 'link': UnoLink,
            # ~ 'tab': UnoTab,
            # ~ 'roadmap': UnoRoadmap,
            # ~ 'image': UnoImage,
            # ~ 'radio': UnoRadio,
            # ~ 'groupbox': UnoGroupBox,
            # ~ 'tree': UnoTree,
            # ~ 'grid': UnoGrid,
        }
        return classes[tipo](obj)

    @catch_exception
    def add_control(self, properties):
        tipo = properties.pop('Type').lower()
        model = self.model.createInstance(self._get_control_model(tipo))
        set_properties(model, properties)
        name = properties['Name']
        self.model.insertByName(name, model)
        control = self._get_custom_class(tipo, self.obj.getControl(name))
        self._add_listeners(control)
        setattr(self, name, control)
        return


# ~ Python >= 3.7
# ~ def __getattr__(name):


def _get_class_doc(obj):
    classes = {
        'calc': LOCalc,
        'writer': LOWriter,
        'base': LOBase,
        'impress': LOImpress,
        'draw': LODraw,
        'math': LOMath,
        'basic': LOBasicIde,
    }
    type_doc = get_type_doc(obj)
    return classes[type_doc](obj)


def get_document():
    doc = None
    desktop = get_desktop()
    try:
        doc = _get_class_doc(desktop.getCurrentComponent())
    except Exception as e:
        log.error(e)
    return doc


def get_selection():
    return get_document().selection


def get_cell(*args):
    if args:
        index = args
        if len(index) == 1:
            index = args[0]
        cell = get_document().get_cell(index)
    else:
        cell = get_selection().first
    return cell


def active_cell():
    return get_cell()


def create_dialog(properties):
    return LODialog(properties)


def set_properties(model, properties):
    if 'X' in properties:
        properties['PositionX'] = properties.pop('X')
    if 'Y' in properties:
        properties['PositionY'] = properties.pop('Y')
    keys = tuple(properties.keys())
    values = tuple(properties.values())
    model.setPropertyValues(keys, values)
    return


def get_config_path(name='Work'):
    """
        Return de path name in config
        http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1util_1_1XPathSettings.html
    """
    path = create_instance('com.sun.star.util.PathSettings')
    return _path_system(getattr(path, name))


def get_file(init_dir='', multiple=False, filters=()):
    """
        init_folder: folder default open
        multiple: True for multiple selected
        filters: Example
        (
            ('XML', '*.xml'),
            ('TXT', '*.txt'),
        )
    """
    if not init_dir:
        init_dir = get_config_path()
    file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker')
    file_picker.setTitle(_('Select file'))
    file_picker.setDisplayDirectory(init_dir)
    file_picker.setMultiSelectionMode(multiple)

    if filters:
        file_picker.setCurrentFilter(filters[0][0])
        for f in filters:
            file_picker.appendFilter(f[0], f[1])

    if file_picker.execute():
        if multiple:
            return [_path_system(f) for f in file_picker.getSelectedFiles()]
        return _path_system(file_picker.getSelectedFiles()[0])

    return ''


def get_info_path(path):
    path, filename = os.path.split(path)
    name, extension = os.path.splitext(filename)
    return (path, filename, name, extension)


def inputbox(message, default='', title=TITLE):

    class ControllersInput(object):

        def __init__(self, dlg):
            self.d = dlg

        def cmd_ok_action(self, event):
            self.d.close(1)
            return

    args = {
        'Title': title,
        'Width': 200,
        'Height': 80,
    }
    dlg = LODialog(args)
    dlg.events = ControllersInput(dlg)

    args = {
        'Type': 'Label',
        'Name': 'lbl_msg',
        'Label': message,
        'Width': 140,
        'Height': 50,
        'X': 5,
        'Y': 5,
        'MultiLine': True,
        'Border': 1,
    }
    dlg.add_control(args)

    args = {
        'Type': 'Text',
        'Name': 'txt_value',
        'Text': default,
        'Width': 190,
        'Height': 15,
    }
    dlg.add_control(args)
    dlg.txt_value.move(dlg.lbl_msg)

    args = {
        'Type': 'button',
        'Name': 'cmd_ok',
        'Label': _('OK'),
        'Width': 40,
        'Height': 15,
        'DefaultButton': True,
        'PushButtonType': 1,
    }
    dlg.add_control(args)
    dlg.cmd_ok.move(dlg.lbl_msg, 10, 0)

    args = {
        'Type': 'button',
        'Name': 'cmd_cancel',
        'Label': _('Cancel'),
        'Width': 40,
        'Height': 15,
        'PushButtonType': 2,
    }
    dlg.add_control(args)
    dlg.cmd_cancel.move(dlg.cmd_ok)

    if dlg.open():
        return dlg.txt_value.value

    return ''


def new_doc(type_doc=CALC):
    path = 'private:factory/s{}'.format(type_doc)
    doc = get_desktop().loadComponentFromURL(path, '_default', 0, ())
    return _get_class_doc(doc)


def open_doc(path, **kwargs):
    """ Open document in path
        Usually options:
            Hidden: True or False
            AsTemplate: True or False
            ReadOnly: True or False
            Password: super_secret
            MacroExecutionMode: 4 = Activate macros
            Preview: True or False

        http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1frame_1_1XComponentLoader.html
        http://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1document_1_1MediaDescriptor.html
    """
    path = _path_url(path)
    # ~ opt = _properties(kwargs)
    opt = dict_to_property(kwargs)
    doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt)
    if doc is None:
        return

    return _get_class_doc(doc)


def open_file(path):
    if IS_WIN:
        os.startfile(path)
    else:
        subprocess.Popen(['xdg-open', path])
    return


def join(*paths):
    return os.path.join(*paths)


def is_dir(path):
    return Path(path).is_dir()


def is_file(path):
    return Path(path).is_file()


def get_file_size(path):
    return Path(path).stat().st_size


def is_created(path):
    return is_file(path) and bool(get_file_size(path))


def replace_ext(path, extension):
    path, _, name, _ = get_info_path(path)
    return '{}/{}.{}'.format(path, name, extension)


def zip_names(path):
    with zipfile.ZipFile(path) as z:
        names = z.namelist()
    return names


def run(command, wait=False):
    # ~ debug(command)
    # ~ debug(shlex.split(command))
    try:
        if wait:
            # ~ p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
            # ~ p.wait()
            result = subprocess.check_output(command, shell=True)
        else:
            p = subprocess.Popen(shlex.split(command), stdin=None,
                stdout=None, stderr=None, close_fds=True)
            result, er = p.communicate()
    except subprocess.CalledProcessError as e:
        msg = ("run [ERROR]: output = %s, error code = %s\n"
            % (e.output, e.returncode))
        error(msg)
        return False

    if result is None:
        return True

    return result.decode()


def _zippwd(source, target, pwd):
    if IS_WIN:
        return False
    if not exists_app('zip'):
        return False

    cmd = 'zip'
    opt = '-j '
    args = "{} --password {} ".format(cmd, pwd)

    if isinstance(source, (tuple, list)):
        if not target:
            return False
        args += opt + target + ' ' + ' '.join(source)
    else:
        if is_file(source) and not target:
            target = replace_ext(source, 'zip')
        elif is_dir(source) and not target:
            target = join(PurePath(source).parent,
                '{}.zip'.format(PurePath(source).name))
            opt = '-r '
        args += opt + target + ' ' + source

    result = run(args, True)
    if not result:
        return False

    return is_created(target)


def zip(source, target='', mode='w', pwd=''):
    if pwd:
        return _zippwd(source, target, pwd)

    if isinstance(source, (tuple, list)):
        if not target:
            return False

        with zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED) as z:
            for path in source:
                _, name, _, _ = get_info_path(path)
                z.write(path, name)

        return is_created(target)

    if is_file(source):
        if not target:
            target = replace_ext(source, 'zip')
        z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED)
        _, name, _, _ = get_info_path(source)
        z.write(source, name)
        z.close()
        return is_created(target)

    if not target:
        target = join(
            PurePath(source).parent,
            '{}.zip'.format(PurePath(source).name))
    z = zipfile.ZipFile(target, mode, compression=zipfile.ZIP_DEFLATED)
    root_len = len(os.path.abspath(source))
    for root, dirs, files in os.walk(source):
        relative = os.path.abspath(root)[root_len:]
        for f in files:
            fullpath = join(root, f)
            file_name = join(relative, f)
            z.write(fullpath, file_name)
    z.close()

    return is_created(target)


def unzip(source, path='', members=None, pwd=None):
    if not path:
        path, _, _, _ = get_info_path(source)
    with zipfile.ZipFile(source) as z:
        if not pwd is None:
            pwd = pwd.encode()
        if isinstance(members, str):
            members = (members,)
        z.extractall(path, members=members, pwd=pwd)
    return True


def merge_zip(target, zips):
    try:
        with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as t:
            for path in zips:
                with zipfile.ZipFile(path, compression=zipfile.ZIP_DEFLATED) as s:
                    for name in s.namelist():
                        t.writestr(name, s.open(name).read())
    except Exception as e:
        error(e)
        return False

    return True


def kill(path):
    p = Path(path)
    if p.is_file():
        try:
            p.unlink()
        except:
            pass
    elif p.is_dir():
        p.rmdir()
    return


def get_size_screen():
    if IS_WIN:
        user32 = ctypes.windll.user32
        res = '{}x{}'.format(user32.GetSystemMetrics(0), user32.GetSystemMetrics(1))
    else:
        args = 'xrandr | grep "*" | cut -d " " -f4'
        res = run(args, True)
    return res.strip()


def get_clipboard():
    df = None
    text = ''
    sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard')
    transferable = sc.getContents()
    data = transferable.getTransferDataFlavors()
    for df in data:
        if df.MimeType == CLIPBOARD_FORMAT_TEXT:
            break
    if df:
        text = transferable.getTransferData(df)
    return text


class TextTransferable(unohelper.Base, XTransferable):
    """Keep clipboard data and provide them."""

    def __init__(self, text):
        df = DataFlavor()
        df.MimeType = CLIPBOARD_FORMAT_TEXT
        df.HumanPresentableName = "encoded text utf-16"
        self.flavors = [df]
        self.data = [text]

    def getTransferData(self, flavor):
        if not flavor:
            return
        for i, f in enumerate(self.flavors):
            if flavor.MimeType == f.MimeType:
                return self.data[i]
        return

    def getTransferDataFlavors(self):
        return tuple(self.flavors)

    def isDataFlavorSupported(self, flavor):
        if not flavor:
            return False
        mtype = flavor.MimeType
        for f in self.flavors:
            if mtype == f.MimeType:
                return True
        return False


def set_clipboard(value):
    ts = TextTransferable(value)
    sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard')
    sc.setContents(ts, None)
    return


def copy(doc=None):
    if doc is None:
        doc = get_document()
    if hasattr(doc, 'frame'):
        frame = doc.frame
    else:
        frame = doc.getCurrentController().getFrame()
    dispatch = get_dispatch()
    dispatch.executeDispatch(frame, '.uno:Copy', '', 0, ())
    return


def get_epoch():
    now = datetime.datetime.now()
    return int(time.mktime(now.timetuple()))


def file_copy(source, target='', name=''):
    p, f, n, e = get_info_path(source)
    if target:
        p = target
    if name:
        n = name
    path_new = join(p, '{}{}'.format(n, e))
    shutil.copy(source, path_new)
    return


def get_files(path, ext='*'):
    docs = []
    for folder, _, files in os.walk(path):
        pattern = re.compile(r'\.{}'.format(ext), re.IGNORECASE)
        docs += [join(folder, f) for f in files if pattern.search(f)]
    return docs


def _get_menu(type_doc, name_menu):
    instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
    service = TYPE_DOC[type_doc]
    manager = create_instance(instance, True)
    ui = manager.getUIConfigurationManager(service)
    menus = ui.getSettings(NODE_MENUBAR, True)
    command = MENUS_APP[type_doc][name_menu]
    for menu in menus:
        data = property_to_dict(menu)
        if data.get('CommandURL', '') == command:
            idc = data.get('ItemDescriptorContainer', None)
            return ui, menus, idc
    return None, None, None


def _get_index_menu(menu, command):
    for i, m in enumerate(menu):
        data = property_to_dict(m)
        cmd = data.get('CommandURL', '')
        if cmd == command:
            return i
        # ~ submenu = data.get('ItemDescriptorContainer', None)
        # ~ if not submenu is None:
            # ~ get_index_menu(submenu, command, count + 1)
    return 0


def _store_menu(ui, menus, menu, index, data=(), remove=False):
    if remove:
        uno.invoke(menu, 'removeByIndex', (index,))
    else:
        properties = dict_to_property(data, True)
        uno.invoke(menu, 'insertByIndex', (index + 1, properties))
    ui.replaceSettings(NODE_MENUBAR, menus)
    ui.store()
    return


def insert_menu(type_doc, name_menu, **kwargs):
    ui, menus, menu = _get_menu(type_doc, name_menu.lower())
    if menu is None:
        return 0

    label = kwargs.get('Label', '-')
    separator = False
    if label == '-':
        separator = True
    command = kwargs.get('Command', '')
    index = kwargs.get('Index', 0)
    if not index:
        index = _get_index_menu(menu, kwargs['After'])
    if separator:
        data = {'Type': 1}
        _store_menu(ui, menus, menu, index, data)
        return index + 1

    index_menu = _get_index_menu(menu, command)
    if index_menu:
        msg = 'Exists: %s' % command
        debug(msg)
        return 0

    sub_menu = kwargs.get('Submenu', ())
    idc = None
    if sub_menu:
        idc = ui.createSettings()

    data = {
        'CommandURL': command,
        'Label': label,
        'Style': 0,
        'Type': 0,
        'ItemDescriptorContainer': idc
    }
    _store_menu(ui, menus, menu, index, data)
    if sub_menu:
        _add_sub_menus(ui, menus, idc, sub_menu)
    return True


def _add_sub_menus(ui, menus, menu, sub_menu):
    for i, sm in enumerate(sub_menu):
        submenu = sm.pop('Submenu', ())
        sm['Type'] = 0
        if submenu:
            idc = ui.createSettings()
            sm['ItemDescriptorContainer'] = idc
        _store_menu(ui, menus, menu, i - 1, sm)
        if submenu:
            _add_sub_menus(ui, menus, idc, submenu)
    return


def remove_menu(type_doc, name_menu, command):
    ui, menus, menu = _get_menu(type_doc, name_menu.lower())
    if menu is None:
        return False

    index = _get_index_menu(menu, command)
    if not index:
        debug('Not exists: %s' % command)
        return False

    _store_menu(ui, menus, menu, index, remove=True)
    return True


# ~ name = 'com.sun.star.configuration.ConfigurationProvider'
# ~ cp = create_instance(name, True)
# ~ node = PropertyValue(Name='nodepath', Value=NODE_SETTING)
# ~ try:
    # ~ cua = cp.createInstanceWithArguments(
        # ~ 'com.sun.star.configuration.ConfigurationUpdateAccess', (node,))
    # ~ cua.setPropertyValue(key, json.dumps(value))
    # ~ cua.commitChanges()
# ~ except Exception as e:
    # ~ log.error(e, exc_info=True)
    # ~ return False
