#!/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 base64
import ctypes
import datetime
import errno
import getpass
import hashlib
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 traceback
import zipfile

from collections import OrderedDict
from collections.abc import MutableMapping
from functools import wraps
from operator import itemgetter
from pathlib import Path, PurePath
from pprint import pprint
from string import Template
from subprocess import PIPE

import smtplib
from smtplib import SMTPException, SMTPAuthenticationError
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.utils import formatdate
from email import encoders
import mailbox

import uno
import unohelper
from com.sun.star.util import Time, Date, DateTime
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

try:
    from fernet import Fernet, InvalidToken
    CRYPTO = True
except ImportError:
    CRYPTO = False


MSG_LANG = {
    'es': {
        'OK': 'Aceptar',
        'Cancel': 'Cancelar',
        'Select file': 'Seleccionar archivo',
        'Incorrect user or password': 'Nombre de usuario o contraseña inválidos',
        'Allow less secure apps in GMail': 'Activa: Permitir aplicaciones menos segura en GMail',
    }
}


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',
    'base': 'com.sun.star.sdb.DocumentDataSource',
    '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_WRITER = {
    'file': '.uno:PickList',
    'edit': '.uno:EditMenu',
    'view': '.uno:ViewMenu',
    'insert': '.uno:InsertMenu',
    'format': '.uno:FormatMenu',
    'styles': '.uno:FormatStylesMenu',
    'sheet': '.uno:TableMenu',
    'data': '.uno:FormatFormMenu',
    'tools': '.uno:ToolsMenu',
    'windows': '.uno:WindowList',
    'help': '.uno:HelpMenu',
}

MENUS_APP = {
    'calc': MENUS_CALC,
    'writer': MENUS_WRITER,
}


EXT = {
    'pdf': 'pdf',
}


FILE_NAME_DEBUG = 'debug.odt'
FILE_NAME_CONFIG = 'zaz-{}.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__)


_start = 0
_stop_thread = {}
TIMEOUT = 10


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


# ~ Export ok
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')


# ~ Export ok
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:
            name = f.__name__
            if IS_WIN:
                debug(traceback.format_exc())
            log.error(name, exc_info=True)
    return func


class LogWin(object):

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

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


# ~ Export ok
def info(data):
    log.info(data)
    return


# ~ Export ok
def debug(info):
    if IS_WIN:
        doc = get_document(FILE_NAME_DEBUG)
        if doc is None:
            # ~ doc = new_doc('writer')
            return
        doc = LogWin(doc.obj)
        doc.write(info)
        return

    log.debug(str(info))
    return


# ~ Export ok
def error(info):
    log.error(info)
    return


