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

# Copyright 2010-2012 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.


__version__ = "3.0.2"
__app__ = "calculate-lib"

import sys, os
from shutil import copy2
import importlib

from soaplib.serializers.primitive import String, Boolean, Integer
from soaplib.serializers.clazz import Array, ClassSerializer
from soaplib.service import rpc
from calculate.core.server.api_types import ReturnedMessage
from calculate.core.server.api_types import ViewInfo, ViewParams
from calculate.core.server.decorators import Dec
core_method = Dec.core_method

from calculate.core.datavars import DataVarsCore
from calculate.lib.datavars import Variable
from calculate.lib.cl_log import log
from calculate.lib import datavars
from calculate.lib.utils.files import (scanDirectory, pathJoin)
from calculate.lib.utils.common import getPasswdUsers
import pwd
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_core',sys.modules[__name__])

__ = getLazyLocalTranslate(_)

class CoreSetupInfo(ClassSerializer):
    """Parameters for method core setup"""
    cl_core_pkg_name = String
    cl_core_pkg_version = String
    cl_core_pkg_category = String
    cl_core_pkg_path = String
    cl_core_pkg_system_set = Boolean
    cl_core_pkg_desktop_set = Boolean
    cl_core_pkg_root_set = Boolean

    Default = Array(String)
    CheckOnly = Boolean

class shareUpdateConfigs:
    """Common methods"""

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

    def getFlagUpdAndInstPrograms(self,variables):
        """flagUpdate and programs"""
        flagUpdate = variables.Get("cl_autoupdate_set") == "on"
        return flagUpdate, variables.Get("cl_merges")

    def ERROR(self, *arg, **argv):
        """Print Error and log error"""
        # Запись в log ошибки
        self.logger.error(arg[0])
        self.printERROR(*arg, **argv)

    def SUCCESS(self, *arg, **argv):
        """Print and log success"""
        # Запись в log информации
        self.logger.info(arg[0])
        self.printSUCCESS(*arg, **argv)

    def WARNING(self, *arg, **argv):
        """Print and log warning"""
        # Запись в log предупреждения
        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()
        self.startTask(_("Configuring dependent packages: %s")%
                    ",".join(packages),progress=True)
        return True

class StubVariable(Variable):
    value = ""

