#!/usr/bin/env python3

# == Rapid Develop Macros in LibreOffice ==

# ~ https://git.cuates.net/elmau/easymacro

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

# ~ easymacro 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 easymacro.  If not, see <https://www.gnu.org/licenses/>.



import getpass
import hashlib
import io

import logging
import os
import platform
import re
import shlex
import socket
import ssl



from string import Template

from socket import timeout
from urllib import parse
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

from com.sun.star.awt import Rectangle, Size, Point
from com.sun.star.awt import Key, KeyEvent, KeyModifier

from com.sun.star.datatransfer import XTransferable, DataFlavor
from com.sun.star.io import IOException, XOutputStream

from com.sun.star.sheet import XRangeSelectionListener

from com.sun.star.container import NoSuchElementException



SALT = b'00a1bfb05353bb3fd8e7aa7fe5efdccc'

_EVENTS = {}

FILES = {
    'CONFIG': 'zaz-{}.json',
}
DIRS = {}


def run_in_thread(fn):
    """Run any function in thread

    :param fn: Any Python function (macro)
    :type fn: Function instance
    """
    def run(*k, **kw):
        t = threading.Thread(target=fn, args=k, kwargs=kw)
        t.start()
        return t
    return run



def _property_to_dict(values):
    d = {v.Name: v.Value for v in values}
    return d


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

    :param data: Dictionary of values
    :type data: array of tuples, list, PropertyValue or NamedValue
    :return: Dictionary
    :rtype: dict
    """
    d = {}
    if not isinstance(data, (tuple, list)):
        return d

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

    return d


def render(template, data):
    s = Template(template)
    return s.safe_substitute(**data)

# Classes


class Json(object):
    """Class for json data
    """

    @classmethod
    def dumps(cls, data: Any) -> str:
        """Dumps

        :param data: Any data
        :type data: Any
        :return: Return string json
        :rtype: str
        """
        return json.dumps(data, indent=4, sort_keys=True)

    @classmethod
    def loads(cls, data: str) -> Any:
        """Loads

        :param data: String data
        :type data: str
        :return: Return any object
        :rtype: Any
        """
        return json.loads(data)


class Macro(object):
    """Class for call macro

    `See Scripting Framework <https://wiki.openoffice.org/wiki/Documentation/DevGuide/Scripting/Scripting_Framework_URI_Specification>`_
    """
    @classmethod
    def call(cls, args: dict, in_thread: bool=False):
        """Call any macro

        :param args: Dictionary with macro location
        :type args: dict
        :param in_thread: If execute in thread
        :type in_thread: bool
        :return: Return None or result of call macro
        :rtype: Any
        """

        result = None
        if in_thread:
            t = threading.Thread(target=cls._call, args=(args,))
            t.start()
        else:
            result = cls._call(args)
        return result

    @classmethod
    def get_url_script(cls, args: dict):
        library = args['library']
        name = args['name']
        language = args.get('language', 'Python')
        location = args.get('location', 'user')
        module = args.get('module', '.')

        if language == 'Python':
            module = '.py$'
        elif language == 'Basic':
            module = f".{module}."
            if location == 'user':
                location = 'application'

        url = 'vnd.sun.star.script'
        url = f'{url}:{library}{module}{name}?language={language}&location={location}'
        return url

    @classmethod
    def _call(cls, args: dict):
        url = cls.get_url_script(args)
        args = args.get('args', ())

        service = 'com.sun.star.script.provider.MasterScriptProviderFactory'
        factory = create_instance(service)
        script = factory.createScriptProvider('').getScript(url)
        result = script.invoke(args, None, None)[0]

        return result


class Shell(object):
    """Class for subprocess

    `See Subprocess <https://docs.python.org/3.7/library/subprocess.html>`_
    """
    @classmethod
    def run(cls, command, capture=False, split=False):
        """Execute commands

        :param command: Command to run
        :type command: str
        :param capture: If capture result of command
        :type capture: bool
        :param split: Some commands need split.
        :type split: bool
        :return: Result of command
        :rtype: Any
        """
        if split:
            cmd = shlex.split(command)
            result = subprocess.run(cmd, capture_output=capture, text=True, shell=IS_WIN)
            if capture:
                result = result.stdout
            else:
                result = result.returncode
        else:
            if capture:
                result = subprocess.check_output(command, shell=True).decode()
            else:
                result = subprocess.Popen(command)
        return result

    @classmethod
    def popen(cls, command):
        """Execute commands and return line by line

        :param command: Command to run
        :type command: str
        :return: Result of command
        :rtype: Any
        """
        try:
            proc = subprocess.Popen(shlex.split(command), shell=IS_WIN,
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            for line in proc.stdout:
                yield line.decode().rstrip()
        except Exception as e:
            error(e)
            yield (e.errno, e.strerror)


class Timer(object):
    """Class for timer thread"""

    class TimerThread(threading.Thread):

        def __init__(self, event, seconds, macro):
            threading.Thread.__init__(self)
            self._event = event
            self._seconds = seconds
            self._macro = macro

        def run(self):
            while not self._event.wait(self._seconds):
                Macro.call(self._macro)
            info('\tTimer stopped... ')
            return

    @classmethod
    def exists(cls, name):
        """Validate in timer **name** exists

        :param name: Timer name, it must be unique
        :type name: str
        :return: True if exists timer name
        :rtype: bool
        """
        global _EVENTS
        return name in _EVENTS

    @classmethod
    def start(cls, name: str, seconds: float, macro: dict):
        """Start timer **name** every **seconds** and execute **macro**

        :param name: Timer name, it must be unique
        :type name: str
        :param seconds: Seconds for wait
        :type seconds: float
        :param macro: Macro for execute
        :type macro: dict
        """
        global _EVENTS

        _EVENTS[name] = threading.Event()
        info(f"Timer '{name}' started, execute macro: '{macro['name']}'")
        thread = cls.TimerThread(_EVENTS[name], seconds, macro)
        thread.start()
        return

    @classmethod
    def stop(cls, name: str):
        """Stop timer **name**

        :param name: Timer name
        :type name: str
        """
        global _EVENTS
        _EVENTS[name].set()
        del _EVENTS[name]
        return

    @classmethod
    def once(cls, name: str, seconds: float, macro: dict):
        """Start timer **name** only once in **seconds** and execute **macro**

        :param name: Timer name, it must be unique
        :type name: str
        :param seconds: Seconds for wait before execute macro
        :type seconds: float
        :param macro: Macro for execute
        :type macro: dict
        """
        global _EVENTS

        _EVENTS[name] = threading.Timer(seconds, Macro.call, (macro,))
        _EVENTS[name].start()
        info(f'Event: "{name}", started... execute in {seconds} seconds')

        return

    @classmethod
    def cancel(cls, name: str):
        """Cancel timer **name** only once events.

        :param name: Timer name, it must be unique
        :type name: str
        """
        global _EVENTS

        if name in _EVENTS:
            try:
                _EVENTS[name].cancel()
                del _EVENTS[name]
                info(f'Cancel event: "{name}", ok...')
            except Exception as e:
                error(e)
        else:
            debug(f'Cancel event: "{name}", not exists...')
        return


class Hash(object):
    """Class for hash
    """
    @classmethod
    def digest(cls, method: str, data: str, in_hex: bool=True):
        """Get digest from data with method

        :param method: Digest method: md5, sha1, sha256, sha512, etc...
        :type method: str
        :param data: Data for get digest
        :type data: str
        :param in_hex: If True, get digest in hexadecimal, if False, get bytes
        :type in_hex: bool
        :return: bytes or hex digest
        :rtype: bytes or str
        """

        result = ''
        obj = getattr(hashlib, method)(data.encode())
        if in_hex:
            result = obj.hexdigest()
        else:
            result = obj.digest()
        return result


class Config(object):
    """Class for set and get configurations
    """
    @classmethod
    def set(cls, prefix: str, value: Any, key: str='') -> bool:
        """Save data config in user config like json

        :param prefix: Unique prefix for this data
        :type prefix: str
        :param value: Value for save
        :type value: Any
        :param key: Key for value
        :type key: str
        :return: True if save correctly
        :rtype: bool
        """
        name_file = FILES['CONFIG'].format(prefix)
        path = Paths.join(Paths.user_config, name_file)
        data = value
        if key:
            data = cls.get(prefix)
            data[key] = value
        result = Paths.to_json(path, data)
        return result

    @classmethod
    def get(cls, prefix: str, key: str='', default: Any={}) -> Any:
        """Get data config from user config like json

        :param prefix: Unique prefix for this data
        :type prefix: str
        :param key: Key for value
        :type key: str
        :param default: Get if not exists key
        :type default: Any
        :return: data
        :rtype: Any
        """
        data = {}
        name_file = FILES['CONFIG'].format(prefix)
        path = Paths.join(Paths.user_config, name_file)
        if not Paths.exists(path):
            return data

        data = Paths.from_json(path)
        if key:
            data = data.get(key, default)

        return data


class Url(object):
    """Class for simple url open
    """
    @classmethod
    def _open(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \
        json: bool=False, timeout: int=TIMEOUT, method: str='GET') -> tuple:
        """URL Open"""

        debug(url)
        result = None
        context = None
        rheaders = {}
        err = ''

        if verify:
            if not data is None:
                if isinstance(data, str):
                    data = data.encode()
                elif isinstance(data, dict):
                    data = parse.urlencode(data).encode('ascii')
        else:
            context = ssl._create_unverified_context()

        try:
            req = Request(url, data=data, headers=headers, method=method)
            response = urlopen(req, timeout=timeout, context=context)
        except HTTPError as e:
            error(e)
            err = str(e)
        except URLError as e:
            error(e.reason)
            err = str(e.reason)
        # ToDo
        # ~ except timeout:
            # ~ err = 'timeout'
            # ~ error(err)
        else:
            rheaders = dict(response.info())
            result = response.read().decode()
            if json:
                result = Json.loads(result)

        return result, rheaders, err

    @classmethod
    def get(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \
        json: bool=False, timeout: int=TIMEOUT) -> tuple:
        """Method GET

        :param url: Url to open
        :type url: str
        :return: result, headers and error
        :rtype: tuple
        """
        return cls._open(url, data, headers, verify, json, timeout)

    # ToDo
    @classmethod
    def post(cls, url: str, data: Any=None, headers: dict={}, verify: bool=True, \
        json: bool=False, timeout: int=TIMEOUT) -> tuple:
        """Method POST
        """
        data = parse.urlencode(data).encode('ascii')
        return cls._open(url, data, headers, verify, json, timeout, 'POST')


class Color(object):
    """Class for colors

    `See Web Colors <https://en.wikipedia.org/wiki/Web_colors>`_
    """
    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,
    }

    def _get_color(self, index):
        if isinstance(index, tuple):
            color = (index[0] << 16) + (index[1] << 8) + index[2]
        else:
            if index[0] == '#':
                r, g, b = bytes.fromhex(index[1:])
                color = (r << 16) + (g << 8) + b
            else:
                color = self.COLORS.get(index.lower(), -1)
        return color

    def __call__(self, index):
        return self._get_color(index)

    def __getitem__(self, index):
        return self._get_color(index)


COLOR_ON_FOCUS = Color()('LightYellow')


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

    class TextTransferable(unohelper.Base, XTransferable):

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

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

        def getTransferDataFlavors(self):
            return self.flavors

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

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


class IOStream(object):
    """Classe for input/output stream"""

    class OutputStream(unohelper.Base, XOutputStream):

        def __init__(self):
            self._buffer = b''
            self.closed = 0

        @property
        def buffer(self):
            return self._buffer

        def closeOutput(self):
            self.closed = 1

        def writeBytes(self, seq):
            if seq.value:
                self._buffer = seq.value

        def flush(self):
            pass

    @classmethod
    def buffer(cls):
        return io.BytesIO()

    @classmethod
    def input(cls, buffer):
        service = 'com.sun.star.io.SequenceInputStream'
        stream = create_instance(service, True)
        stream.initialize((uno.ByteSequence(buffer.getvalue()),))
        return stream

    @classmethod
    def output(cls):
        return cls.OutputStream()


class EventsRangeSelectionListener(EventsListenerBase, XRangeSelectionListener):

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

    def done(self, event):
        range_selection = event.RangeDescriptor
        event_name = 'range_selection_done'
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)(range_selection)
        return

    def aborted(self, event):
        range_selection = event.RangeDescriptor
        event_name = 'range_selection_aborted'
        if hasattr(self._controller, event_name):
            getattr(self._controller, event_name)()
        return


class LOShapes(object):
    _type = 'ShapeCollection'

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

    def __len__(self):
        return self.obj.Count

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            s = self.obj[self._index]
            shape = LOShape(s)
        except IndexError:
            raise StopIteration

        self._index += 1
        return shape

    def __str__(self):
        return 'Shapes'

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


class LOShape(object):
    IMAGE = 'com.sun.star.drawing.GraphicObjectShape'

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

    def __str__(self):
        return f'Shape: {self.name}'

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

    @property
    def properties(self):
        # ~ properties = self.obj.PropertySetInfo.Properties
        # ~ data = {p.Name: getattr(self.obj, p.Name) for p in properties}
        data = self.obj.PropertySetInfo.Properties
        keys = [p.Name for p in data]
        values = self.obj.getPropertyValues(keys)
        data = dict(zip(keys, values))
        return data
    @properties.setter
    def properties(self, values):
        _set_properties(self.obj, values)

    @property
    def shape_type(self):
        return self.obj.ShapeType

    @property
    def name(self):
        return self.obj.Name
    @name.setter
    def name(self, value):
        self.obj.Name = value

    @property
    def is_image(self):
        return self.shape_type == self.IMAGE

    @property
    def is_shape(self):
        return self.shape_type != self.IMAGE

    @property
    def size(self):
        s = self.obj.Size
        return s
    @size.setter
    def size(self, value):
        self.obj.Size = value

    @property
    def width(self):
        s = self.obj.Size
        return s.Width
    @width.setter
    def width(self, value):
        s = self.size
        s.Width = value
        self.size = s

    @property
    def height(self):
        s = self.obj.Size
        return s.Height
    @height.setter
    def height(self, value):
        s = self.size
        s.Height = value
        self.size = s

    @property
    def position(self):
        return self.obj.Position
    @property
    def x(self):
        return self.position.X
    @property
    def y(self):
        return self.position.Y

    @property
    def string(self):
        return self.obj.String
    @string.setter
    def string(self, value):
        self.obj.String = value

    @property
    def title(self):
        return self.obj.Title
    @title.setter
    def title(self, value):
        self.obj.Title = value

    @property
    def description(self):
        return self.obj.Description
    @description.setter
    def description(self, value):
        self.obj.Description = value



class LOShortCuts(object):
    """Classe for manager shortcuts"""
    KEYS = {getattr(Key, k): k for k in dir(Key)}
    MODIFIERS = {
        'shift': KeyModifier.SHIFT,
        'ctrl': KeyModifier.MOD1,
        'alt': KeyModifier.MOD2,
        'ctrlmac': KeyModifier.MOD3,
    }
    COMBINATIONS = {
        0: '',
        1: 'shift',
        2: 'ctrl',
        4: 'alt',
        8: 'ctrlmac',
        3: 'shift+ctrl',
        5: 'shift+alt',
        9: 'shift+ctrlmac',
        6: 'ctrl+alt',
        10: 'ctrl+ctrlmac',
        12: 'alt+ctrlmac',
        7: 'shift+ctrl+alt',
        11: 'shift+ctrl+ctrlmac',
        13: 'shift+alt+ctrlmac',
        14: 'ctrl+alt+ctrlmac',
        15: 'shift+ctrl+alt+ctrlmac',
    }

    def __init__(self, app: str=''):
        self._app = app
        service = 'com.sun.star.ui.GlobalAcceleratorConfiguration'
        if app:
            service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
            type_app = LODocuments.TYPES[app]
            manager = create_instance(service, True)
            uicm = manager.getUIConfigurationManager(type_app)
            self._config = uicm.ShortCutManager
        else:
            self._config = create_instance(service)

    def __getitem__(self, index):
        return LOShortCuts(index)

    def __contains__(self, item):
        cmd = self.get_by_shortcut(item)
        return bool(cmd)

    def __iter__(self):
        self._i = -1
        return self

    def __next__(self):
        self._i += 1
        try:
            event = self._config.AllKeyEvents[self._i]
            event = self._get_info(event)
        except IndexError:
            raise StopIteration

        return event

    @classmethod
    def to_key_event(cls, shortcut: str):
        """Convert from string shortcut (Shift+Ctrl+Alt+LETTER) to KeyEvent"""
        key_event = KeyEvent()
        keys = shortcut.split('+')
        try:
            for m in keys[:-1]:
                key_event.Modifiers += cls.MODIFIERS[m.lower()]
            key_event.KeyCode = getattr(Key, keys[-1].upper())
        except Exception as e:
            error(e)
            key_event = None
        return key_event

    @classmethod
    def get_url_script(cls, command: Union[str, dict]) -> str:
        """Get uno command or url for macro"""
        url = command
        if isinstance(url, str) and not url.startswith('.uno:'):
            url = f'.uno:{command}'
        elif isinstance(url, dict):
            url = Macro.get_url_script(command)
        return url

    def _get_shortcut(self, k):
        """Get shortcut for key event"""
        # ~ print(k.KeyCode, str(k.KeyChar), k.KeyFunc, k.Modifiers)
        shortcut = f'{self.COMBINATIONS[k.Modifiers]}+{self.KEYS[k.KeyCode]}'
        return shortcut

    def _get_info(self, key):
        """Get shortcut and command"""
        cmd = self._config.getCommandByKeyEvent(key)
        shortcut = self._get_shortcut(key)
        return shortcut, cmd

    def get_all(self):
        """Get all events key"""
        events = [(self._get_info(k)) for k in self._config.AllKeyEvents]
        return events

    def get_by_command(self, command: Union[str, dict]):
        """Get shortcuts by command"""
        url = LOShortCuts.get_url_script(command)
        key_events = self._config.getKeyEventsByCommand(url)
        shortcuts = [self._get_shortcut(k) for k in key_events]
        return shortcuts

    def get_by_shortcut(self, shortcut: str):
        """Get command by shortcut"""
        command = ''
        key_event = LOShortCuts.to_key_event(shortcut)
        if key_event:
            command = self._config.getCommandByKeyEvent(key_event)
        return command

    def set(self, shortcut: str, command: Union[str, dict]) -> bool:
        """Set shortcut to command

        :param shortcut: Shortcut like Shift+Ctrl+Alt+LETTER
        :type shortcut: str
        :param command: Command tu assign, 'UNOCOMMAND' or dict with macro info
        :type command: str or dict
        :return: True if set sucesfully
        :rtype: bool
        """
        result = True
        url = LOShortCuts.get_url_script(command)
        key_event = LOShortCuts.to_key_event(shortcut)
        try:
            self._config.setKeyEvent(key_event, url)
            self._config.store()
        except Exception as e:
            error(e)
            result = False

        return result

    def remove_by_shortcut(self, shortcut: str):
        """Remove by shortcut"""
        key_event = LOShortCuts.to_key_event(shortcut)
        try:
            self._config.removeKeyEvent(key_event)
            result = True
        except NoSuchElementException:
            debug(f'No exists: {shortcut}')
            result = False
        return result

    def remove_by_command(self, command: Union[str, dict]):
        """Remove by shortcut"""
        url = LOShortCuts.get_url_script(command)
        self._config.removeCommandFromAllKeyEvents(url)
        return

    def reset(self):
        """Reset configuration"""
        self._config.reset()
        self._config.store()
        return


class LOMenuDebug():
    """Classe for debug info menu"""

    @classmethod
    def _get_info(cls, menu, index):
        """Get every option menu"""
        line = f"({index}) {menu.get('CommandURL', '----------')}"
        submenu = menu.get('ItemDescriptorContainer', None)
        if not submenu is None:
            line += cls._get_submenus(submenu)
        return line

    @classmethod
    def _get_submenus(cls, menu, level=1):
        """Get submenus"""
        line = ''
        for i, v in enumerate(menu):
            data = data_to_dict(v)
            cmd = data.get('CommandURL', '----------')
            line += f'\n{"  " * level}├─ ({i}) {cmd}'
            submenu = data.get('ItemDescriptorContainer', None)
            if not submenu is None:
                line += cls._get_submenus(submenu, level + 1)
        return line

    def __call__(cls, menu):
        for i, m in enumerate(menu):
            data = data_to_dict(m)
            print(cls._get_info(data, i))
        return


class LOMenuBase():
    """Classe base for menus"""
    NODE = 'private:resource/menubar/menubar'
    config = None
    menus = None
    app = ''

    @classmethod
    def _get_index(cls, parent: Any, name: Union[int, str]=''):
        """Get index menu from name

        :param parent: Menu parent
        :type parent: pyUno
        :param name: Menu name for search if is str
        :type name: int or str
        :return: Index of menu
        :rtype: int
        """
        index = None
        if isinstance(name, str) and name:
            for i, m in enumerate(parent):
                menu = data_to_dict(m)
                if menu.get('CommandURL', '') == name:
                    index = i
                    break
        elif isinstance(name, str):
            index = len(parent) - 1
        elif isinstance(name, int):
            index = name
        return index

    @classmethod
    def _get_command_url(cls, menu: dict):
        """Get url from command and set shortcut

        :param menu: Menu data
        :type menu: dict
        :return: URL command
        :rtype: str
        """
        shortcut = menu.pop('ShortCut', '')
        command = menu['CommandURL']
        url = LOShortCuts.get_url_script(command)
        if shortcut:
            LOShortCuts(cls.app).set(shortcut, command)
        return url

    @classmethod
    def _save(cls, parent: Any, menu: dict, index: int):
        """Insert menu

        :param parent: Menu parent
        :type parent: pyUno
        :param menu: New menu data
        :type menu: dict
        :param index: Position to insert
        :type index: int
        """
        # ~ Some day
        # ~ self._menus.insertByIndex(index, new_menu)
        properties = dict_to_property(menu, True)
        uno.invoke(parent, 'insertByIndex', (index, properties))
        cls.config.replaceSettings(cls.NODE, cls.menus)
        return

    @classmethod
    def _insert_submenu(cls, parent: Any, menus: list):
        """Insert submenus recursively

        :param parent: Menu parent
        :type parent: pyUno
        :param menus: List of menus
        :type menus: list
        """
        for i, menu in enumerate(menus):
            submenu = menu.pop('Submenu', False)
            if submenu:
                idc = cls.config.createSettings()
                menu['ItemDescriptorContainer'] = idc
            menu['Type'] = 0
            if menu['Label'][0] == '-':
                menu['Type'] = 1
            else:
                menu['CommandURL'] = cls._get_command_url(menu)
            cls._save(parent, menu, i)
            if submenu:
                cls._insert_submenu(idc, submenu)
        return

    @classmethod
    def _get_first_command(cls, command):
        url = command
        if isinstance(command, dict):
            url = Macro.get_url_script(command)
        return url

    @classmethod
    def insert(cls, parent: Any, menu: dict, after: Union[int, str]=''):
        """Insert new menu

        :param parent: Menu parent
        :type parent: pyUno
        :param menu: New menu data
        :type menu: dict
        :param after: After menu insert
        :type after: int or str
        """
        index = cls._get_index(parent, after) + 1
        submenu = menu.pop('Submenu', False)
        menu['Type'] = 0
        idc = cls.config.createSettings()
        menu['ItemDescriptorContainer'] = idc
        menu['CommandURL'] = cls._get_first_command(menu['CommandURL'])
        cls._save(parent, menu, index)
        if submenu:
            cls._insert_submenu(idc, submenu)
        return

    @classmethod
    def remove(cls, parent: Any, name: Union[str, dict]):
        """Remove name in parent

        :param parent: Menu parent
        :type parent: pyUno
        :param menu: Menu name
        :type menu: str
        """
        if isinstance(name, dict):
            name = Macro.get_url_script(name)
        index = cls._get_index(parent, name)
        if index is None:
            debug(f'Not found: {name}')
            return
        uno.invoke(parent, 'removeByIndex', (index,))
        cls.config.replaceSettings(cls.NODE, cls.menus)
        cls.config.store()
        return


class LOMenu(object):
    """Classe for individual menu"""

    def __init__(self, config: Any, menus: Any, app: str, menu: Any):
        """
        :param config: Configuration Mananer
        :type config: pyUno
        :param menus: Menu bar main
        :type menus: pyUno
        :param app: Name LibreOffice module
        :type app: str
        :para menu: Particular menu
        :type menu: pyUno
        """
        self._config = config
        self._menus = menus
        self._app = app
        self._parent = menu

    def __contains__(self, name):
        """If exists name in menu"""
        exists = False
        for m in self._parent:
            menu = data_to_dict(m)
            cmd = menu.get('CommandURL', '')
            if name == cmd:
                exists = True
                break
        return exists

    def __getitem__(self, index):
        """Index access"""
        if isinstance(index, int):
            menu = data_to_dict(self._parent[index])
        else:
            for m in self._parent:
                menu = data_to_dict(m)
                cmd = menu.get('CommandURL', '')
                if cmd == index:
                    break

        obj = LOMenu(self._config, self._menus, self._app,
            menu['ItemDescriptorContainer'])
        return obj

    def debug(self):
        """Debug menu"""
        LOMenuDebug()(self._parent)
        return

    def insert(self, menu: dict, after: Union[int, str]='', save: bool=True):
        """Insert new menu

        :param menu: New menu data
        :type menu: dict
        :param after: Insert in after menu
        :type after: int or str
        :param save: For persistente save
        :type save: bool
        """
        LOMenuBase.config = self._config
        LOMenuBase.menus = self._menus
        LOMenuBase.app = self._app
        LOMenuBase.insert(self._parent, menu, after)
        if save:
            self._config.store()
        return

    def remove(self, menu: str):
        """Remove menu

        :param menu: Menu name
        :type menu: str
        """
        LOMenuBase.config = self._config
        LOMenuBase.menus = self._menus
        LOMenuBase.remove(self._parent, menu)
        return


class LOMenuApp(object):
    """Classe for manager menu by LibreOffice module"""
    NODE = 'private:resource/menubar/menubar'
    MENUS = {
        'file': '.uno:PickList',
        'picklist': '.uno:PickList',
        'tools': '.uno:ToolsMenu',
        'help': '.uno:HelpMenu',
        'window': '.uno:WindowList',
        'edit': '.uno:EditMenu',
        'view': '.uno:ViewMenu',
        'insert': '.uno:InsertMenu',
        'format': '.uno:FormatMenu',
        'styles': '.uno:FormatStylesMenu',
        'formatstyles': '.uno:FormatStylesMenu',
        'sheet': '.uno:SheetMenu',
        'data': '.uno:DataMenu',
        'table': '.uno:TableMenu',
        'formatform': '.uno:FormatFormMenu',
        'page': '.uno:PageMenu',
        'shape': '.uno:ShapeMenu',
        'slide': '.uno:SlideMenu',
        'slideshow': '.uno:SlideShowMenu',
    }

    def __init__(self, app: str):
        """
        :param app: LibreOffice Module: calc, writer, draw, impress, math, main
        :type app: str
        """
        self._app = app
        self._config = self._get_config()
        self._menus = self._config.getSettings(self.NODE, True)

    def _get_config(self):
        """Get config manager"""
        service = 'com.sun.star.ui.ModuleUIConfigurationManagerSupplier'
        type_app = LODocuments.TYPES[self._app]
        manager = create_instance(service, True)
        config = manager.getUIConfigurationManager(type_app)
        return config

    def debug(self):
        """Debug menu"""
        LOMenuDebug()(self._menus)
        return

    def __contains__(self, name):
        """If exists name in menu"""
        exists = False
        for m in self._menus:
            menu = data_to_dict(m)
            cmd = menu.get('CommandURL', '')
            if name == cmd:
                exists = True
                break
        return exists

    def __getitem__(self, index):
        """Index access"""
        if isinstance(index, int):
            menu = data_to_dict(self._menus[index])
        else:
            for m in self._menus:
                menu = data_to_dict(m)
                cmd = menu.get('CommandURL', '')
                if cmd == index or cmd == self.MENUS[index.lower()]:
                    break

        obj = LOMenu(self._config, self._menus, self._app,
            menu['ItemDescriptorContainer'])
        return obj

    def insert(self, menu: dict, after: Union[int, str]='', save: bool=True):
        """Insert new menu

        :param menu: New menu data
        :type menu: dict
        :param after: Insert in after menu
        :type after: int or str
        :param save: For persistente save
        :type save: bool
        """
        LOMenuBase.config = self._config
        LOMenuBase.menus = self._menus
        LOMenuBase.app = self._app
        LOMenuBase.insert(self._menus, menu, after)
        if save:
            self._config.store()
        return

    def remove(self, menu: str):
        """Remove menu

        :param menu: Menu name
        :type menu: str
        """
        LOMenuBase.config = self._config
        LOMenuBase.menus = self._menus
        LOMenuBase.remove(self._menus, menu)
        return


class LOMenus(object):
    """Classe for manager menus"""

    def __getitem__(self, index):
        """Index access"""
        return LOMenuApp(index)


class LOEvents():

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

    def __contains__(self, item):
        return self.obj.hasByName(item)

    def __getitem__(self, index):
        """Index access"""
        return self.obj.getByName(index)

    def __setitem__(self, name: str, macro: dict):
        """Set macro to event

        :param name: Event name
        :type name: str
        :param macro: Macro execute in event
        :type name: dict
        """
        pv = '[]com.sun.star.beans.PropertyValue'
        args = ()
        if macro:
            url = Macro.get_url_script(macro)
            args = dict_to_property(dict(EventType='Script', Script=url))
        uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, args)))

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

    @property
    def names(self):
        return self.obj.ElementNames

    def remove(self, name):
        pv = '[]com.sun.star.beans.PropertyValue'
        uno.invoke(self.obj, 'replaceByName', (name, uno.Any(pv, ())))
        return


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

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

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

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

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

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

            return result

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

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

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

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

    @classproperty
    def cmd(cls):
        """Disable or enable commands"""
        return cls.commands

    @classproperty
    def desktop(cls):
        """Create desktop instance

        :return: Desktop instance
        :rtype: pyUno
        """
        obj = create_instance('com.sun.star.frame.Desktop', True)
        return obj

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

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

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

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

        :return: tuple of FontDescriptors
        :rtype: tuple

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

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

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


class LODocument():

    def __init__(self, obj):
        self._obj = obj
        self._cc = obj.getCurrentController()
        self._undo = True

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

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

    @property
    def type(self):
        """Get type document"""
        return self._type

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

    @property
    def title(self):
        """Get title document"""
        return self.obj.getTitle()
    @title.setter
    def title(self, value):
        self.obj.setTitle(value)

    @property
    def uid(self):
        """Get Runtime UID"""
        return self.obj.RuntimeUID

    @property
    def is_saved(self):
        """Get is saved"""
        return self.obj.hasLocation()

    @property
    def is_modified(self):
        """Get is modified"""
        return self.obj.isModified()

    @property
    def is_read_only(self):
        """Get is read only"""
        return self.obj.isReadonly()

    @property
    def path(self):
        """Get path in system files"""
        return Paths.to_system(self.obj.URL)

    @property
    def dir(self):
        """Get directory from path"""
        return Paths(self.path).path

    @property
    def file_name(self):
        """Get only file name"""
        return Paths(self.path).file_name

    @property
    def name(self):
        """Get name without extension"""
        return Paths(self.path).name

    @property
    def visible(self):
        """Get windows visible"""
        w = self.frame.ContainerWindow
        return w.isVisible()
    @visible.setter
    def visible(self, value):
        w = self.frame.ContainerWindow
        w.setVisible(value)

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

    @property
    def status_bar(self):
        """Get status bar"""
        bar = self._cc.getStatusIndicator()
        return bar

    @property
    def selection(self):
        """Get current selecction"""
        sel = self.obj.CurrentSelection
        return sel

    @property
    def table_auto_formats(self):
        taf = create_instance('com.sun.star.sheet.TableAutoFormats')
        return taf.ElementNames

    def save(self, path: str='', args: dict={}) -> bool:
        """Save document

        :param path: Path to save document
        :type path: str
        :param args: Optional: Extra argument for save
        :type args: dict
        :return: True if save correctly, False if not
        :rtype: bool
        """
        if not path:
            self.obj.store()
            return True

        path_save = Paths.to_url(path)
        opt = dict_to_property(args)

        try:
            self.obj.storeAsURL(path_save, opt)
        except Exception as e:
            error(e)
            return False

        return True

    def close(self):
        """Close document"""
        self.obj.close(True)
        return

    def to_pdf(self, path: str='', args: dict={}):
        """Export to PDF

        :param path: Path to export document
        :type path: str
        :param args: Optional: Extra argument for export
        :type args: dict
        :return: None if path or stream in memory
        :rtype: bytes or None

        `See PDF Export <https://wiki.documentfoundation.org/Macros/Python_Guide/PDF_export_filter_data>`_
        """
        stream = None
        path_pdf = 'private:stream'

        filter_name = f'{self.type}_pdf_Export'
        filter_data = dict_to_property(args, True)
        filters = {
            'FilterName': filter_name,
            'FilterData': filter_data,
        }
        if path:
            path_pdf = Paths.to_url(path)
        else:
            stream = IOStream.output()
            filters['OutputStream'] = stream

        opt = dict_to_property(filters)
        try:
            self.obj.storeToURL(path_pdf, opt)
        except Exception as e:
            error(e)

        if not stream is None:
            stream = stream.buffer

        return stream

    def export(self, path: str='', filter_name: str='', args: dict={}):
        """Export to others formats

        :param path: Path to export document
        :type path: str
        :param filter_name: Filter name to export
        :type filter_name: str
        :param args: Optional: Extra argument for export
        :type args: dict
        :return: None if path or stream in memory
        :rtype: bytes or None
        """
        FILTERS = {
            'xlsx': 'Calc MS Excel 2007 XML',
            'xls': 'MS Excel 97',
            'docx': 'MS Word 2007 XML',
            'doc': 'MS Word 97',
            'rtf': 'Rich Text Format',
        }

        stream = None
        path_target = 'private:stream'

        filter_name = FILTERS.get(filter_name, filter_name)
        filter_data = dict_to_property(args, True)
        filters = {
            'FilterName': filter_name,
            'FilterData': filter_data,
        }
        if path:
            path_target = Paths.to_url(path)
        else:
            stream = IOStream.output()
            filters['OutputStream'] = stream

        opt = dict_to_property(filters)
        try:
            self.obj.storeToURL(path_target, opt)
        except Exception as e:
            error(e)

        if not stream is None:
            stream = stream.buffer

        return stream

    def set_focus(self):
        """Send focus to windows"""
        w = self.frame.ComponentWindow
        w.setFocus()
        return

    def copy(self):
        """Copy current selection"""
        LOMain.dispatch(self.frame, 'Copy')
        return

    def paste(self):
        """Paste current content in clipboard"""
        sc = create_instance('com.sun.star.datatransfer.clipboard.SystemClipboard')
        transferable = sc.getContents()
        self._cc.insertTransferable(transferable)
        return

    def paste_special(self):
        """Insert contents, show dialog box Paste Special"""
        LOMain.dispatch(self.frame, 'InsertContents')
        return

    def paste_values(self):
        """Paste only values"""
        args = {
            'Flags': 'SVDT',
            # ~ 'FormulaCommand': 0,
            # ~ 'SkipEmptyCells': False,
            # ~ 'Transpose': False,
            # ~ 'AsLink': False,
            # ~ 'MoveMode': 4,
        }
        LOMain.dispatch(self.frame, 'InsertContents', args)
        return

    def clear_undo(self):
        """Clear history undo"""
        self.obj.getUndoManager().clear()
        return


class LODocMain():
    """Classe for start module"""
    _type = 'main'

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

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

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


class LOCellStyle():

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

    def __str__(self):
        return f'CellStyle: {self.name}'

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

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

    @property
    def properties(self):
        properties = self.obj.PropertySetInfo.Properties
        data = {p.Name: getattr(self.obj, p.Name) for p in properties}
        return data
    @properties.setter
    def properties(self, values):
        _set_properties(self.obj, values)


class LOCellStyles():

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

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

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

    def __setitem__(self, key, value):
        self.obj[key] = value

    def __delitem__(self, key):
        if not isinstance(key, str):
            key = key.Name
        del self.obj[key]

    def __contains__(self, item):
        return item in self.obj

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

    @property
    def names(self):
        return self.obj.ElementNames

    def new(self, name: str):
        obj = self._doc.create_instance('com.sun.star.style.CellStyle')
        self.obj[name] = obj
        return LOCellStyle(obj)


class LODocCalc(LODocument):
    """Classe for Calc module"""
    TYPE_RANGES = ('ScCellObj', 'ScCellRangeObj')
    RANGES = 'ScCellRangesObj'
    SHAPE = 'com.sun.star.drawing.SvxShapeCollection'
    _type = 'calc'

    def __init__(self, obj):
        super().__init__(obj)
        self._sheets = obj.Sheets
        self._listener_range_selection = None

    def __getitem__(self, index):
        """Index access"""
        return LOCalcSheet(self._sheets[index])

    def __setitem__(self, key: str, value: Any):
        """Insert new sheet"""
        self._sheets[key] = value

    def __len__(self):
        return self._sheets.Count

    def __contains__(self, item):
        return item in self._sheets

    def __iter__(self):
        self._i = 0
        return self

    def __next__(self):
        try:
            sheet = LOCalcSheet(self._sheets[self._i])
        except Exception as e:
            raise StopIteration
        self._i += 1
        return sheet

    def __str__(self):
        return f'Calc: {self.title}'

    @property
    def selection(self):
        sel = self.obj.CurrentSelection
        type_obj = sel.ImplementationName
        if type_obj in self.TYPE_RANGES:
            sel = LOCalcRange(sel)
        elif type_obj == self.RANGES:
            sel = LOCalcRanges(sel)
        elif type_obj == self.SHAPE:
            if len(sel) == 1:
                sel = LOShape(sel[0])
            else:
                sel = LOShapes(sel)
        else:
            debug(type_obj)
        return sel

    @property
    def headers(self):
        """Get true if is visible columns/rows headers"""
        return self._cc.ColumnRowHeaders
    @headers.setter
    def headers(self, value):
        """Set visible columns/rows headers"""
        self._cc.ColumnRowHeaders = value

    @property
    def tabs(self):
        """Get true if is visible tab sheets"""
        return self._cc.SheetTabs
    @tabs.setter
    def tabs(self, value):
        """Set visible tab sheets"""
        self._cc.SheetTabs = value

    @property
    def names(self):
        """Get all sheet names"""
        names = self.obj.Sheets.ElementNames
        return names

    @property
    def active(self):
        """Get active sheet"""
        return LOCalcSheet(self._cc.ActiveSheet)

    @property
    def new_sheet(self):
        sheet = self._create_instance('com.sun.star.sheet.Spreadsheet')
        return sheet

    @property
    def events(self):
        return LOEvents(self.obj.Events)

    @property
    def cs(self):
        return self.cell_styles
    @property
    def cell_styles(self):
        obj = self.obj.StyleFamilies['CellStyles']
        return LOCellStyles(obj, self)

    def activate(self, sheet: Any):
        """Activate sheet

        :param sheet: Sheet to activate
        :type sheet: str, pyUno or LOCalcSheet
        """
        obj = sheet
        if isinstance(sheet, LOCalcSheet):
            obj = sheet.obj
        elif isinstance(sheet, str):
            obj = self._sheets[sheet]
        self._cc.setActiveSheet(obj)
        return

    def remove(self, name: str):
        """Remove sheet by name

        :param name: Name sheet will remove
        :type name: str
        """
        if isinstance(name, LOCalcSheet):
            name = name.name
        self._sheets.removeByName(name)
        return

    def move(self, name:str, pos: int=-1):
        """Move sheet name to position

        :param name: Name sheet to move
        :type name: str
        :param pos: New position, if pos=-1 move to end
        :type pos: int
        """
        index = pos
        if pos < 0:
            index = len(self)
        if isinstance(name, LOCalcSheet):
            name = name.name
        self._sheets.moveByName(name, index)
        return

    def _get_new_name_sheet(self, name):
        i = 1
        new_name = f'{name}_{i}'
        while new_name in self:
            i += 1
            new_name = f'{name}_{i}'
        return new_name

    def copy_sheet(self, name: Any, new_name: str='', pos: int=-1):
        """Copy sheet by name

        """
        if isinstance(name, LOCalcSheet):
            name = name.name
        index = pos
        if pos < 0:
            index = len(self)
        if not new_name:
            new_name = self._get_new_name_sheet(name)
        self._sheets.copyByName(name, new_name, index)
        return LOCalcSheet(self._sheets[new_name])

    def copy_from(self, doc: Any, source: Any=None, target: Any=None, pos: int=-1):
        """Copy sheet from document

        """
        index = pos
        if pos < 0:
            index = len(self)

        names = source
        if not source:
            names = doc.names
        elif isinstance(source, str):
            names = (source,)
        elif isinstance(source, LOCalcSheet):
            names = (source.name,)

        new_names = target
        if not target:
            new_names = names
        elif isinstance(target, str):
            new_names = (target,)

        for i, name in enumerate(names):
            self._sheets.importSheet(doc.obj, name, index + i)
            self[index + i].name = new_names[i]

        return LOCalcSheet(self._sheets[index])

    def sort(self, reverse=False):
        """Sort sheets by name

        :param reverse: For order in reverse
        :type reverse: bool
        """
        names = sorted(self.names, reverse=reverse)
        for i, n in enumerate(names):
            self.move(n, i)
        return

    @run_in_thread
    def start_range_selection(self, controllers: Any, args: dict={}):
        """Start select range selection by user

        `See Api RangeSelectionArguments <https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1sheet_1_1RangeSelectionArguments.html>`_
        """
        if args:
            args['CloseOnMouseRelease'] = args.get('CloseOnMouseRelease', True)
        else:
            args = dict(
                Title = 'Please select a range',
                CloseOnMouseRelease = True)
        properties = dict_to_property(args)

        self._listener_range_selection = EventsRangeSelectionListener(controllers(self))
        self._cc.addRangeSelectionListener(self._listener_range_selection)
        self._cc.startRangeSelection(properties)
        return

    def remove_range_selection_listener(self):
        if not self._listener_range_selection is None:
            self._cc.removeRangeSelectionListener(self._listener_range_selection)
        return

    def select(self, rango: Any):
        obj = rango
        if hasattr(rango, 'obj'):
            obj = rango.obj
        self._cc.select(obj)
        return

    @property
    def ranges(self):
        obj = self._create_instance('com.sun.star.sheet.SheetCellRanges')
        return LOCalcRanges(obj)

    def get_ranges(self, address: str):
        ranges = self.ranges
        ranges.add([sheet[address] for sheet in self])
        return ranges




class LOCalcRanges(object):

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

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def __len__(self):
        return self._obj.Count

    def __iter__(self):
        self._index = 0
        return self

    def __next__(self):
        try:
            r = self.obj[self._index]
            rango = LOCalcRange(r)
        except IndexError:
            raise StopIteration

        self._index += 1
        return rango

    def __contains__(self, item):
        return self._obj.hasByName(item.name)

    def __getitem__(self, index):
        r = self.obj[index]
        rango = LOCalcRange(r)
        return rango

    def __str__(self):
        s = f'Ranges: {self.names}'
        return s

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

    @property
    def names(self):
        return self.obj.ElementNames

    @property
    def data(self):
        rows = [r.data for r in self]
        return rows
    @data.setter
    def data(self, values):
        for i, data in enumerate(values):
            self[i].data = data

    @property
    def style(self):
        return ''
    @style.setter
    def style(self, value):
        for r in self:
            r.style = value

    def add(self, rangos: Any):
        if isinstance(rangos, LOCalcRange):
            rangos = (rangos,)
        for r in rangos:
            self.obj.addRangeAddress(r.range_address, False)
        return

    def remove(self, rangos: Any):
        if isinstance(rangos, LOCalcRange):
            rangos = (rangos,)
        for r in rangos:
            self.obj.removeRangeAddress(r.range_address)
        return


class LOWriterTextRange(object):

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

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

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

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

    def insert_comment(self, content: str, author: str='', dt: Any=None):
        # ~ range.Text.insertTextContent(cursor, comment, False)
        comment = self._doc._create_instance('com.sun.star.text.textfield.Annotation')
        comment.Content = content
        comment.Author = author
        comment.attach(self.cursor.End)
        return


class LODocWriter(LODocument):
    _type = 'writer'
    TEXT_RANGES = 'SwXTextRanges'

    def __init__(self, obj):
        super().__init__(obj)
        self._view_settings = self._cc.ViewSettings

    @property
    def selection(self):
        sel = self.obj.CurrentSelection
        type_obj = sel.ImplementationName
        if type_obj == self.TEXT_RANGES:
            if len(sel) == 1:
                sel = LOWriterTextRange(sel[0], self)

        return sel

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


class LODocDrawImpress(LODocument):

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


class LODocDraw(LODocDrawImpress):
    _type = 'draw'

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


class LODocImpress(LODocDrawImpress):
    _type = 'impress'

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


class LODocMath(LODocDrawImpress):
    _type = 'math'

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


class LODocBase(LODocument):
    _type = 'base'

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


class LODocIDE(LODocument):
    _type = 'basicide'

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


def __getattr__(name):
    classes = {
        'json': Json,
        'macro': Macro,
        'shell': Shell,
        'timer': Timer,
        'hash': Hash,
        'config': Config,
        'url': Url,
        'color': Color(),
        'io': IOStream,
        'clipboard': ClipBoard,
        'shortcuts': LOShortCuts(),
        'menus': LOMenus(),
        'lo': LOMain,
        'command': LOMain.cmd,
        'docs': LODocuments(),
    }
    if name in classes:
        return classes[name]

    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")


class LOServer(object):
    """Started LibeOffice like server
    """
    HOST = 'localhost'
    PORT = '8100'
    ARG = f'socket,host={HOST},port={PORT};urp;StarOffice.ComponentContext'
    CMD = ['soffice',
        '-env:SingleAppInstance=false',
        '-env:UserInstallation=file:///tmp/LO_Process8100',
        '--headless', '--norestore', '--invisible',
        f'--accept={ARG}']

    def __init__(self):
        self._server = None
        self._ctx = None
        self._sm = None
        self._start_server()
        self._init_values()

    def _init_values(self):
        global CTX
        global SM

        if not self.is_running:
            return

        ctx = uno.getComponentContext()
        service = 'com.sun.star.bridge.UnoUrlResolver'
        resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx)
        self._ctx = resolver.resolve('uno:{}'.format(self.ARG))
        self._sm = self._ctx.getServiceManager()
        CTX = self._ctx
        SM = self._sm
        return

    @property
    def is_running(self):
        try:
            s = socket.create_connection((self.HOST, self.PORT), 5.0)
            s.close()
            debug('LibreOffice is running...')
            return True
        except ConnectionRefusedError:
            return False

    def _start_server(self):
        if self.is_running:
            return

        for i in range(3):
            self._server = subprocess.Popen(self.CMD,
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            time.sleep(3)
            if self.is_running:
                break
        return

    def stop(self):
        """Stop server
        """
        if self._server is None:
            print('Search pgrep soffice')
        else:
            self._server.terminate()
        debug('LibreOffice is stop...')
        return

    def _create_instance(self, name, with_context=True):
        if with_context:
            instance = self._sm.createInstanceWithContext(name, self._ctx)
        else:
            instance = self._sm.createInstance(name)
        return instance