# ~ Export ok
def save_log(path, data):
    with open(path, 'a') as out:
        out.write('{} -{}- '.format(str(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 now():
    return datetime.datetime.now()


# ~ Export ok
def get_config(key='', default=None, prefix='config'):
    path_json = FILE_NAME_CONFIG.format(prefix)
    values = None
    path = join(get_config_path('UserConfig'), path_json)
    if not exists_path(path):
        return default

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

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

    return values


# ~ Export ok
def set_config(key, value, prefix='config'):
    path_json = FILE_NAME_CONFIG.format(prefix)
    path = join(get_config_path('UserConfig'), path_json)
    values = get_config(default={}, prefix=prefix)
    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


# ~ Export ok
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]


# ~ Export ok
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()


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


# ~ Export ok
def warning(message, title=TITLE):
    return msgbox(message, title, type_msg='warningbox')


# ~ Export ok
def errorbox(message, title=TITLE):
    return msgbox(message, title, type_msg='errorbox')


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


# ~ Export ok
def get_dispatch():
    return create_instance('com.sun.star.frame.DispatchHelper')


# ~ Export ok
def call_dispatch(url, args=()):
    frame = get_document().frame
    dispatch = get_dispatch()
    dispatch.executeDispatch(frame, url, '', 0, args)
    return


# ~ Export ok
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


# ~ Export ok
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


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


# ~ Export ok
def get_type_doc(obj):
    for k, v in TYPE_DOC.items():
        if obj.supportsService(v):
            return k
    return ''


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


def array_to_dict(values):
    d = {r[0]: r[1] for r in values}
    return d


# ~ 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)
        if self._type_doc == 'base':
            self._cc = self.obj.DatabaseDocument.getCurrentController()
        else:
            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 statusbar(self):
        return self._cc.getStatusIndicator()

    @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()

    def to_pdf(self, path, **kwargs):
        path_pdf = path
        if path:
            if is_dir(path):
                _, _, n, _ = get_info_path(self.path)
                path_pdf = join(path, '{}.{}'.format(n, EXT['pdf']))
        else:
            path_pdf = replace_ext(self.path, EXT['pdf'])

        filter_name = '{}_pdf_Export'.format(self.type)
        filter_data = dict_to_property(kwargs, True)
        args = {
            'FilterName': filter_name,
            'FilterData': filter_data,
        }
        args = dict_to_property(args)
        try:
            self.obj.storeToURL(_path_url(path_pdf), args)
        except Exception as e:
            error(e)
            path_pdf = ''

        return path_pdf


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()

    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 EventsMouseGrid(EventsMouse):
    selected = False

    def mousePressed(self, event):
        super().mousePressed(event)
        # ~ obj = event.Source
        # ~ col = obj.getColumnAtPoint(event.X, event.Y)
        # ~ row = obj.getRowAtPoint(event.X, event.Y)
        # ~ print(col, row)
        # ~ if col == -1 and row == -1:
            # ~ if self.selected:
                # ~ obj.deselectAllRows()
            # ~ else:
                # ~ obj.selectAllRows()
            # ~ self.selected = not self.selected
        return

    def mouseReleased(self, event):
        obj = event.Source
        col = obj.getColumnAtPoint(event.X, event.Y)
        row = obj.getRowAtPoint(event.X, event.Y)
        if row == -1 and col > -1:
            gdm = obj.Model.GridDataModel
            for i in range(gdm.RowCount):
                gdm.updateRowHeading(i, i + 1)
        return


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 type(self):
        return 'label'

    @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)

    @property
    def type(self):
        return 'button'

    @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 type(self):
        return 'text'

    @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 type(self):
        return 'listbox'

    @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 UnoGrid(UnoBaseObject):

    def __init__(self, obj):
        super().__init__(obj)
        self._gdm = self._model.GridDataModel
        # ~ self._data = []
        self._columns = {}
        # ~ self._format_columns = ()

    def __getitem__(self, index):
        value = self._gdm.getCellData(index[0], index[1])
        return value

    @property
    def type(self):
        return 'grid'

    def _format_cols(self):
        rows = tuple(tuple(
            self._format_columns[i].format(r) for i, r in enumerate(row)) for row in self._data
        )
        return rows

    # ~ @property
    # ~ def format_columns(self):
        # ~ return self._format_columns
    # ~ @format_columns.setter
    # ~ def format_columns(self, value):
        # ~ self._format_columns = value

    @property
    def data(self):
        return self._data
    @data.setter
    def data(self, values):
        # ~ self._data = values
        self._gdm.removeAllRows()
        headings = tuple(range(1, len(values) + 1))
        self._gdm.addRows(headings, values)

        # ~ rows = range(grid_dm.RowCount)
        # ~ colors = [COLORS['GRAY'] if r % 2 else COLORS['WHITE'] for r in rows]
        # ~ grid.Model.RowBackgroundColors = tuple(colors)
        return

    @property
    def row(self):
        return self.obj.CurrentRow

    @property
    def rows(self):
        return self._gdm.RowCount

    @property
    def column(self):
        return self.obj.CurrentColumn

    @property
    def columns(self):
        return self._gdm.ColumnCount

    def set_cell_tooltip(self, col, row, value):
        self._gdm.updateCellToolTip(col, row, value)
        return

    def get_cell_tooltip(self, col, row):
        value = self._gdm.getCellToolTip(col, row)
        return value

    def _validate_column(self, data):
        row = []
        for i, d in enumerate(data):
            if i in self._columns:
                if 'image' in self._columns[i]:
                    row.append(self._columns[i]['image'])
            else:
                row.append(d)
        return tuple(row)

    def add_row(self, data):
        # ~ self._data.append(data)
        data = self._validate_column(data)
        self._gdm.addRow(self.rows + 1, data)
        return

    def remove_row(self, row):
        self._gdm.removeRow(row)
        # ~ del self._data[row]
        self._update_row_heading()
        return

    def _update_row_heading(self):
        for i in range(self.rows):
            self._gdm.updateRowHeading(i, i + 1)
        return

    def sort(self, column, asc=True):
        self._gdm.sortByColumn(column, asc)
        # ~ self._data.sort(key=itemgetter(column), reverse=not asc)
        self._update_row_heading()
        return

    def set_column_image(self, column, path):
        gp = create_instance('com.sun.star.graphic.GraphicProvider')
        data = dict_to_property({'URL': _path_url(path)})
        image = gp.queryGraphic(data)
        if not column in self._columns:
            self._columns[column] = {}
        self._columns[column]['image'] = image
        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):
                if control.type == 'grid' and key == 'addMouseListener':
                    control.obj.addMouseListener(EventsMouseGrid(self.events))
                    continue
                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,
            'grid': UnoGrid,
            # ~ 'link': UnoLink,
            # ~ 'tab': UnoTab,
            # ~ 'roadmap': UnoRoadmap,
            # ~ 'image': UnoImage,
            # ~ 'radio': UnoRadio,
            # ~ 'groupbox': UnoGroupBox,
            # ~ 'tree': UnoTree,
        }
        return classes[tipo](obj)

    def _set_column_model(self, columns):
        #~ https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1awt_1_1grid_1_1XGridColumn.html
        column_model = create_instance('com.sun.star.awt.grid.DefaultGridColumnModel', True)
        # ~ column_model.setDefaultColumns(len(columns))
        for column in columns:
            grid_column = create_instance('com.sun.star.awt.grid.GridColumn', True)
            for k, v in column.items():
                setattr(grid_column, k, v)
            column_model.addColumn(grid_column)
        # ~ mri(grid_column)
        return column_model

    def _set_image_url(self, path):
        if exists_path(path):
            return _path_url(path)
        return ''

    def add_control(self, properties):
        tipo = properties.pop('Type').lower()

        columns = properties.pop('Columns', ())
        if tipo == 'grid':
            properties['ColumnModel'] = self._set_column_model(columns)
        if tipo == 'button' and 'ImageURL' in properties:
            properties['ImageURL'] = self._set_image_url(properties['ImageURL'])

        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)


