# -*- 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 random import choice
import os
import re
import math
from os import path
from calculate.lib.utils.dracut import Dracut
from calculate.lib import cl_overriding
import sys
import getpass
from types import StringType
import string
import pwd

from calculate.lib.cl_lang import setLocalTranslate

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


class CommonError(Exception):
    pass


class _error(object):
    # Здесь ошибки, если они есть
    error = []

    def getError(self):
        """Выдать ошибки"""
        if not self.error:
            return False
        error = ""
        for e in self.error:
            error += e + "\n"
        return error

    def setError(self, error):
        """Установка ошибки"""
        self.error.append(error)
        return True

    def clearErrors(self):
        while self.error:
            self.error.pop()


class _warning(object):
    # Здесь предупреждения
    warning = []

    def getWarning(self):
        """Выдать ошибки"""
        if not self.warning:
            return False
        warning = ""
        for w in self.warning:
            warning += w + "\n"
        return warning

    def setWarning(self, warning):
        """Установка ошибки"""
        self.warning.append(warning)
        return True

    def clearWarnings(self):
        while self.warning:
            self.warning.pop()


def genpassword(passlen=9, chars=string.ascii_letters + string.digits):
    """Return random charset specified lenght (passlen)"""
    return ''.join(map(lambda x: choice(chars),
                       xrange(0, passlen)))


def getpathenv():
    """Return path for run utilities"""
    bindir = set(filter(path.exists,
                        ['/sbin', '/bin', '/usr/sbin', '/usr/bin']))
    env = os.environ
    envPath = set(env.get('PATH', '').split(":")) | bindir
    return ":".join(envPath)


from files import process, listDirectory


def getUserPassword(flag="dialog", pwDialog=False):
    """Получить пароль у пользователя

    flag - опция "dalog" или "stdin" - откуда получаем пароль
    pwDialog  - структура для вывода приглашения в режиме диалога
    """
    if flag == "dialog":
        if not pwDialog:
            pwDialog = [_("New password"), _("Retype the new password")]
        pwdA = getpass.getpass(pwDialog[0] + _(":"))
        pwdB = getpass.getpass(pwDialog[1] + _(":"))
    elif flag == "stdin":
        pwdA = sys.stdin.readline().rstrip()
        pwdB = sys.stdin.readline().rstrip()
    else:
        cl_overriding.printERROR(_("ERROR in function getUserPassword, "
                                   "incorrect option 'flag=%s'") % flag)
        return False
    if not pwdA or not (pwdA == pwdB):
        cl_overriding.printERROR(_("ERROR") + ": " +
                                 _("wrong password") + ": " + _("try again"))
        return False
    userPwd = pwdA
    return userPwd


def cmpVersion(v1, v2):
    """Compare versions specified by tuple or string"""
    if isinstance(v1, StringType):
        v1 = getTupleVersion(v1)
    if isinstance(v2, StringType):
        v2 = getTupleVersion(v2)
    return cmp((v1[0] + [0, ] * (len(v2[0]) - len(v1[0])), v1[1]),
               (v2[0] + [0, ] * (len(v1[0]) - len(v2[0])), v2[1]))


def getTupleVersion(ver):
    """Get version specified by string as list:
    Example:
    2.6.30          [(2,6,30),('r',0)]
    2.6.31-r1       [(2,6,31),('r',1)]
    """
    suffix_value = {"pre": -2, "p": 0, "alpha": -4, "beta": -3,
                    "rc": -1}

    def toTuple(v):
        return map(lambda x: suffix_value[x] if x in suffix_value else x,
                   map(lambda x: int(x) if x.isdigit() else x,
                       re.findall("r\d+$|\d+|[a-zA-Z+]+",
                                  v.replace('-SNAPSHOT', ''))))

    vers, revision = re.search("(^.*?)(-r\d+)?$", ver, re.S).groups()
    vers = toTuple(vers)
    vers.extend([0] * (10 - len(vers)))
    revision = toTuple(revision or "r0")
    return [vers, revision]


def appendProgramToEnvFile(nameProg, objVar):
    """Append name program to variable cl-merges and

    save /etc/calculate/calculate.env """
    if not objVar.AppendToList("cl_merges", nameProg, force=True):
        return False
    if not objVar.WriteList("cl_merges", force=True):
        return False
    return True


def removeProgramToEnvFile(nameProg, objVar):
    """Remove name program from variable cl-merges and save

    /etc/calculate/calculate.env"""
    if not objVar.RemoveToList("cl_merges", nameProg, force=True):
        return False
    if not objVar.WriteList("cl_merges", force=True):
        return False
    return True


