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

# Copyright 2011-2013 Calculate Ltd. 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.


import sys, os
from shutil import copy2
import importlib
from os import path
import time
import re

from calculate.core.datavars import DataVarsCore,DataVars
from calculate.core.server.gen_pid import (search_worked_process, get_pid_info,
    ProcessMode)
from calculate.lib.datavars import Variable
from calculate.lib.cl_log import log
from calculate.lib import datavars
from calculate.lib.utils.files import (runOsCommand,scanDirectory,
                                       pathJoin,readFile,process)
from calculate.lib.utils.common import getPasswdUsers,getTupleVersion
from calculate.lib.utils.portage import isPkgInstalled,reVerSplitToPV
from calculate.lib.utils.content import getCfgFiles,PkgContents
from calculate.lib.utils.colortext import get_color_print
import pwd
import glob
from calculate.lib.utils.files import getModeFile
import calculate.lib.cl_template as cl_template

from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_core3',sys.modules[__name__])

__ = getLazyLocalTranslate(_)

class SetupPackageError(Exception):
    """
    Исключение вызванное во время настройки пакета
    """

class UpdateLogger:
    """
    Логгер для обновления настроек системы
    """

    logger = log("apply-templates",
                 filename="/var/log/calculate/update_config.log",
                 formatter="%(asctime)s - %(levelname)s - %(message)s")

    def ERROR(self, *arg, **argv):
        """
        Вывести ошибку в лог и на экран
        """
        self.logger.error(arg[0])
        self.printERROR(*arg, **argv)

    def SUCCESS(self, *arg, **argv):
        """
        Вывести сообщение в лог и на экран
        """
        self.logger.info(arg[0])
        self.printSUCCESS(*arg, **argv)

    def WARNING(self, *arg, **argv):
        """
        Вывести предупреждение в лог и на экран
        """
        self.logger.warn(arg[0])
        self.printWARNING(*arg, **argv)

class ChainProgressTemplate(cl_template.ProgressTemplate):
    """
    Наложение шаблонов с определением перенастройки зависимых пакетов
    """
    def __init__(self,startTask,endTask,*args,**kwargs):
        self.startTask = startTask
        self.endTask = endTask
        cl_template.ProgressTemplate.__init__(self,*args,**kwargs)

    def changeMergePackage(self,packages):
        """
        Изменился настраиваемый пакет (по зависимостям)
        """
        self.endTask()
        packages = filter(isPkgInstalled,
                   packages)
        self.startTask(_("Configuring dependencies: %s")%
                    ",".join(packages))
        return True

class StubVariable(Variable):
    """
    Переменная-заглушка используется при обновлении настроек пакета
    в emerge. Если переменная не найдена, то будет возвращена пустая
    строка.
    """
    value = ""