# ~ Export ok
def get_document(title=''):
    doc = None
    desktop = get_desktop()
    if not title:
        doc = _get_class_doc(desktop.getCurrentComponent())
        return doc

    for d in desktop.getComponents():
        if d.Title == title:
            doc = d
            break

    if doc is None:
        return

    return _get_class_doc(doc)


# ~ Export ok
def get_documents(custom=True):
    docs = []
    desktop = get_desktop()
    for doc in desktop.getComponents():
        if custom:
            docs.append(_get_class_doc(doc))
        else:
            docs.append(doc)
    return docs


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


# ~ Export ok
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))


# ~ Export ok
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()
    init_dir = _path_url(init_dir)
    file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker')
    file_picker.setTitle(_('Select file'))
    file_picker.setDisplayDirectory(init_dir)
    file_picker.setMultiSelectionMode(multiple)

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

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

    return path


# ~ Export ok
def get_path(init_dir='', filters=()):
    """
        Options: http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1ui_1_1dialogs_1_1TemplateDescription.html
        filters: Example
        (
            ('XML', '*.xml'),
            ('TXT', '*.txt'),
        )
    """
    if not init_dir:
        init_dir = get_config_path()
    init_dir = _path_url(init_dir)
    file_picker = create_instance('com.sun.star.ui.dialogs.FilePicker')
    file_picker.setTitle(_('Select file'))
    file_picker.setDisplayDirectory(init_dir)
    file_picker.initialize((2,))
    if filters:
        file_picker.setCurrentFilter(filters[0][0])
        for f in filters:
            file_picker.appendFilter(f[0], f[1])

    path = ''
    if file_picker.execute():
        path =  _path_system(file_picker.getSelectedFiles()[0])
    return path


