#!/usr/bin/python3
# -*- coding: UTF-8 -*-

import sys
import os
import os.path
import uno
import errno

# КЛАСС ВЗАИМОДЕЙСТВИЯ С LIBREOFFICE
class unoOfficeClass(object):

    ### ИНИЦИАЛИЗАЦИЯ КЛАССА
    def __init__(self):
        import sys
        self.args = sys.argv[1:]
        self.office = False
        self.context = object
        self.FRAME_DESCTOP = 'com.sun.star.frame.Desktop'
        self.UNO_URL_RESOLVER = 'com.sun.star.bridge.UnoUrlResolver'
        # Если аргументов нет
        if len(self.args) == 0:
            self.COMMAND = 'help'
        # Если параметры вызова справки
        elif self.args[0] in ['-h', '--help']:
            # Вывод справочной информации
            self.COMMAND = 'help'
        # Если команда new filendme
        elif self.args[0] == 'new':
            # создание нового документа
            self.COMMAND = 'new'
            self.PIPE_NAME = 'AstraLinuxFlyFileManager_new'
            self.OFFICE_CONNECT = ['soffice', \
                    '--accept=pipe,name={};urp;'.format(self.PIPE_NAME), \
                    '--nocrashreport', '--nodefault', \
                    '--nologo', '--nofirststartwizard', '--norestore']
            self.OFFICE_CONTEXT = 'uno:pipe,name={};'.format(self.PIPE_NAME) \
                + 'urp;StarOffice.ComponentContext'
        # Если команда find [-c|--case-sensitive] substr filename
        elif self.args[0] == 'find':
            # Поиск подстроки в документе
            self.COMMAND = 'find'
            self.PIPE_NAME = 'AstraLinuxFlyFileManager_find'
            if ('XDG_RUNTIME_DIR' in os.environ) \
                    and os.path.isdir(os.environ['XDG_RUNTIME_DIR']):
                profile_dir = 'file://{}/unooffice'.format(os.environ['XDG_RUNTIME_DIR'])
            else:
                profile_dir = 'file:///tmp/unooffice-{}'.format(os.environ['USER'])
            self.OFFICE_CONNECT = ['soffice', \
                    '--accept=pipe,name={};urp;'.format(self.PIPE_NAME), \
                    '--nocrashreport', '--nodefault', \
                    '--nologo', '--nofirststartwizard', '--norestore', \
                    '-env:UserInstallation={}'.format(profile_dir)]
            self.OFFICE_CONTEXT = 'uno:pipe,name={};'.format(self.PIPE_NAME) \
                + 'urp;StarOffice.ComponentContext'
        # Если команда stop
        elif self.args[0] == 'stop':
            # Останов LibreOffice в режиме взаимодействия по UNO
            self.COMMAND = 'stop'
            self.PIPE_NAME = 'AstraLinuxFlyFileManager_find'
            self.OFFICE_CONTEXT = 'uno:pipe,name={};'.format(self.PIPE_NAME) \
                + 'urp;StarOffice.ComponentContext'
        # Во всех остальных случаях
        else:
            # Вывод справочной информации
            self.COMMAND = 'help'

    ### ФОРМИРОВАНИЕ ПАРАМЕТРОВ ОТКРЫТИЯ ДОКУМЕНТА
    def uno_props(self, **args):
        from com.sun.star.beans import PropertyValue
        props = []
        for key in args:
            prop = PropertyValue()
            prop.Name = key
            prop.Value = args[key]
            props.append(prop)
        return tuple(props)

    ### ФОРМИРОВАНИЕ ИМЕНИ ФАЙЛА
    def get_docPath(self, name):
        # Если путь начинается с наклонной черты
        if name.startswith('/'):
            # Возвращаем полученное имя
            return name
        # Если путь НЕ начинается с наклонной черты
        else:
            # Возвращаем имя вначале дополненное текущим рабочим каталогом
            return os.path.join(os.getcwd(), name)

    ### ЗАПУСК ПРОЦЕССА LIBREOFFICE В РЕЖИМЕ ВЗАИМОДЕЙСТВИЯ UNO
    def start_office(self):
        import time
        from subprocess import Popen, DEVNULL
        from com.sun.star.connection import NoConnectException
        localContext = uno.getComponentContext()
        resolver = localContext.ServiceManager.createInstanceWithContext( \
                self.UNO_URL_RESOLVER, localContext)
        # Подключение к LibreOffice
        try:
            # Попытка подключения к LibreOffice
            self.context = resolver.resolve(self.OFFICE_CONTEXT)
        # Если подключиться не удалось
        except NoConnectException as ex:
            # Запуск процесса LibreOffice, взаимодействующего по UNO
            self.office = Popen( \
                    self.OFFICE_CONNECT, stdout=DEVNULL, stderr=DEVNULL)
            # Цикл попыток подключения к запущенному процессу LibreOffice
            for i in range(100):
                # Пробуем
                try:
                    # Ждем 0.2 сек.
                    time.sleep(0.2)
                    # Снова пробуем подключиться к LibreOffice
                    self.context = resolver.resolve(self.OFFICE_CONTEXT)
                    # Если удалось подключиться выходим из цикла
                    break
                # Если не удалось подключиться
                except NoConnectException as ex:
                    # Игнорируем неудачное подключение
                    pass
            # Если в течение 20 сек. подключиться не удалось
            else:
                # Останов запущенного процесса LibreOffice
                self.office.terminate()
                ## Выход с кодом ошибочного завершения
                sys.exit(errno.ESRCH)

    ### ОСТАНОВ ПРОЦЕССА LIBREOFFICE В РЕЖИМЕ ВЗАИМОДЕЙСТВИЯ UNO
    def stop_office(self):
        from subprocess import call
        from com.sun.star.connection import NoConnectException
        # Если программа была вызвана с командой stop
        if self.args[0] == 'stop':
            # Получение объекта для взаимодействия с LibreOffice
            localContext = uno.getComponentContext()
            resolver = localContext.ServiceManager.createInstanceWithContext( \
                    self.UNO_URL_RESOLVER, localContext)
            try:
                self.context = resolver.resolve(self.OFFICE_CONTEXT)
                # Получение объекта для взаимодействия с LibreOffice
                desktop = self.context.ServiceManager.createInstanceWithContext( \
                        self.FRAME_DESCTOP, self.context)
                # Получение объекта для перебора открытых документов
                enums = desktop.Components.createEnumeration()
                # Если открытых документов НЕТ
                if not enums.hasMoreElements():
                    # Останов процесса LibreOffice
                    call(['pkill', '-f', self.PIPE_NAME])
            # Обработка ошибки соединения с LibreOffice
            except NoConnectException as ex:
                # Продолжить выполнение программы
                pass
        # Если программа была вызвана НЕ с командой stop
        else:
            # Если процесс LibreOffice запускался в программе
            if self.office:
                # Останавливаем его
                self.office.terminate()
        # Выход с кодом успешного завершения
        sys.exit(0)

    ### СОЗДАНИЕ НОВОГО ДОКУМЕНТА
    def create_new_doc(self):
        import re
        # Определение параметров открытия нового файла
        # в соответствии с типом документа
        DOC_TYPE = {'.odt': 'private:factory/swriter',
                    '.ods': 'private:factory/scalc',
                    '.odp': 'private:factory/simpress',
                    '.odg': 'private:factory/sdraw'}
        # Проверка количества аргументов
        if len(self.args) < 2:
            # Если меньше двух, выход с кодом возврата - 22
            sys.exit(errno.EINVAL)
        # Определение пути создаваемого файла
        docPath = self.get_docPath(self.args[1])
        # Проверка имени создаваемого файла на соответствие шаблону
        if not re.search('.+\.od[tspg]$', docPath.lower()):
            # Если не соответствует, выход с кодом возврата - 22
            sys.exit(errno.EINVAL)
        # Определение расширения файла
        fileExt = docPath[-4:].lower()
        # Проверка существования файла с указанным именем
        if os.path.isfile(docPath):
            # Если такой файл уже существует, выход с кодом возврата - 17
            sys.exit(errno.EEXIST)
        # Реализация алгоритма
        try:
            # Запуск LibreOffice
            self.start_office()
            # Получение объекта для взаимодействия с LibreOffice
            desktop = self.context.ServiceManager.createInstanceWithContext( \
                    self.FRAME_DESCTOP, self.context)
            # Задание параметров открытия нового документа:
            # Hidden - режим невидимости
            props = self.uno_props(Hidden=True)
            # Открытие нового документа в соответствии с типом
            doc = desktop.loadComponentFromURL( \
                    DOC_TYPE[fileExt], '_blank', 0, props)
            # Сохранение открытого документа в файл по заданному пути
            doc.storeAsURL(uno.systemPathToFileUrl(docPath), ())
            # Закрытие документа
            doc.dispose()
            # Останов процесса LibreOffice
            self.stop_office()
            # Возврат кода успешного завершения
            sys.exit(0)
        # Обработка ошибок
        except Exception as eх:
            print(str(eх))
            sys.exit(112)

    ### ПОИСК СТРОКИ В ДОКУМЕНТЕ
    def find_substr_in_doc(self):
        # Проверка количества аргументов
        if len(self.args) < 3:
            # Если меньше двух, выход с кодом возврата - 22
            sys.exit(errno.EINVAL)
        # Определение пути создаваемого файла
        docPath = self.get_docPath(self.args[-1])
        # Проверка существования указанного файла
        if not os.path.isfile(docPath):
            # Если файл НЕ существует, выход с кодом возврата - 2
            sys.exit(errno.ENOENT)
        # Реализация алгоритма
        try:
            # Запуск LibreOffice
            self.start_office()
            # Получение объекта для взаимодействия с LibreOffice
            desktop = self.context.ServiceManager.createInstanceWithContext( \
                    self.FRAME_DESCTOP, self.context)
            # Определение строки (регулярного выражения) для поиска
            findStr = self.args[-2]
            # Задание параметров открытия документа:
            # Hidden - режим невидимости; Preview - режим предпросмотра;
            props = self.uno_props(Hidden=True, Preview=True)
            # Открытие документа из файла docPath
            doc = desktop.loadComponentFromURL( \
                    uno.systemPathToFileUrl(docPath), '_blank', 0, props)
            # Создание дескриптора для поиска
            descriptor = doc.createSearchDescriptor()
            # Определение строки для поиска
            descriptor.SearchString = findStr
            # Задание режима поиска с использованием регулярных выражений
            descriptor.SearchRegularExpression = True
            # Если заданы параметры -с или --case-sensitive
            if (len(self.args) > 3) and (self.args[1] in ['-c', '--case-sensitive']):
                # Задание режима поиска, чувствительного к регистру символов
                descriptor.SearchCaseSensitive = True
            # Поиск первого вхождения
            found = doc.findFirst(descriptor)
            # Закрытие документа
            doc.dispose()
            # Возврат кода завершения
            # Если найдено
            if found:
                # Определение кода завершения соответствующего успешному поиску
                sys.exit(0)
            # Если не найдено
            else:
                # Определение кода завершения соответствующего НЕуспешному поиску
                sys.exit(1)
        # Обработка ошибок
        except Exception as eх:
            print(str(eх))
            sys.exit(112)

    ### ПЕЧАТЬ СПРАВКИ
    def print_help(self):
        import textwrap
        print(textwrap.dedent('''\x1b[0m
Программа обработки документов с примением LibreOffice, взаимодействующим через UNO
    Версия: 2019-06-18

Применение: \x1b[32munooffice [-h, --help] [команда]\x1b[0m

Описание команд и параметров:

    \x1b[32munooffice -h, --help\x1b[0m
        Вывод справки о программе.

    \x1b[32munooffice new FILENAME\x1b[0m
        Создание нового документа с именем FILENAME.

        FILENAME - Имя создаваемого файла. Тип создаваемого документа
                   определяется по расширению файла.
                   Начинающееся с / имя считается абсолютным путем,
                   иначе имя дополняется путем к текущему каталогу.

        Коды возврата:
            0  - документ успешно создан;
            17 - файл с заданным именем уже существует;
            22 - количество параметров меньше двух или
                 имя файла не соответствует шаблону;

    \x1b[32munooffice find [-c] SUBSTR FILENAME\x1b[0m
        Поиск строки SUBSTR в документе FILENAME.

        -с, --case-sensitive - Поиск с учетом регистра символов.
                               По умолчанию поиск не чувствителен к регистру.
        SUBSTR - Регулярное выражение, задающее подстроку для поиска
                 в соответствии с правилами, принятыми в LibreOffice.
        FILENAME - Имя файла документа.
                   Начинающееся с / имя считается абсолютным путем,
                   иначе имя дополняется путем к текущему каталогу.

        Коды возврата:
            0  - строка в документе найдена;
            1  - строка в документе НЕ найдена;
            2  - заданный файл не существует;
            22 - количество параметров меньше трех.

    \x1b[32munooffice stop\x1b[0m
        Останов процесса LibreOffice, запущенного командой find.
            '''))

### ОСНОВНОЙ МОДУЛЬ
def main():
    # Создание экземпляра класса обработки команд
    uo = unoOfficeClass()
    # Если команда find [-c|--case-sensitive] substr filename
    if uo.COMMAND == 'find':
        # Поиск подстроки в документе
        uo.find_substr_in_doc()
    # Если команда new filendme
    elif uo.COMMAND == 'new':
        # создание нового документа
        uo.create_new_doc()
    # Если команда stop
    elif uo.COMMAND == 'stop':
        # Останов LibreOffice в режиме взаимодействия по UNO
        uo.stop_office()
    # Во всех остальных случаях
    else:
        # Вывод справочной информации
        uo.print_help()

### ВЫЗОВ ОСНОВНОГО МОДУЛЯ
if __name__ == "__main__":
    main()