class UpdateConfigs(UpdateLogger):
    """
    Обновить настройки пакета в пользовательских профилях
    """
    def init(self):
        self.color_print = get_color_print()

    def getXUsers(self):
        """
        Получить пользователей в X сессии
        """
        return list(self.clVars.Get('desktop.cl_desktop_online_user'))+["root"]

    def getConfiguredPasswdUsers(self):
        """
        Получить пользоватлей, которые есть в /etc/passwd (UID>=1000)
        и при этом у них есть настройка профиля (.calculate/ini.env)
        """
        USER,DIR = 0,1
        iniEnv = ".calculate/ini.env"
        return map(lambda x:x[USER], 
               filter(lambda x:path.exists(path.join(x[DIR],iniEnv)), 
               map(lambda x:(x,pwd.getpwnam(x).pw_dir),
               getPasswdUsers())))

    def _setClMergePkg(self,clVars,category,nameProgram):
        """
        Установить переменную cl_merge_pkg в зависимости от category и nameProgram
        """
        # выбрана перенастройка всех пакетов, установленных в системе
        if nameProgram == "all":
            clVars.Set("cl_merge_pkg",
                       map(lambda x:"{CATEGORY}/{PN}".format(**x),
                       filter(None,
                       map(reVerSplitToPV,
                       glob.glob('/var/db/pkg/*/*')))),
                       True)
        else:
            clVars.Set("cl_merge_pkg", ["%s/%s"%(category,nameProgram)], True)
        clVars.Set("cl_merge_set","on",True)

    def updateDesktopConfig(self,nameProgram,version,slot,category,configPath,
                           rootSet,verbose,dispatchConf,templates_locate,
                           ebuildPhase, useClt):
        """
        Настроить пакеты в профилях пользователей
        """
        # настраиватся будут пользователи из активных X сессии
        # и сконфигурированные
        xUsers = filter(lambda x:not "(unknown)" in x,
                 self.getXUsers())
        if not xUsers:
            self.logger.info(_("Package %s") %nameProgram)
            self.logger.warn(_("X session users not found"))
            return True
        self.logger.info(_("Package %s") %nameProgram)
        self.logger.info(_("Updating user configuration files"))
        mergeProgram = "calculate-utilities"
        firstValue = True

        clVars = DataVars()
        try:
            clVars.importData()
            clVars.flIniFile()
            for userName in xUsers:
                clVars.Set("cl_root_path", '/', True)
                clVars.Set("ur_login", userName, True)
                clVars.Set("cl_action", "desktop", True)
                clVars.Set("cl_verbose_set", verbose, True)
                clVars.Set("cl_protect_use_set", "off", True)
                clVars.Set("cl_template_path_use", templates_locate, True)

                self._setClMergePkg(clVars,category,nameProgram)

                clTempl = ChainProgressTemplate(self.startTask,
                                   self.endTask,
                                   self.setProgress,
                                   clVars, cltObj = False,
                                   printSUCCESS=self.printSUCCESS,
                                   printERROR=self.printERROR,
                                   askConfirm=self.askConfirm,
                                   printWARNING=self.printWARNING,
                                   printWarning=False)
                clTempl.onFirstValue = lambda *args: \
                        self.startTask(
                            _("User configuring the {nameProgram} package by "
                              "Calculate Utilities").format(
                              nameProgram=nameProgram))
                clTempl.firstValue = firstValue
                clTempl.applyTemplates()
                firstValue = clTempl.firstValue
        finally:
            clVars.close()
            self.endTask()
        return True


    def updateSystemConfig(self,nameProgram,version,slot,category,configPath,
                           rootSet,verbose,dispatchConf,templates_locate,
                           ebuildPhase, useClt):
        """
        Обновить конфигурационные файлы системы
        """
        self.logger.info(_("Package %s") %nameProgram)
        self.logger.info(_("Updating system cofiguration files"))
        if not os.path.exists(configPath):
            self.ERROR(_("Path '%s' does not exist")%configPath)
            return False

        clTempl = False
        mergeProgram = "calculate-utilities"
        clVars = DataVars()
        try:
            clVars.importData()
            clVars.flIniFile()
            clVars.Set("cl_root_path", configPath, True)

            # если конфигурирование пакета происходит не в корне
            if rootSet:
                # остальные пакеты настраиваются в корень
                clVars.Set("cl_root_path_next", '/', True)

            self._setClMergePkg(clVars,category,nameProgram)
            clVars.Set("cl_action", 'merge', True)
            clVars.Set("cl_verbose_set", verbose, True)
            clVars.Set("cl_dispatch_conf", dispatchConf, True)
            clVars.Set("cl_template_path_use", templates_locate, True)
            useClt = useClt in (True,"on")

            dictVer = {slot:version}
            cl_template.templateFunction.installProg.update(
                        {"%s/%s"%(category,nameProgram):dictVer,
                            "%s"%(nameProgram):dictVer})

            configFiles = []
            # используем объект шаблонов
            # с clt шаблонами, clt фильтром, без использования postDispatchConf
            clTempl = ChainProgressTemplate( self.startTask,
                               self.endTask,
                               self.setProgress,
                               clVars, cltObj=useClt,
                               cltFilter = True,
                               printSUCCESS=self.printSUCCESS,
                               printERROR=self.printERROR,
                               printWARNING=self.printWARNING,
                               askConfirm=self.askConfirm,
                               dispatchConf= self.dispatchConf
                                    if not ebuildPhase and \
                                        self.isInteractive() else None,
                               printWarning=False)
            # выводим сообщение о настройке пакета только если действительно
            # менялись файлы
            clTempl.onFirstValue = lambda *args: \
                self.startTask(_("System configuring for {nameProgram} "
                                 "package by Calculate Utilities").format(
                                 nameProgram=nameProgram))
            clTempl.applyTemplates()
        finally:
            clVars.close()
            self.endTask()
        return True

    def patchPackage(self,configPath,nameProgram):
        """
        Наложить патчи на пакет
        """
        self.clVars.Set("cl_root_path", configPath, True)
        clTempl = ChainProgressTemplate( self.startTask,
                           self.endTask,
                           self.setProgress,
                           self.clVars, cltObj = False,
                           printSUCCESS=self.printSUCCESS,
                           printERROR=self.printERROR,
                           askConfirm=self.askConfirm,
                           printWARNING=self.printWARNING,
                           printWarning=False)
        clTempl.onFirstValue = lambda *args: \
                self.startTask(
                    _("Using patches for the {nameProgram} package by "
                      "Calculate Utilities").format(
                      nameProgram=nameProgram),
                      progress=True)
        clTempl.applyTemplates()
        return True

    def checkRunning(self):
        """
        Проверить наличие запущенных процессов в cl-core
        """
        from calculate.core.server.loaded_methods import LoadedMethods
        cur_pid = os.getpid()
        pid_list = [pid for pid in get_pid_info(self.clVars)
                    if (pid.get("mode",'') == ProcessMode.CoreDaemon and
                        pid.get("os_pid",'') != cur_pid)]
        if pid_list:
            _print = self.color_print
            method_names = {value[0]:value[2] for key,value in 
                            LoadedMethods.conMethods.items()}
            self.printSUCCESS(
                _("Calculate core is executing the following tasks"))
            mult = _print.bold("*")
            for pid in pid_list:
                name = pid['name']
                method_name = method_names.get(name, name)
                self.printDefault(
                    "&nbsp;{mult} {title} ({name})".format(mult=mult,
                        title = method_name, name=name))
            answer = self.askConfirm(
                _("Would you like to terminate these tasks?"), "no")
            if answer == "no":
                raise KeyboardInterrupt
        return True

    def restartService(self, service_name):
        """
        Перезапустить указанный сервис
        """
        import time
        time.sleep(1)
        os.system('/etc/init.d/%s restart &>/dev/null &'%service_name)
        return True