# ~ Export ok
def get_dir(init_dir=''):
    folder_picker = create_instance('com.sun.star.ui.dialogs.FolderPicker')
    if not init_dir:
        init_dir = get_config_path()
    init_dir = _path_url(init_dir)
    folder_picker.setDisplayDirectory(init_dir)

    path = ''
    if folder_picker.execute():
        path = _path_system(folder_picker.getDirectory())
    return path


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


# ~ Export ok
def read_file(path, mode='r', array=False):
    data = ''
    with open(path, mode) as f:
        if array:
            data = tuple(f.read().splitlines())
        else:
            data = f.read()
    return data


# ~ Export ok
def save_file(path, mode='w', data=None):
    with open(path, mode) as f:
        f.write(data)
    return


# ~ Export ok
def to_json(path, data):
    with open(path, 'w') as f:
        f.write(json.dumps(data, indent=4, sort_keys=True))
    return


# ~ Export ok
def from_json(path):
    with open(path) as f:
        data = json.loads(f.read())
    return data


def get_path_extension(id):
    pip = CTX.getValueByName('/singletons/com.sun.star.deployment.PackageInformationProvider')
    path = _path_system(pip.getPackageLocation(id))
    return path


# ~ Export ok
def inputbox(message, default='', title=TITLE, echochar=''):

    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,
    }
    if echochar:
        args['EchoChar'] = ord(echochar[0])
    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 ''


# ~ Export ok
def new_doc(type_doc=CALC, **kwargs):
    path = 'private:factory/s{}'.format(type_doc)
    opt = dict_to_property(kwargs)
    doc = get_desktop().loadComponentFromURL(path, '_default', 0, opt)
    return _get_class_doc(doc)


# ~ Export ok
def new_db(path):
    dbc = create_instance('com.sun.star.sdb.DatabaseContext')
    db = dbc.createInstance()
    db.URL = 'sdbc:embedded:firebird'  # hsqldb
    db.DatabaseDocument.storeAsURL(_path_url(path), ())
    return _get_class_doc(db)


# ~ Export ok
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 = dict_to_property(kwargs)
    doc = get_desktop().loadComponentFromURL(path, '_blank', 0, opt)
    if doc is None:
        return

    return _get_class_doc(doc)


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


# ~ Export ok
def join(*paths):
    return os.path.join(*paths)


# ~ Export ok
def is_dir(path):
    return Path(path).is_dir()


# ~ Export ok
def is_file(path):
    return Path(path).is_file()


# ~ Export ok
def get_file_size(path):
    return Path(path).stat().st_size


# ~ Export ok
def is_created(path):
    return is_file(path) and bool(get_file_size(path))


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


# ~ Export ok
def zip_content(path):
    with zipfile.ZipFile(path) as z:
        names = z.namelist()
    return names


# ~ Export ok
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)


# ~ Export ok
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)


# ~ Export ok
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


# ~ Export ok
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


# ~ Export ok
def kill(path):
    p = Path(path)
    try:
        if p.is_file():
            p.unlink()
        elif p.is_dir():
            shutil.rmtree(path)
    except OSError as e:
        log.error(e)
    return


# ~ Export ok
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()


# ~ Export ok
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


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


# ~ Todo
def copy():
    call_dispatch('.uno:Copy')
    return


# ~ Export ok
def get_epoch():
    n = now()
    return int(time.mktime(n.timetuple()))


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


# ~ Export ok
def get_path_content(path, filters='*'):
    paths = []
    for folder, _, files in os.walk(path):
        pattern = re.compile(r'\.(?:{})$'.format(filters), re.IGNORECASE)
        paths += [join(folder, f) for f in files if pattern.search(f)]
    return paths


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('CommandURL', '')
    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
        if sm['Label'] == '-':
            sm = {'Type': 1}
        _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


def _get_app_submenus(menus, count=0):
    for i, menu in enumerate(menus):
        data = property_to_dict(menu)
        cmd = data.get('CommandURL', '')
        msg = '  ' * count + '├─' + cmd
        debug(msg)
        submenu = data.get('ItemDescriptorContainer', None)
        if not submenu is None:
            _get_app_submenus(submenu, count + 1)
    return