def getInstalledVideo(prefix="/"):
    """Get installed video drivers"""
    x11Drivers = path.join(prefix, "usr/lib/xorg/modules/drivers")
    return map(lambda x: x[:-7],
               filter(lambda x: x.endswith('_drv.so'),
                      listDirectory(x11Drivers)))


def getDistfilesVideo(prefix="/"):
    """Get video drivers from distfiles"""
    distFiles = path.join(prefix, "usr/portage/distfiles")
    driver_map = {'ati': 'fglrx',
                  'amd': 'fglrx',
                  'radeon': 'fglrx',
                  'nvidia-linux': 'nvidia',
                  'fglrx': 'fglrx'}

    def driver_by_fn(fn):
        fn = fn.lower()
        for part in driver_map:
            if part in fn:
                return driver_map[part]
        else:
            return None

    return list(set(
        filter(None,
               map(driver_by_fn,
                   listDirectory(distFiles)))))


def getAvailableVideo(prefix="/"):
    """Get available video drivers (installed and maybe installed)"""
    return list(set(getInstalledVideo(prefix=prefix) +
                    getDistfilesVideo(prefix=prefix)))


def getPasswdUsers(minId=1000, maxId=65000, prefix="/"):
    """
    Get users from passwd from minId to maxId
    """
    retList = []
    fileName = path.join(prefix, "etc/passwd")
    if os.access(fileName, os.R_OK):
        reNumb = re.compile("^\d+$")
        lenData = 7
        userData = filter(lambda x: len(x) == lenData,
                          map(lambda x: x.rstrip().split(":"),
                              open(fileName)))
        userData = filter(
            lambda x: reNumb.match(x[2]) and minId <= int(x[2]) <= maxId,
            userData)
        sortUsers = map(lambda x: x[0], userData)
        sortUsers.sort()
        retList = ["root"] + sortUsers
    return retList


def getSupportArch():
    """Get supported architectures by processor.

    Is support processor x86_64 else only i686.
    """
    if filter(lambda x: x.startswith('flags') and " lm " in x,
              readLinesFile('/proc/cpuinfo')):
        return ['i686', 'x86_64']
    else:
        return ['i686']


class CmdlineParams(object):
    """
    Параметры опции загрузки ядра calculate=
    """
    # названия параметров
    Calculate = "calculate"
    IsoscanFile = Dracut.IsoScanCmdParam
    IOScheduler = "elevator"

    # внутренние параметры calculate
    Locale = "lang"
    Keymap = "keymap"
    Timezone = "timezone"
    Resolution = "resolution"
    Video = "video"
    Composite = "composite"
    Domain = "domain"
    DomainPassword = "domain_pw"


def getValueFromCmdLine(option, num=None):
    """Get value of parameter from boot params
    
    Parameters:
    option      param name
    num         number part of value parameter (, split)
    """
    cmdLine = "/proc/cmdline"
    calculateParam = option
    names = (
        CmdlineParams.Locale,
        CmdlineParams.Keymap,
        CmdlineParams.Timezone,
        CmdlineParams.Resolution,
        CmdlineParams.Video,
        CmdlineParams.Composite,
        CmdlineParams.Domain,
        CmdlineParams.DomainPassword)
    # try get timezone from kernel calculate param
    try:
        if num is None:
            name = None
        elif type(num) == str and not num.isdigit():
            name = num
            num = names.index(name)
        else:
            name = names[int(num)]
        for param in open(cmdLine, "r").read().split(" "):
            parname, op, value = param.partition("=")
            if parname == calculateParam and op == "=":
                # new format
                if name is None:
                    return value.strip()
                if ":" in value:
                    params = dict(
                        map(lambda x: x.partition(':')[0::2],
                            filter(lambda x: x,
                                   value.split(','))))
                    return params.get(name, "").strip()
                # old format
                else:
                    values = value.split(",")
                    if len(values) > num and values[num].strip():
                        return values[num].strip()
    except (IOError, ValueError, IndexError):
        return ""


def getValueFromConfig(config, name):
    """Get value of parameter from bash type file
    
    Parameters:
    config      config file name
    name        param name
    """
    reMatch = re.compile("^%s\s*=\s*(.*?)\s*$" % name, re.I)
    val = None
    try:
        if path.exists(config):
            for line in open(config, "r").readlines():
                match = reMatch.match(line)
                if match:
                    val = match.groups()[0].strip()
                    for quotas in ('"', "'"):
                        if val.startswith(quotas) and val.endswith(quotas):
                            val = val.strip(quotas)
                            break
    except Exception:
        pass
    return val