#class updateUserConfigs(shareUpdateConfigs):
#    """Обновление пользовательских конфигурационных файлов"""
#
#    def getXUsers(self):
#        """Имена пользователей в X сессии"""
#        xSession = 0
#        foundTwoSession = False
#        retCode, resWho = runOsCommand("who")
#        xUsers = []
#        if retCode==0:
#            if resWho:
#                listProcessing = lambda x: (x[0], x[1], x[4])\
#                                 if len(x)==5 else []
#                xUsers = list(set(filter(lambda x: x!="root",
#                              map(lambda x: x[0],
#                                  filter(lambda x: x and\
#                                         (x[2].startswith("(:") or \
#                                          x[1].startswith(":")),
#                                         map(lambda x: listProcessing(\
#                                             filter(lambda y: y, x.split(" "))),
#                                             resWho))))))
#        else:
#            self.printERROR(_("Can not execute 'who'"))
#            return False
#
#        return xUsers
#
#    def getConfiguredPasswdUsers(self):
#        """
#        Get users from passwd and discard users which has not
#        .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 updateConfig(self, nameProgram, category, version):
#        """Обновление конфигурационных файлов у пользователей"""
#        # флаг обновления и программы используемые для наложения шаблонов
#        flagUpdate, mergePrograms = self.getFlagUpdAndInstPrograms()
#        # X session user + configured local users (if user profile was not
#        # configured then there is no need setup package for this user
#        xUsers = filter(lambda x:not "(unknown)" in x,
#                 list((set(self.getXUsers()) | 
#                       set(self.getConfiguredPasswdUsers()))))
#        if not xUsers:
#            self.logger.info(_("Package %s") %nameProgram)
#            self.logger.warn(_("Not found X sessions users"))
#            return True
#        self.logger.info(_("Package %s") %nameProgram)
#        self.logger.info(_("Update desktop configuration files"))
#        if "calculate-desktop" in mergePrograms:
#            mergePrograms = ["calculate-desktop"]
#        else:
#            mergePrograms = []
#        dictPakkages = {}
#        listIndex = []
#        # Добавление условия, что программа category/nameProgram
#        # установлена
#        cl_template.templateFunction.installProg.update(\
#                        {"%s/%s"%(category,nameProgram):[version],
#                         "%s"%(nameProgram):[version]})
#        for mergeProgram in mergePrograms:
#            for userName in xUsers:
#                clVars = DataVarsObject(mergeProgram)
#                if not clVars.findPathVars():
#                    continue
#                clVars.importDataObject()
#                clVars.Set("ur_login", userName, True)
#                clVars.Set("cl_action", "desktop", True)
#                clVars.Set("cl_belong_pkg", nameProgram, True)
#                clTempl = template(clVars, printWarning=False)
#                dirsFiles = clTempl.applyTemplates()
#                if dirsFiles is False:
#                    self.printERROR(\
#                        _("Error using templates for the user %s")\
#                            %userName)
#                    for errMess in clTempl.getError().splitlines():
#                        self.printERROR(errMess)
#                    return False
#                if dirsFiles and dirsFiles[1]:
#                    nameAndVerPkg = clVars.Get("cl_name")+"-"+\
#                                    clVars.Get("cl_ver")
#                    if not nameAndVerPkg in dictPakkages:
#                        listIndex.append(nameAndVerPkg)
#                        dictPakkages[nameAndVerPkg] = []
#                    dictPakkages[nameAndVerPkg].append((userName,
#                        sorted(list(set(dirsFiles[1])))))
#            if dictPakkages:
#                for calcPkg in listIndex:
#                    self.printWARNING(_("Package %s has changed files")\
#                                      %calcPkg+":")
#                    for userName, configFiles in dictPakkages[calcPkg]:
#                        self.printWARNING(" "*2 + _("User %s")%userName + ":")
#                        for nameConfigFile in configFiles:
#                            self.printWARNING(" "*5 + nameConfigFile)
#        if not dictPakkages:
#             self.logger.warn(_("Template not found"))
#        return True

