#!/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'
        # Параметры открытия нового файла в соответствии с типом документа
        self.DOC_TYPE = {'writer': 'private:factory/swriter',
                    'calc': 'private:factory/scalc',
                    'impress': 'private:factory/simpress',
                    'draw': 'private:factory/sdraw'}
        # Типы процессов в соответствии с типом документа
        self.NODE_TYPE = {'writer': 'com.sun.star.text.TextDocument',
                    'calc': 'com.sun.star.sheet.SpreadsheetDocument',
                    'impress': 'com.sun.star.presentation.PresentationDocument',
                    'draw': 'com.sun.star.drawing.DrawingDocument'}
        # Типы документов в зависимости от расширения файла
        self.DOC_EXT = {'.odt':'writer', '.ott':'writer', '.fodt':'writer', '.uot':'writer',
                   '.docx':'writer', '.dotx':'writer', '.xml':'writer', '.doc':'writer',
                   '.dot':'writer', '.html':'writer', '.rtf':'writer', '.txt':'writer',
                   '.docm':'writer',
                   '.ods':'calc', '.ots':'calc', '.fods':'calc', '.uos':'calc', '.xlsx':'calc',
                   '.xltx':'calc', '.xls':'calc', '.xlt':'calc', '.dif':'calc', '.dbf':'calc',
                   '.slk':'calc', '.csv':'calc', '.xlsm':'calc',
                   '.odp':'impress', '.otp':'impress', '.odg':'impress', '.fodp':'impress',
                   '.uop':'impress', '.pptx':'impress', '.ppsx':'impress', '.potx':'impress',
                   '.ppt':'impress', '.pps':'impress', '.pot':'impress', '.pptm':'impress',
                   '.odg':'draw', '.otg':'draw', '.fodg':'draw'}

        # Если аргументов нет
        if len(self.args) == 0:
            self.COMMAND = 'help'
        # Если параметры вызова справки
        elif self.args[0] in ['-h', '--help']:
            # Вывод справочной информации
            self.COMMAND = 'help'
        # Если команда new filendme
        elif self.args[0] in ['ext', 'new']:
            # создание нового документа
            self.COMMAND = self.args[0]
            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 get_new_doc_ext(self):
        # Проверка количества аргументов
        # Если меньше двух аргументов
        if len(self.args) < 2:
            # Выход с кодом возврата - 22
            sys.exit(errno.EINVAL)
        # Если задана офисная программа
        else:
            # Определение вызываемой офисной программы
            docType = self.args[1]
            if not docType in self.DOC_TYPE.keys():
                sys.exit(errno.EINVAL)
        # Реализация алгоритма
        try:
            # Запуск LibreOffice
            self.start_office()
            # Получение объекта для взаимодействия с LibreOffice
            desktop = self.context.ServiceManager.createInstanceWithContext( \
                    self.FRAME_DESCTOP, self.context)
            # Определение формата создаваемого документа в зависимости от настроек LibreOffice
            ConfigurationProvider = 'com.sun.star.configuration.ConfigurationProvider'
            ConfigurationAccess = 'com.sun.star.configuration.ConfigurationAccess'
            nodePath = "/org.openoffice.Setup/Office/Factories/org.openoffice.Setup:Factory['{}']".format(self.NODE_TYPE[docType])
            parmName = 'ooSetupFactoryDefaultFilter'
            filterName = self.context.ServiceManager.createInstanceWithContext(
                ConfigurationProvider, self.context).createInstanceWithArguments(
                    ConfigurationAccess, self.uno_props(nodepath=nodePath)).getByName(parmName)
            # Получение форматов документов и соответствующих им расширений файлов
            docFormat_ext = {}
            TypeDetection = self.context.ServiceManager.createInstanceWithContext( \
                'com.sun.star.document.TypeDetection', self.context)
            for docFormat in TypeDetection.getElementNames():
                parms = TypeDetection[docFormat]
                for parm in parms:
                    if parm.Name == 'PreferredFilter':
                        PreferredFilter = parm.Value
                    elif  parm.Name == 'Extensions':
                        Extensions = parm.Value
                if PreferredFilter:
                    docFormat_ext[PreferredFilter] = '.'
                    if Extensions:
                        docFormat_ext[PreferredFilter] += Extensions[0]
            # Если формат файла не найден среди определенных форматов
            if not filterName in docFormat_ext.keys():
                # Выход с кодом возврата - 22
                sys.exit(errno.EINVAL)
            print(docFormat_ext[filterName][1:])
            # Останов процесса LibreOffice
            self.stop_office()
            # Возврат кода успешного завершения
            sys.exit(0)
        # Обработка ошибок
        except Exception as eх:
            print(str(eх))
            sys.exit(112)

    ### СОЗДАНИЕ НОВОГО ДОКУМЕНТА
    def create_new_doc(self):
        # Проверка количества аргументов
        # Если меньше двух аргументов
        if len(self.args) < 2:
            # Выход с кодом возврата - 22
            sys.exit(errno.EINVAL)
        # Если задано только имя создаваемого файла
        elif len(self.args) == 2:
            # Определение пути создаваемого файла
            docPath = self.get_docPath(self.args[1])            
            # Определение имени и расширения создаваемого файла
            fileName, fileExt = os.path.splitext(docPath.lower())
            # Если расширение отсутствует в списке допустимых
            if not fileExt in self.DOC_EXT.keys():
                # Выход с кодом возврата - 22
                sys.exit(errno.EINVAL)
            docType = self.DOC_EXT[fileExt]
        # Если заданы офисная программа и имя создаваемого файла
        else:
            # Определение вызываемой офисной программы
            docType = self.args[1]
            # Если расширение отсутствует в списке допустимых
            if not docType in self.DOC_TYPE.keys():
                # Выход с кодом возврата - 22
                sys.exit(errno.EINVAL)
            # Определение пути создаваемого файла
            docPath = self.get_docPath(self.args[2])
        # Реализация алгоритма
        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(self.DOC_TYPE[docType], '_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)
            # Заготовка массива объектов для поиска
            objs = []
            # Если открыта электронная таблица
            if doc.ImplementationName == 'ScModelObj':
                # Будем искать в массиве листов
                objs = doc.Sheets
            # Если открыта презентация или рисунок
            elif doc.ImplementationName == 'SdXImpressDocument':
                # Будем искать в массиве слайдов
                objs = doc.DrawPages
            # Если открыт текстовый документ
            else:
                # Будем искать в самом документе
                objs.append(doc)
            # Начальное значение результата поиска
            found = False
            # Цикл по массиву объектов для поиска
            for obj in objs:
                # Создание дескриптора для поиска
                descriptor = obj.createSearchDescriptor()
                # Определение строки для поиска
                descriptor.SearchString = findStr
                # Если возможен поиск с использованием регулярных выражений
                if 'SearchRegularExpression' in dir(descriptor):
                    # Задание режима поиска с использованием регулярных выражений
                    descriptor.SearchRegularExpression = True
                # Задание режима поиска не чувствительного к регистру символов
                descriptor.SearchCaseSensitive = False
                # Если заданы параметры -с или --case-sensitive
                if (len(self.args) > 3) and (self.args[1] in ['-c', '--case-sensitive']):
                    # Задание режима поиска чувствительного к регистру символов
                    descriptor.SearchCaseSensitive = True
                # Поиск первого вхождения и логическое ИЛИ с предыдущими результатами
                found = found or obj.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
    Версия: 2023-10-25

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

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

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

    \x1b[32munooffice ext writer|calc|impress|draw\x1b[0m
        Вывод расширения нового документа в соответствии с настройками LibreOffice

        writer|calc|impress|draw - программа LibreOffice, для которой
                                   запрашивается расширение документа.

        Коды возврата:
            0  - обработка успешно завершена;
            22 - количество параметров меньше двух или
                 недопустимое наименование программы LibreOffice.

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

        writer|calc|impress|draw - программа LibreOffice, с помощью
                                   которой будет создан документ.

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

        Коды возврата:
            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()
    # Если команда ext libreofficeapplication
    elif uo.COMMAND == 'ext':
        # получение расширения нового документа
        uo.get_new_doc_ext()
    # Если команда new filenаme
    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()