def get_app_menus(name_app, index=-1):
    instance = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
    service = TYPE_DOC[name_app]
    manager = create_instance(instance, True)
    ui = manager.getUIConfigurationManager(service)
    menus = ui.getSettings(NODE_MENUBAR, True)
    if index == -1:
        for menu in menus:
            data = property_to_dict(menu)
            debug(data.get('CommandURL', ''))
    else:
        menus = property_to_dict(menus[index])['ItemDescriptorContainer']
        _get_app_submenus(menus)
    return menus


# ~ Export ok
def start():
    global _start
    _start = now()
    log.info(_start)
    return


# ~ Export ok
def end():
    global _start
    e = now()
    return str(e - _start).split('.')[0]


# ~ Export ok
# ~ https://en.wikipedia.org/wiki/Web_colors
def get_color(value):
    COLORS = {
        'aliceblue': 15792383,
        'antiquewhite': 16444375,
        'aqua': 65535,
        'aquamarine': 8388564,
        'azure': 15794175,
        'beige': 16119260,
        'bisque': 16770244,
        'black': 0,
        'blanchedalmond': 16772045,
        'blue': 255,
        'blueviolet': 9055202,
        'brown': 10824234,
        'burlywood': 14596231,
        'cadetblue': 6266528,
        'chartreuse': 8388352,
        'chocolate': 13789470,
        'coral': 16744272,
        'cornflowerblue': 6591981,
        'cornsilk': 16775388,
        'crimson': 14423100,
        'cyan': 65535,
        'darkblue': 139,
        'darkcyan': 35723,
        'darkgoldenrod': 12092939,
        'darkgray': 11119017,
        'darkgreen': 25600,
        'darkgrey': 11119017,
        'darkkhaki': 12433259,
        'darkmagenta': 9109643,
        'darkolivegreen': 5597999,
        'darkorange': 16747520,
        'darkorchid': 10040012,
        'darkred': 9109504,
        'darksalmon': 15308410,
        'darkseagreen': 9419919,
        'darkslateblue': 4734347,
        'darkslategray': 3100495,
        'darkslategrey': 3100495,
        'darkturquoise': 52945,
        'darkviolet': 9699539,
        'deeppink': 16716947,
        'deepskyblue': 49151,
        'dimgray': 6908265,
        'dimgrey': 6908265,
        'dodgerblue': 2003199,
        'firebrick': 11674146,
        'floralwhite': 16775920,
        'forestgreen': 2263842,
        'fuchsia': 16711935,
        'gainsboro': 14474460,
        'ghostwhite': 16316671,
        'gold': 16766720,
        'goldenrod': 14329120,
        'gray': 8421504,
        'grey': 8421504,
        'green': 32768,
        'greenyellow': 11403055,
        'honeydew': 15794160,
        'hotpink': 16738740,
        'indianred': 13458524,
        'indigo': 4915330,
        'ivory': 16777200,
        'khaki': 15787660,
        'lavender': 15132410,
        'lavenderblush': 16773365,
        'lawngreen': 8190976,
        'lemonchiffon': 16775885,
        'lightblue': 11393254,
        'lightcoral': 15761536,
        'lightcyan': 14745599,
        'lightgoldenrodyellow': 16448210,
        'lightgray': 13882323,
        'lightgreen': 9498256,
        'lightgrey': 13882323,
        'lightpink': 16758465,
        'lightsalmon': 16752762,
        'lightseagreen': 2142890,
        'lightskyblue': 8900346,
        'lightslategray': 7833753,
        'lightslategrey': 7833753,
        'lightsteelblue': 11584734,
        'lightyellow': 16777184,
        'lime': 65280,
        'limegreen': 3329330,
        'linen': 16445670,
        'magenta': 16711935,
        'maroon': 8388608,
        'mediumaquamarine': 6737322,
        'mediumblue': 205,
        'mediumorchid': 12211667,
        'mediumpurple': 9662683,
        'mediumseagreen': 3978097,
        'mediumslateblue': 8087790,
        'mediumspringgreen': 64154,
        'mediumturquoise': 4772300,
        'mediumvioletred': 13047173,
        'midnightblue': 1644912,
        'mintcream': 16121850,
        'mistyrose': 16770273,
        'moccasin': 16770229,
        'navajowhite': 16768685,
        'navy': 128,
        'oldlace': 16643558,
        'olive': 8421376,
        'olivedrab': 7048739,
        'orange': 16753920,
        'orangered': 16729344,
        'orchid': 14315734,
        'palegoldenrod': 15657130,
        'palegreen': 10025880,
        'paleturquoise': 11529966,
        'palevioletred': 14381203,
        'papayawhip': 16773077,
        'peachpuff': 16767673,
        'peru': 13468991,
        'pink': 16761035,
        'plum': 14524637,
        'powderblue': 11591910,
        'purple': 8388736,
        'red': 16711680,
        'rosybrown': 12357519,
        'royalblue': 4286945,
        'saddlebrown': 9127187,
        'salmon': 16416882,
        'sandybrown': 16032864,
        'seagreen': 3050327,
        'seashell': 16774638,
        'sienna': 10506797,
        'silver': 12632256,
        'skyblue': 8900331,
        'slateblue': 6970061,
        'slategray': 7372944,
        'slategrey': 7372944,
        'snow': 16775930,
        'springgreen': 65407,
        'steelblue': 4620980,
        'tan': 13808780,
        'teal': 32896,
        'thistle': 14204888,
        'tomato': 16737095,
        'turquoise': 4251856,
        'violet': 15631086,
        'wheat': 16113331,
        'white': 16777215,
        'whitesmoke': 16119285,
        'yellow': 16776960,
        'yellowgreen': 10145074,
    }

    if isinstance(value, tuple):
        return (value[0] << 16) + (value[1] << 8) + value[2]

    if isinstance(value, str) and value[0] == '#':
        r, g, b = bytes.fromhex(value[1:])
        return (r << 16) + (g << 8) + b

    return COLORS.get(value.lower(), -1)