class updateSystemConfigs(shareUpdateConfigs):
    """Update system configs"""

    def isExistsProtectFiles(self, configPath):
        """Есть ли в защищенных директориях конфигурационные файлы"""
        if not "CONFIG_PROTECT" in os.environ:
            self.ERROR(_("Missing environment variable CONFIG_PROTECT"))

            exit(1)
        protectPaths = ["/etc"] + filter(lambda x: x.strip(),
                                      os.environ["CONFIG_PROTECT"].split(" "))
        flagFoundProtect = False
        for pPath in protectPaths:
            fPath = os.path.join(configPath, pPath[1:])
            if os.path.exists(fPath) and os.listdir(fPath):
                flagFoundProtect = True
                break
        if not flagFoundProtect:
            return False
        return True

    def scanProtectDirs(self, configPath,protectPaths):
        configFiles = []
        scanObj = scanDirectory()
        scanObj.processingFile = lambda path,prefix:configFiles.append(path) or\
                                                    True
        configPath = os.path.realpath(configPath)
        for pPath in protectPaths:
            realPath = pathJoin(configPath, pPath)
            if os.path.exists(realPath):
                scanObj.scanningDirectory(realPath)
        configFiles = map(lambda x: x.partition(configPath)[2], configFiles)
        configFiles = map(lambda x: pathJoin('/',x), configFiles)
        return configFiles

    def createDir(self, configPath, dstDir):
        """Create need dirs"""
        if os.path.exists(dstDir):
            return True

        def splPath(path):
            listPath = []
            if path in ("","/"):
                return []
            base, p = os.path.split(path)
            listPath.append(p)
            while(not base in ("","/")):
                base, p = os.path.split(base)
                listPath.append(p)
            listPath.reverse()
            return listPath
        notFoundPaths = []
        path = "/"
        for p in splPath(dstDir):
            path = os.path.join(path,p)
            if not os.path.exists(path):
                notFoundPaths.append(path)
        for mkPath in notFoundPaths:
            srcPath = pathJoin(configPath, mkPath)
            dMode, dUid, dGid = getModeFile(srcPath)
            os.mkdir(mkPath, dMode)
            os.chown(mkPath, dUid, dGid)
        return True

    def copyConfigFiles(self, configPath,configProtect):
        """Копирование конфигурационных файлов"""
        configDstFiles = self.scanProtectDirs(configPath,configProtect)
        if configDstFiles:
            self.logger.warn(_("Replaced files:"))
        for dst in configDstFiles:
            src = pathJoin(configPath, dst)
            if src != dst:
                dstDir = os.path.dirname(dst)
                self.createDir(configPath, dstDir)
                copy2(src, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)
                self.logger.warn(" "*5 + dst)
        return True

    def copyDirOrFile(self, src, dst, configPath):
        if src != dst:
            if os.path.isfile(src):
                dstDir = os.path.dirname(dst)
                self.createDir(configPath, dstDir)
                copy2(src, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)
            elif os.path.isdir(src):
                self.createDir(configPath, dst)
                sMode, sUid, sGid = getModeFile(src)
                os.chown(dst, sUid, sGid)
                os.chmod(dst, sMode)

    def updateConfig(self, variables):
        #nameProgram, category, version, configPath):
        """Update system configs"""
        # get programs and flag update
        nameProgram = variables.Get('cl_core_pkg_name')
        version = variables.Get('cl_core_pkg_version')
        category = variables.Get('cl_core_pkg_category')
        configPath = variables.Get('cl_core_pkg_path')
        #print "DESKTOP",variables.Get('cl_core_pkg_desktop_set')
        #print "SYSTEM",variables.Get('cl_core_pkg_system_set')
        flagUpdate, mergePrograms = self.getFlagUpdAndInstPrograms(variables)
        self.logger.info(_("Package %s") %nameProgram)
        self.logger.info(_("Update system cofiguration files"))
        if not os.path.exists(configPath):
            self.ERROR(_("Path '%s' does not exist")%configPath)
            return False
        dictPakkages = {}
        listIndex = []
        # define that category/nameProgram installed
        cl_template.templateFunction.installProg.update(\
                    {"%s/%s"%(category,nameProgram):[version],
                        "%s"%(nameProgram):[version]})
        clTempl = False
        for mergeProgram in filter(None,mergePrograms):
            clVars = None
            try:
                module_name = '%s.datavars'% (
                        mergeProgram.replace("-","."))
                datavarsmodule = importlib.import_module(module_name)
                module = mergeProgram.rpartition("-")[2].capitalize()
                clVars = getattr(datavarsmodule,
                         "DataVars%s"%module)()
                getattr(clVars,"import%s"%module)()
                if os.environ.get("EBUILD_PHASE",""):
                    clVars.stubVariable = \
                        lambda name,*args,**kwargs: StubVariable(*args,
                                                                 **kwargs)
                self.startTask(_("Configuring the {nameProgram} package by "
                                 "{mergeProgram}").format(
                                    mergeProgram=mergeProgram,
                                    nameProgram=nameProgram),
                               progress=True)
                clVars.Set("cl_root_path", configPath, True)
                if variables.Get('cl_core_pkg_root_set') == 'on':
                    clVars.Set("cl_root_path_next", '/', True)
                clVars.Set("cl_belong_pkg", [nameProgram], True)
                clVars.Set("cl_action", 'merge', True)
                configFiles = []
                nameProg = clVars.Get("cl_name")
                if nameProg == "calculate-install":
                    configFiles = self.scanProtectDirs(configPath,
                                       clVars.Get('cl_config_protect'))
                if configFiles:
                    cltObject = cl_template.templateClt(clVars)
                    cltObject.filterApplyTemplates = configFiles
                    clTempl = ChainProgressTemplate( self.startTask,
                                       self.endTask,
                                       self.setProgress,
                                       clVars, cltObj=cltObject,
                                       printWarning=False)
                else:
                    clTempl = ChainProgressTemplate(self.startTask,
                                       self.endTask,
                                       self.setProgress,
                                       clVars, cltObj=False,
                                       printWarning=False)
                dirsFiles = clTempl.applyTemplates()
                nameAndVerPkg = nameProg + "-"+clVars.Get("cl_ver")
                if dirsFiles is False:
                    self.ERROR(_("Template error in package %s")\
                                    %nameAndVerPkg)
                    self.ERROR(clTempl.getError())
                    return False
                copyFiles = clTempl.autoUpdateFiles
                copyDirs = clTempl.autoUpdateDirs
                allCopyAutoupdateFiles = copyDirs + copyFiles
                for fileOrDir in allCopyAutoupdateFiles:
                    dst = "/" + fileOrDir.partition(configPath)[2]
                    self.copyDirOrFile(fileOrDir, dst, configPath)
                if dirsFiles and dirsFiles[1]:
                    if not nameAndVerPkg in listIndex:
                        listIndex.append(nameAndVerPkg)
                    dictPakkages[nameAndVerPkg] =\
                        sorted(list(set(dirsFiles[1])))
            except ImportError:
                continue
            finally:
                if clVars:
                    clVars.close()
        if dictPakkages:
            for calcPkg in listIndex:
                self.SUCCESS(_("Package %s has changed files")%calcPkg+":")
                for nameF in dictPakkages[calcPkg]:
                    nameFile = nameF.partition(configPath)[2]
                    if nameFile:
                        if nameFile[:1] != "/":
                            nameFile = "/" + nameFile
                    else:
                        nameFile = nameF
                    self.SUCCESS(" "*5 + nameFile)
        else:
            self.logger.warn(_("Template not found"))
        if flagUpdate:
            self.copyConfigFiles(configPath,variables.Get('cl_config_protect'))
        if clTempl and clTempl.getWarning():
            for warn in clTempl.getWarning().split("\n"):
                self.SUCCESS(warn)
        self.printSUCCESS(_("Package configured"))
        return True