class PackageUpdater(UpdateConfigs): #,updateUserConfigs):
    def __init__(self):
        self.clVars = None
        self.clTempl = None

    def initVars(self,datavars=None):
        """Primary variables initialization"""
        if not datavars:
            self.clVars = DataVars()
            self.clVars.importData()
            self.clVars.flIniFile()
        else:
            self.clVars = datavars

    def closeClTemplate(self):
        if self.clTempl:
            if self.clTempl.cltObj:
                self.clTempl.cltObj.closeFiles()
            self.clTempl.closeFiles()
            self.clTempl = None

    def postAction(self,error):
        """
        Post action for umount user res and write status_sync=error
        """
        self.closeClTemplate()
        return True


    def updateConfig(self):
        if not os.path.exists('/etc/calculate/calculate.env') and \
            not (dv.Get('cl_core_pkg_name') in ('calculate-utilities',
                                                'calculate-utils',
                                                'calculate-install') and
                 getTupleVersion(dv.Get('cl_core_pkg_version')) >=
                 getTupleVersion("3.1.0_alpha4")):
            self.printWARNING(
                _("Configuration skipping until "
                  "calculate-utils are updated"))
            return True
        self.initVars(dv)
        clVars = DataVars()
        clVars.importData()
        clVars.flIniFile()
        slot = dv.Get('cl_core_pkg_slot')
        version = dv.Get('cl_core_pkg_version')
        category = dv.Get('cl_core_pkg_category')
        nameProgram = dv.Get('cl_core_pkg_name')
        # define that category/nameProgram installed
        if dv.Get('cl_ebuild_phase') in ('prerm','postrm'):
            version = ""
        dictVer = {slot:version}
        cl_template.templateFunction.installProg.update(\
                    {"%s/%s"%(category,nameProgram):dictVer,
                        "%s"%(nameProgram):dictVer})
        try:
            if os.environ.get("EBUILD_PHASE",""):
                clVars.raiseVariableNotFound = \
                    lambda name,*args,**kwargs: StubVariable(*args,
                                                             **kwargs)
                clVars.raiseModuleError = \
                    lambda name,*args,**kwargs: False
            if dv.Get('cl_core_pkg_system_set') == 'on':
                self.updateSystemConfig(dv,clVars)
            if dv.Get('cl_core_pkg_desktop_set') == 'on' and \
                not dv.Get('cl_ebuild_phase') in ("preinst","prerm"):
                self.updateDesktopConfig(dv,clVars)
        finally:
            if clVars:
                clVars.close()
        return True
