# mounts.py
# Active mountpoints cache.
#
# Copyright (C) 2015  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): Vojtech Trefny <vtrefny@redhat.com>
#
from collections import defaultdict
from .udev import resolve_devspec
from . import util
from .devicelibs import btrfs

import logging
log = logging.getLogger("blivet")

import os


class _MountinfoCache(object):

    """ Cache for info from /proc/self/mountinfo. Looks up the root of the
        mount within the filesystem using a pair of mountpoint, mount
        source as keys.

        This is a very simple helper class for MountsCache, and would
        have to be altered for general purpose use.

        Note that only info for fstype btrfs is stored within the cache,
        as MountsCache methods only require information for btrfs.
    """

    def __init__(self):
        self._cache = None

    def _get_cache(self):
        """ Reads lines in /proc/self/mountinfo and builds a table. """
        cache = {}

        with open("/proc/self/mountinfo") as mountinfos:
            for line in mountinfos:
                fields = line.split()
                separator_index = fields.index("-")
                fstype = fields[separator_index + 1]
                if not fstype.startswith("btrfs"):
                    continue

                root = fields[3]
                mountpoint = fields[4]
                # store the canonical device path
                devspec = resolve_devspec(fields[separator_index + 2],
                                          sysname=True)
                cache[(devspec, mountpoint)] = root

        return cache

    def get_root(self, devspec, mountpoint):
        """ Retrieves the root of the mount within the filesystem
            that corresponds to devspec and mountpoint.

            :param str devspec: device specification
            :param str mountpoint: mountpoint
            :rtype: str or NoneType
            :returns: the root of the mount within the filesystem, if available
        """
        if self._cache is None:
            self._cache = self._get_cache()

        # get the canonical device path
        devspec = resolve_devspec(devspec, sysname=True)
        return self._cache.get((devspec, mountpoint))


class MountsCache(object):

    """ Cache object for system mountpoints; checks /proc/mounts and
        /proc/self/mountinfo for up-to-date information.
    """

    def __init__(self):
        self.mounts_hash = 0
        self.mountpoints = defaultdict(list)

    def get_mountpoints(self, devspec, subvolspec=None):
        """ Get mountpoints for selected device

            :param devscpec: device specification, eg. "/dev/vda1"
            :type devspec: str
            :param subvolspec: btrfs subvolume specification, eg. ID or name
            :type subvolspec: object (may be NoneType)
            :returns: list of mountpoints (path)
            :rtype: list of str or empty list

            .. note::
                Devices can be mounted on multiple paths, and paths can have multiple
                devices mounted to them (hiding previous mounts). Callers should take this into account.
        """
        self._cache_check()

        if subvolspec is not None:
            subvolspec = str(subvolspec)

        # devspec == None means "get 'nodev' mount points"
        if devspec not in (None, "tmpfs"):
            if devspec.startswith("/dev"):
                # try to avoid using resolve_devspec if possible
                name = os.path.realpath(devspec).split("/")[-1]
                if (name, subvolspec) in self.mountpoints.keys():
                    return self.mountpoints[(name, subvolspec)]

            # use the canonical device path (if available)
            canon_devspec = resolve_devspec(devspec, sysname=True)
            if canon_devspec is not None:
                devspec = canon_devspec
            else:
                # udev doesn't know about the given device, it can hardly be
                # mounted
                return []

        return self.mountpoints[(devspec, subvolspec)]

    def is_mountpoint(self, path):
        """ Check to see if a path is already mounted

            :param str path: Path to check
        """
        self._cache_check()

        return any(path in p for p in self.mountpoints.values())

    def _get_active_mounts(self):
        """ Get information about mounted devices from /proc/mounts and
            /proc/self/mountinfo

            Refreshes self.mountpoints with current mountpoint information
        """
        self.mountpoints = defaultdict(list)
        mountinfo = _MountinfoCache()

        with open("/proc/mounts") as mounts:
            for line in mounts:
                try:
                    (devspec, mountpoint, fstype, _rest) = line.split(None, 3)
                except ValueError:
                    log.error("failed to parse /proc/mounts line: %s", line)
                    continue

                # use the canonical device path (if available)
                if devspec.startswith("/dev"):
                    devspec = resolve_devspec(devspec, sysname=True) or devspec

                if fstype == "btrfs":
                    root = mountinfo.get_root(devspec, mountpoint)
                    if root is not None:
                        subvolspec = root[1:] or str(btrfs.MAIN_VOLUME_ID)
                        self.mountpoints[(devspec, subvolspec)].append(mountpoint)
                    else:
                        log.error("failed to obtain subvolspec for btrfs device %s at mountpoint %s", devspec, mountpoint)
                else:
                    self.mountpoints[(devspec, None)].append(mountpoint)

    def _cache_check(self):
        """ Computes the SHA256 hash on /proc/mounts and updates the cache on change
        """

        sha256hash = util.sha256_file("/proc/mounts")

        if sha256hash != self.mounts_hash:
            self.mounts_hash = sha256hash
            self._get_active_mounts()


mounts_cache = MountsCache()