class CoreWsdl:
    @rpc(Integer, CoreSetupInfo, _returns = Array(ReturnedMessage))
    @core_method(category=__('Configuration'),title=__('Configure package'),
        image='applications-other',
        gui=True,command='cl-core-setup',
        rights=['configure'])
    def core_setup(self, sid, info):
        try:
            dv = self.get_cache(sid,"core_setup","vars")
            if not dv:
                dv = self.core_setup_vars()
            else:
                dv.processRefresh()
            if info:
                checkonly = info.CheckOnly
            else:
                checkonly = False
            errors = map(lambda x:ReturnedMessage(**x),
                     dv.checkGroups(info,allvars=not checkonly))
            if errors:
                return errors
            if checkonly:
                returnmess = ReturnedMessage(type = '', message = None)
                return [returnmess]
            install_meth = type("CommonCore",(self.Common,
                                updateSystemConfigs, object), {})
            pid = self.startprocess(sid, target=install_meth,
                            method="updateConfig",\
                            method_name="core_setup",
                            args_proc = (dv,))
            returnmess = ReturnedMessage(type = 'pid', message = pid)
            returnmess.type = "pid"
            returnmess.message = pid
            dv = self.clear_cache(sid,"core_setup")
            return [returnmess]
        finally:
            if dv:
                self.set_cache(sid,"core_setup","vars",dv,smart=False)
        return []

    def core_setup_vars(self,dv=None):
        if not dv:
            dv = DataVarsCore()
            dv.importCore()
            dv.flIniFile()
            dv.Set('cl_action','merge',True)
        dv.addGroup(None,
            normal=('cl_core_pkg_name',),
            expert=('cl_core_pkg_category', 'cl_core_pkg_version',
                    'cl_core_pkg_path', 'cl_core_pkg_system_set',
                    'cl_core_pkg_desktop_set', 'cl_core_pkg_root_set'),
            next_label=_("Setup"))
        return dv

    @rpc(Integer, ViewParams,_returns = ViewInfo)
    def core_setup_view (self, sid, params):
        dv = self.get_cache(sid,"core_setup","vars")
        if not dv:
            dv = self.core_setup_vars()
        else:
            dv.processRefresh()
        view = ViewInfo(dv,step=params.step,
                           expert=params.expert,
                           brief=params.brief)
        self.set_cache(sid, 'core_setup', "vars",dv,smart=False)
        return view