# ~ Export ok
def render(template, data):
    s = Template(template)
    return s.safe_substitute(**data)


def _to_date(value):
    new_value = value
    if isinstance(value, Time):
        new_value = datetime.time(value.Hours, value.Minutes, value.Seconds)
    elif isinstance(value, Date):
        new_value = datetime.date(value.Year, value.Month, value.Day)
    elif isinstance(value, DateTime):
        new_value = datetime.datetime(
            value.Year, value.Month, value.Day,
            value.Hours, value.Minutes, value.Seconds)
    return new_value


# ~ Export ok
def format(template, data):
    """
        https://pyformat.info/
    """
    if isinstance(data, (str, int, float)):
        # ~ print(template.format(data))
        return template.format(data)

    if isinstance(data, (Time, Date, DateTime)):
        return template.format(_to_date(data))

    if isinstance(data, tuple) and isinstance(data[0], tuple):
        data = {r[0]: _to_date(r[1]) for r in data}
        return template.format(**data)

    data = [_to_date(v) for v in data]
    result = template.format(*data)
    return result


def _call_macro(macro):
    #~ https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification
    name = 'com.sun.star.script.provider.MasterScriptProviderFactory'
    factory = create_instance(name, False)

    data = macro.copy()
    if macro['language'] == 'Python':
        data['module'] = '.py$'
    elif macro['language'] == 'Basic':
        data['module'] = '.{}.'.format(macro['module'])
        if macro['location'] == 'user':
            data['location'] = 'application'
    else:
        data['module'] = '.'

    args = macro.get('args', ())
    url = 'vnd.sun.star.script:{library}{module}{name}?language={language}&location={location}'
    path = url.format(**data)
    script = factory.createScriptProvider('').getScript(path)
    return script.invoke(args, None, None)[0]


# ~ Export ok
def call_macro(macro):
    in_thread = macro.pop('thread')
    if in_thread:
        t = threading.Thread(target=_call_macro, args=(macro,))
        t.start()
        return

    return _call_macro(macro)


class TimerThread(threading.Thread):

    def __init__(self, event, seconds, macro):
        threading.Thread.__init__(self)
        self.stopped = event
        self.seconds = seconds
        self.macro = macro

    def run(self):
        info('Timer started... {}'.format(self.macro['name']))
        while not self.stopped.wait(self.seconds):
            _call_macro(self.macro)
        info('Timer stopped... {}'.format(self.macro['name']))
        return


# ~ Export ok
def timer(name, seconds, macro):
    global _stop_thread
    _stop_thread[name] = threading.Event()
    thread = TimerThread(_stop_thread[name], seconds, macro)
    thread.start()
    return