def getVideoFromXorgLog(prefix="/", available_drivers=()):
    """Get video driver from Xorg log"""
    # Try analize Xorg.{DISPLAY}.log
    display = os.environ.get('DISPLAY', ':0')
    if display and available_drivers:
        reDriver = re.compile('|'.join(map(lambda x: "%s_drv.so" % x,
                                           available_drivers)))
        display_number = re.search(r':(\d+)(\..*)?', display)
        reDriverName = re.compile(r'([^/]+)_drv.so')
        if display_number:
            xorg_log_file = path.join(prefix, 'var/log/Xorg.%s.log' %
                                      display_number.group(1))
            if path.exists(xorg_log_file):
                matchStrs = map(
                    lambda x: x.group(1),
                    filter(lambda x: x, map(
                        reDriverName.search,
                        filter(lambda x: "drv" in x and reDriver.search(x),
                               readLinesFile(xorg_log_file)))))
                if matchStrs:
                    return matchStrs[-1]
    return ""


def getVideoFromXorgConf(prefix="/"):
    """Get video driver from xorg.conf"""
    xorg_conf = path.join(prefix, 'etc/X11/xorg.conf')
    # analize /etc/X11/xorg.conf
    if path.exists(xorg_conf):
        matchSect = re.search(r'Section "Device".*?EndSection',
                              open(xorg_conf).read(), re.S)
        if matchSect:
            resDriver = re.search(r'\n\s*Driver\s*"([^"]+)"',
                                  matchSect.group(0), re.S)
            if resDriver:
                return resDriver.group(1)
    return ""


def getVideoFromCmdLine():
    """Get video driver from cmdline"""
    videoVal = getValueFromCmdLine(CmdlineParams.Calculate,
                                   CmdlineParams.Video)
    videoVal = {'i915': 'intel'}.get(videoVal, videoVal)
    return videoVal


def getVideoFromModules():
    workedModules = map(lambda x: x[0],
                        filter(lambda x: x[1].isdigit() and int(x[1]) > 0,
                               map(lambda x: x.split()[:3:2],
                                   readLinesFile('/proc/modules'))))
    mapModules = {'nouveau': 'nouveau',
                  'radeon': 'radeon',
                  'i915': 'intel',
                  'fglrx': 'fglrx',
                  'nvidia': 'nvidia',
                  # 'amdgpu': 'amdgpu',
                  'via': 'via',
                  'vmware': 'vmware'}
    for module, videodrv in mapModules.items():
        if module in workedModules:
            return videodrv
    else:
        return ""


def getVideoFromVendor(hr_video, available_drivers):
    if "nvidia" in available_drivers:
        defaultNvidia = "nvidia"
    else:
        defaultNvidia = "nv"
    if "fglrx" in available_drivers:
        defaultAti = "fglrx"
    else:
        defaultAti = "radeon"
    defaultDriver = {
        'nvidia': defaultNvidia,
        'ati': defaultAti,
        'intel': 'intel',
        'via': 'via',
        'vmware': 'vmware'}
    if hr_video in defaultDriver and \
                    defaultDriver[hr_video] in available_drivers:
        return defaultDriver[hr_video]
    else:
        return "other"


def getCompositeFromXorgconf(prefix="/"):
    """Get composite value from xorg.conf"""
    xorgConfig = path.join(prefix,
                           "etc/X11/xorg.conf")
    confLines = readLinesFile(xorgConfig)

    flagStartExtensions = False
    lineCompositeTmp = ""
    lineComposite = ""
    for line in confLines:
        line = line.strip()
        if flagStartExtensions:
            if line.startswith('EndSection'):
                lineComposite = lineCompositeTmp
                break
            elif line.startswith('Section'):
                break
            if line.startswith('Option') and '"Composite"' in line:
                lineCompositeTmp = line
        else:
            if line.startswith('Section') and '"Extensions"' in line:
                flagStartExtensions = True
    if lineComposite:
        listOpt = filter(lambda x: x.strip(), lineComposite.split('"'))
        if len(listOpt) == 3:
            ret = listOpt[2].lower()
            if ret in ("on", "true", "yes", "1"):
                return "on"
            elif ret in ("off", "false", "no", "0"):
                return "off"
    return None


def getKernelUid(device):
    """Get Kernel UID by UUID of device"""
    blkidProcess = process('/sbin/blkid', '-c', '/dev/null', '-s', 'UUID',
                           '-o', 'value', device)
    res = blkidProcess.read().strip()
    if res:
        return res[:8]
    else:
        return device.replace("/", "")


def dict_by_columns(i, sep, key, value):
    """
    Получить словарь из файла, где каждая строка разделена sep символом
    на колонки, key - номер колонки ключей, value номер колонки значений
    """
    max_val = max(key, value)
    return dict(
        map(lambda x: (x[key], x[value]),
            filter(lambda x: len(x) >= max_val,
                   map(lambda x: x.split(sep), i))))


