# -*- coding: utf-8 -*-

# Copyright 2008-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
from contextlib import contextmanager

import sys
import os
import errno
from calculate.lib.cl_xml import xpath
import time
import fcntl

from utils.common import _error
from cl_template import FormatFactory, TemplatesError

from calculate.lib.cl_lang import setLocalTranslate

_ = lambda x: x
setLocalTranslate('cl_lib3', sys.modules[__name__])


class iniParser(_error):
    """Класс для работы с ini файлами

    """

    def __init__(self, iniFile=None, text=None):
        # название ini файла
        self.iniFile = iniFile or ""
        # права создаваемого ini-файла
        self.mode = 0640
        # Cоответствует ли формат файла нужному
        self.checkIni = None
        self.FD = None
        self.readOnly = False
        self.text = text
        self.locked = False
        self.formatFactory = FormatFactory(self)

    def joinText(self, iniObj, xmlNewDoc):
        """Объединяет два документа"""
        newRootNode = xmlNewDoc.documentElement
        newBodyNode = xpath.Evaluate('child::body', newRootNode)[0]
        newImportBodyNode = iniObj.doc.importNode(newBodyNode, True)
        iniObj.docObj.joinBody(iniObj.docObj.body, newImportBodyNode)
        # iniObj.docObj.insertBRtoBody(iniObj.docObj.body)

    def setMode(self, mode):
        """установка прав создаваемого ini-файла"""
        self.mode = mode

    def lockfile(self, fd, fn, timeout=5):
        """
        Блокировка файла с таймаутом
        """
        if self.locked:
            return True
        for i in range(0, timeout):
            try:
                fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
                return True
            except IOError, e:
                if e.errno != errno.EAGAIN:
                    raise e
            time.sleep(1)
        else:
            raise TemplatesError(_("Lock timeout of %s") % fn)

    @property
    def iniFile_lock(self):
        return self.iniFile + ".lock~"

    def wait_global_lock(self, timeout=10):
        lockfn = self.iniFile_lock
        for i in range(0, timeout):
            if self.locked or not os.path.exists(lockfn):
                break
            time.sleep(1)

    @contextmanager
    def lock(self):
        lockfn = self.iniFile_lock
        lockf = open(lockfn, "w+")
        self.lockfile(lockf.fileno(), lockfn)
        self.locked = True
        try:
            yield
        finally:
            self.locked = False
            os.unlink(lockfn)

    def _open(self, mode):
        self.wait_global_lock()
        return open(self.iniFile, mode)

    def openIniFile(self):
        if not self.text is None:
            return self.text
        if not os.access(self.iniFile, os.R_OK):
            return ""
        self.FD = self._open("r")
        self.lockfile(self.FD.fileno(), self.iniFile)
        textIni = self.FD.read()
        return textIni

    def openRWIniFile(self):
        if not os.access(self.iniFile, os.R_OK):
            return ""
        try:
            self.FD = self._open("r+")
        except (IOError, OSError):
            self.FD = self._open("r")
            self.readOnly = True
        self.lockfile(self.FD.fileno(), self.iniFile)
        textIni = self.FD.read()
        return textIni

    def writeIniFile(self, txtConfig):
        if self.readOnly:
            self.setError(_("Failed to write to file")
                          + _(": ") + self.iniFile)
            return False
        if not os.path.exists(self.iniFile):
            try:
                # Создание файла
                self.FD = self._open("w+")
                self.lockfile(self.FD.fileno(), self.iniFile)
                os.chmod(self.iniFile, self.mode)
            except Exception:
                self.setError(_("Failed to create the file") +
                              _(": ") + self.iniFile)
                return False
        if not self.FD:
            self.setError(_("Failed to write to file")
                          + _(": ") + self.iniFile)
            return False
        self.FD.truncate(0)
        self.FD.seek(0)
        self.FD.write(txtConfig)
        self.FD.close()
        self.FD = None
        return True

    def setVar(self, strHeader, dictVar):
        """Заменяет или добавляет область и переменные

        Добавляет область в ini-файл или объединяет с существующей
        strHeader - имя области
        dictVar - словарь переменных
        """
        textIni = self.openRWIniFile()
        nameFomat = self.checkIniFile(textIni)
        if not nameFomat:
            return False
        if type(strHeader) in (tuple, list):
            # формат plasma
            classObj = self.formatFactory.getClassObj("plasma")
        else:
            if nameFomat == "plasma":
                self.setError(_("Trying to write a variable of 'samba' "
                                "format to file %s ('plasma' format)") \
                              % self.iniFile)
                return False
            # формат samba
            classObj = self.formatFactory.getClassObj("samba")
        # создаем объект
        # и записываем в него содержимое ini-файла
        objIni = classObj(textIni)
        # создаем текст из строки заголовка и
        # словаря переменных области
        txtConfig = objIni.createTxtConfig(strHeader, dictVar)
        # создаем объект и записываем в него текст
        objIniAdd = classObj(txtConfig)
        # объединяем объекты для получения результирующего текста
        objIni.join(objIniAdd)
        # получаем текст
        txtConfig = objIni.getConfig().encode("UTF-8")
        # записываем его в ini файл
        if not self.writeIniFile(txtConfig):
            return False
        return True

    def isEmptyFile(self, textIni):
        """Если файл пустой или содержит только комментарии - False

        иначе - True
        """
        if textIni.strip():
            if filter(lambda x: x.strip(),
                      map(lambda x: x[0].split(";")[0],
                          map(lambda x: x.split("#"),
                              textIni.splitlines()))):
                return False
            else:
                return True
        else:
            return True

    def checkIniFile(self, textIni):
        """Проверка на правильность формата файла"""
        if self.checkIni is None:
            # Ошибка
            if textIni is False:
                self.checkIni = False
                return False
            self.checkIni = "samba"
            # В файле есть данные
            if not self.isEmptyFile(textIni):
                try:
                    objIni = self.formatFactory.getClassObj("plasma")(textIni)
                except Exception:
                    self.setError(_("Incorrect file format") + _(": ") + \
                                  self.iniFile)
                    self.checkIni = False
                    return self.checkIni
                allAreas = objIni.docObj.getAllAreas()
                for xmlArea in allAreas:
                    parentNode = xmlArea.parentNode
                    if parentNode and parentNode.tagName == "area":
                        self.checkIni = "plasma"
                        break
                if self.checkIni == "samba":
                    objIni = self.formatFactory.getClassObj("samba")(textIni)
                    xmlBody = objIni.docObj.getNodeBody()
                    if not xmlBody.firstChild:
                        self.checkIni = False
        return self.checkIni

    def delVar(self, strHeader, nameVar):
        """Удаляем переменную из ini файла"""
        delNameVar = "!%s" % nameVar
        dictVar = {delNameVar: "del"}
        res = self.setVar(strHeader, dictVar)
        return res

    def delArea(self, strHeader):
        """Удаляем область из ini файла"""
        if type(strHeader) in (tuple, list):
            # Формат plasma
            delStrHeader = list(strHeader[:])
            delStrHeader[-1] = "!%s" % delStrHeader[-1]
        else:
            # Формат samba
            delStrHeader = "!%s" % strHeader
        dictVar = {"del": "del"}
        res = self.setVar(delStrHeader, dictVar)
        return res

    def getVar(self, strHeader, nameVar, checkExistVar=False):
        """Получаем значение переменной из ini-файла"""
        textIni = self.openIniFile()
        if self.FD:
            self.FD.close()
            self.FD = None
        nameFomat = self.checkIniFile(textIni)
        if not nameFomat:
            return False
        formatPlasma = False
        if type(strHeader) in (tuple, list):
            # формат plasma
            classObj = self.formatFactory.getClassObj("plasma")
            formatPlasma = True
        else:
            if nameFomat == "plasma":
                self.setError(_("Trying to fetch a variable of 'samba' "
                                "format from file %s ('plasma' format)") \
                              % self.iniFile)
                return False
            # формат samba
            classObj = self.formatFactory.getClassObj("samba")
        # создаем объект и записываем в него содержимое ini-файла
        objIni = classObj(textIni)
        # получаем ноду body
        xmlBody = objIni.docObj.getNodeBody()
        flagFound, xmlBody = self.getLastNode(objIni, xmlBody, strHeader,
                                              formatPlasma)
        if flagFound and xmlBody:
            if formatPlasma:
                strHeader = strHeader[-1]
            # находим в области переменную
            res = objIni.docObj.getAreaFieldValues(strHeader, nameVar, xmlBody)
        else:
            res = False
        if checkExistVar:
            if res is False:
                return False, ""
            else:
                return True, res
        else:
            if res is False:
                return ""
            else:
                return res

    def getLastNode(self, objIni, xmlBody, strHeader, formatPlasma):
        """Ищет область в XML в которой область с переменными"""
        flagFound = True
        if not strHeader:
            flagFound = False
            return flagFound, xmlBody
        lenStrHeader = len(strHeader)
        if formatPlasma and lenStrHeader > 0:
            xmlAreas = [xmlBody]
            for i in xrange(lenStrHeader - 1):
                flagFound = False
                for xmlArea in xmlAreas:
                    xmlAreas = objIni.docObj.getArea(strHeader[i], xmlArea)
                    if xmlAreas:
                        flagFound = True
                        break
            if xmlAreas:
                xmlBody = xmlAreas[0]
        return flagFound, xmlBody

    def getAreaVars(self, strHeader):
        """Получаем все переменнные области из ini-файла"""
        textIni = self.openIniFile()
        if self.FD:
            self.FD.close()
            self.FD = None
        nameFomat = self.checkIniFile(textIni)
        if not nameFomat:
            return False
        formatPlasma = False
        if type(strHeader) in (tuple, list):
            # формат plasma
            classObj = self.formatFactory.getClassObj("plasma")
            formatPlasma = True
        else:
            if nameFomat == "plasma":
                self.setError(_("Trying to fetch a variable of 'samba' "
                                "format from file %s ('plasma' format)") \
                              % self.iniFile)
                return False
            # формат samba
            classObj = self.formatFactory.getClassObj("samba")
        # создаем объект типа samba и записываем в него содержимое ini-файла
        objIni = classObj(textIni)
        # получаем ноду body
        xmlBody = objIni.docObj.getNodeBody()
        flagFound, xmlBody = self.getLastNode(objIni, xmlBody, strHeader,
                                              formatPlasma)
        if flagFound and xmlBody:
            if formatPlasma:
                strHeader = strHeader[-1]
            # если находим область то выдаем словарем все переменные иначе False
            res = objIni.docObj.getAreaFields(strHeader, xmlBody, allVars=True)
        else:
            res = False
        if res is False:
            return {}
        else:
            return res

    def getAllSectionNames(self):
        """Получаем все имена секций определенных в ini файле

        Если формат ini файла plasma то имя секции -
        имена нескольких секций через запятую
        """
        textIni = self.openIniFile()
        if self.FD:
            self.FD.close()
            self.FD = None
        nameFomat = self.checkIniFile(textIni)
        if not nameFomat:
            return False
        if nameFomat == "samba":
            # создаем объект типа samba и записываем в него содержимое ini-файла
            objIni = self.formatFactory.getClassObj("samba")(textIni)
        elif nameFomat == "plasma":
            # создаем объект типа plasma и записываем в него содержимое
            # ini-файла
            objIni = self.formatFactory.getClassObj("plasma")(textIni)
        else:
            return []
        xmlNodes = objIni.docObj.getAllAreas()
        # Имена секций ini файла
        namesSection = []
        if nameFomat == "plasma":
            for xmlNode in xmlNodes:
                nSect = objIni.docObj.getNameArea(xmlNode)
                if nSect:
                    namesSect = [nSect]
                    parentNode = xmlNode.parentNode
                    while parentNode != objIni.docObj.body:
                        nameSect = objIni.docObj.getNameArea(parentNode)
                        if nameSect:
                            namesSect.append(nameSect)
                            parentNode = parentNode.parentNode
                        else:
                            return []
                    namesSection.append(",".join(reversed(namesSect)))
        elif nameFomat == "samba":
            # получаем ноду body
            for xmlNode in xmlNodes:
                nSect = objIni.docObj.getNameArea(xmlNode)
                if nSect:
                    namesSection.append(nSect)
        return namesSection