# ~ Export ok
def stop_timer(name):
    global _stop_thread
    _stop_thread[name].set()
    del _stop_thread[name]
    return


def _get_key(password):
    digest = hashlib.sha256(password.encode()).digest()
    key = base64.urlsafe_b64encode(digest)
    return key


# ~ Export ok
def encrypt(data, password):
    f = Fernet(_get_key(password))
    token = f.encrypt(data).decode()
    return token


# ~ Export ok
def decrypt(token, password):
    data = ''
    f = Fernet(_get_key(password))
    try:
        data = f.decrypt(token.encode()).decode()
    except InvalidToken as e:
        error('Invalid Token')
    return data


class SmtpServer(object):

    def __init__(self, config):
        self._server = None
        self._error = ''
        self._sender = ''
        self._is_connect = self._login(config)

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    @property
    def is_connect(self):
        return self._is_connect

    @property
    def error(self):
        return self._error

    def _login(self, config):
        name = config['server']
        port = config['port']
        is_ssl = config['ssl']
        self._sender = config['user']
        hosts = ('gmail' in name or 'outlook' in name)
        try:
            if is_ssl and hosts:
                self._server = smtplib.SMTP(name, port, timeout=TIMEOUT)
                self._server.ehlo()
                self._server.starttls()
                self._server.ehlo()
            elif is_ssl:
                self._server = smtplib.SMTP_SSL(name, port, timeout=TIMEOUT)
                self._server.ehlo()
            else:
                self._server = smtplib.SMTP(name, port, timeout=TIMEOUT)

            self._server.login(self._sender, config['pass'])
            msg = 'Connect to: {}'.format(name)
            debug(msg)
            return True
        except smtplib.SMTPAuthenticationError as e:
            if '535' in str(e):
                self._error = _('Incorrect user or password')
                return False
            if '534' in str(e) and 'gmail' in name:
                self._error = _('Allow less secure apps in GMail')
                return False
        except smtplib.SMTPException as e:
            self._error = str(e)
            return False
        except Exception as e:
            self._error = str(e)
            return False
        return False

    def _body(self, msg):
        body = msg.replace('\\n', '<BR>')
        return body

    def send(self, message):
        file_name = 'attachment; filename={}'
        email = MIMEMultipart()
        email['From'] = self._sender
        email['To'] = message['to']
        email['Cc'] = message.get('cc', '')
        email['Subject'] = message['subject']
        email['Date'] = formatdate(localtime=True)
        if message.get('confirm', False):
            email['Disposition-Notification-To'] = email['From']
        email.attach(MIMEText(self._body(message['body']), 'html'))

        for path in message.get('files', ()):
            _, fn, _, _ = get_info_path(path)
            part = MIMEBase('application', 'octet-stream')
            part.set_payload(read_file(path, 'rb'))
            encoders.encode_base64(part)
            part.add_header('Content-Disposition', file_name.format(fn))
            email.attach(part)

        receivers = (
            email['To'].split(',') +
            email['CC'].split(',') +
            message.get('bcc', '').split(','))
        try:
            self._server.sendmail(self._sender, receivers, email.as_string())
            msg = 'Email sent...'
            debug(msg)
            if message.get('path', ''):
                self.save_message(email, message['path'])
            return True
        except Exception as e:
            self._error = str(e)
            return False
        return False

    def save_message(self, email, path):
        mbox = mailbox.mbox(path, create=True)
        mbox.lock()
        try:
            msg = mailbox.mboxMessage(email)
            mbox.add(msg)
            mbox.flush()
        finally:
            mbox.unlock()
        return

    def close(self):
        try:
            self._server.quit()
            msg = 'Close connection...'
            debug(msg)
        except:
            pass
        return


def _send_email(server, messages):
    with SmtpServer(server) as server:
        if server.is_connect:
            for msg in messages:
                server.send(msg)
        else:
            error(server.error)
    return server.error


def send_email(server, message):
    messages = message
    if isinstance(message, dict):
        messages = (message,)
    t = threading.Thread(target=_send_email, args=(server, messages))
    t.start()
    return


def server_smtp_test(config):
    with SmtpServer(config) as server:
        if server.error:
            error(server.error)
    return server.error


# ~ 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