from calculate.lib.utils.files import readLinesFile


def getUserGroups(userName, prefix="/"):
    """
    Get user groups from /etc/groups
    """
    return map(lambda x: x[0],
               filter(lambda x: len(x) > 1 and userName in x[1].split(','),
                      map(lambda x: x.split(':')[0::3],
                          readLinesFile(path.join(prefix, 'etc/group')))))


def getUserPrimaryGroup(userName, prefix="/"):
    """
    Получить основную группу пользователя
    """
    passwd = path.join(prefix, 'etc/passwd')
    group = path.join(prefix, 'etc/group')
    sep_symb = ":"
    userGidMap = dict_by_columns(readLinesFile(passwd), sep_symb, 0, 3)
    gidGroupMap = dict_by_columns(readLinesFile(group), sep_symb, 2, 0)
    if userName in userGidMap:
        gid = userGidMap[userName]
        return gidGroupMap.get(gid, None)
    return None


def getGroups(prefix="/"):
    """
    Get groups from etc/group
    """
    return filter(None,
                  map(lambda x: x.split(':')[0],
                      readLinesFile(path.join(prefix, 'etc/group'))))


def getPagesInterval(count, offset, length):
    """
    Получить интервал страниц (например 2 из 5, при (5,5,15)
    """
    rt = max(0, length - offset - count)
    lt = min(length, offset)
    lt = int(math.ceil(float(lt) / count))
    rt = int(math.ceil(float(rt) / count))
    return lt + 1, rt + lt + 1


def mountEcryptfs(userName, userPwd, userDir):
    """
    Подключить ecryptfs

    Args:
    userName: логин пользователя
    userPwd: пароль пользователя
    userDir: домашний каталог пользователя
    """
    from calculate.lib.utils.files import (process, readLinesFile, STDOUT,
                                           isMount)

    if ".Private" in isMount(userDir):
        return True
    # расшифровать passphrase
    ecryptUserName = path.join('/home/.ecryptfs', userName)
    wrappedPassphraseFile = path.join(ecryptUserName,
                                      ".ecryptfs/wrapped-passphrase")
    p = process('/usr/bin/ecryptfs-unwrap-passphrase', wrappedPassphraseFile)
    p.write(userPwd)
    try:
        if p.failed():
            raise Exception
        result = p.readlines()
        if len(result) > 1 and "Passphrase:" in result[0] and result[1]:
            passphrase = result[1]
        else:
            raise Exception
    except Exception:
        raise CommonError(_("Failed to unwrap the passphrase"))

    # добавить passphrase в ключи ядра
    p = process('/usr/bin/ecryptfs-add-passphrase', '--fnek', '-',
                stderr=STDOUT)
    p.write(passphrase)
    if not p.success():
        raise CommonError(p.read() + "Failed to add passphrase")

    # получить сигнатуры шифрования содержимого файлов и имен файлов
    try:
        ecryptfs_sig, ecryptfs_fnek_sig = \
            readLinesFile(path.join(ecryptUserName, ".ecryptfs/Private.sig"))
    except ValueError:
        raise CommonError(_("Failed to parse Private.sig"))

    # подключить шифрованный раздел
    mountProcess = process(
        '/sbin/mount.ecryptfs',
        path.join(ecryptUserName, '.Private'),
        userDir,
        '-o', 'ecryptfs_passthrough=n,' 'ecryptfs_cipher=aes,'
              'ecryptfs_key_bytes=16,' 'no_sig_cache,'
              'ecryptfs_enable_filename_crypto=y,'
              'ecryptfs_sig={ecryptfs_sig},'
              'ecryptfs_fnek_sig={ecryptfs_fnek_sig},'
              'passphrase_passwd_fd=0'.format(
            ecryptfs_sig=ecryptfs_sig,
            ecryptfs_fnek_sig=ecryptfs_fnek_sig), stderr=STDOUT)
    # отправить пароль через stdin
    mountProcess.write("passphrase_passwd=" + userPwd)
    return mountProcess.success()


def isBootstrapDataOnly(directory):
    """
    Каталог содержит только сертификат, созданный командой cl-core
    а так же настроенные симлинки на .face
    """
    userCalculate = path.join(directory, ".calculate")
    return (set(listDirectory(directory)) <= {
        ".calculate",
        ".face",
        ".dmrc",
        ".face.icon"} and
            set(listDirectory(userCalculate)) == {"client_cert"})


def getPortageUidGid():
    """
    Получить uid и gid пользователя portage
    """
    data = pwd.getpwnam('portage')
    if data:
        return data.pw_uid, data.pw_gid
    else:
        return 250, 250
