# fsminsize.py
# Filesystem size gathering classes.
#
# 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): Anne Mulhern <amulhern@redhat.com>

import abc

from six import add_metaclass

from ..errors import FSError
from .. import util
from ..size import Size

from . import availability
from . import fstask
from . import task


@add_metaclass(abc.ABCMeta)
class FSMinSize(task.BasicApplication, fstask.FSTask):

    """ An abstract class that represents min size information extraction. """

    description = "minimum filesystem size"

    options = abc.abstractproperty(doc="Options for use with app.")

    def _resize_command(self):
        return [str(self.ext)] + self.options + [self.fs.device]

    def _get_resize_info(self):
        """ Get info from fsresize program.

            :rtype: str
            :returns: output returned by fsresize program
        """
        error_msg = None
        try:
            (rc, out) = util.run_program_and_capture_output(self._resize_command())
            if rc:
                error_msg = "failed to gather info from resize program: %d" % rc
        except OSError as e:
            error_msg = "failed to gather info from resize program: %s" % e

        if error_msg:
            raise FSError(error_msg)
        return out

    @abc.abstractmethod
    def do_task(self):  # pylint: disable=arguments-differ
        """ Returns the minimum size for this filesystem object.

            :rtype: :class:`~.size.Size`
            :returns: the minimum size
            :raises FSError: if filesystem can not be obtained
        """
        raise NotImplementedError()


class Ext2FSMinSize(FSMinSize):

    ext = availability.RESIZE2FS_APP
    options = ["-P"]

    @property
    def depends_on(self):
        return [self.fs._info]

    def _extract_block_size(self):
        """ Extract block size from filesystem info.

            :returns: block size of fileystem or None
            :rtype: :class:`~.size.Size` or NoneType
        """
        if self.fs._current_info is None:
            return None

        block_size = None
        for line in (l.strip() for l in self.fs._current_info.splitlines() if l.startswith("Block size:")):
            try:
                block_size = int(line.split(" ")[-1])
                break
            except ValueError:
                continue

        return Size(block_size) if block_size else None

    def _extract_num_blocks(self, info):
        """ Extract the number of blocks from the resizefs info.

           :returns: the number of blocks or None
           :rtype: int or NoneType
        """
        num_blocks = None
        for line in info.splitlines():
            (text, _sep, value) = line.partition(":")
            if "minimum size of the filesystem" not in text:
                continue

            try:
                num_blocks = int(value.strip())
                break
            except ValueError:
                break

        return num_blocks

    def do_task(self):  # pylint: disable=arguments-differ
        error_msgs = self.availability_errors
        if error_msgs:
            raise FSError("\n".join(error_msgs))

        block_size = self._extract_block_size()
        if block_size is None:
            raise FSError("failed to get block size for %s filesystem on %s" % (self.fs.mount_type, self.fs.device))

        resize_info = self._get_resize_info()
        num_blocks = self._extract_num_blocks(resize_info)
        if num_blocks is None:
            raise FSError("failed to get minimum block number for %s filesystem on %s" % (self.fs.mount_type, self.fs.device))

        return block_size * num_blocks


class NTFSMinSize(FSMinSize):

    ext = availability.NTFSRESIZE_APP
    options = ["-m"]

    def _extract_min_size(self, info):
        """ Extract the minimum size from the resizefs info.

            :param str info: info obtained from resizefs prog
            :rtype: :class:`~.size.Size` or NoneType
            :returns: the minimum size, or None
        """
        min_size = None
        for line in info.splitlines():
            (text, _sep, value) = line.partition(":")
            if "Minsize" not in text:
                continue

            try:
                min_size = Size("%d MB" % int(value.strip()))
            except ValueError:
                break

        return min_size

    def do_task(self):  # pylint: disable=arguments-differ
        error_msgs = self.availability_errors
        if error_msgs:
            raise FSError("\n".join(error_msgs))

        resize_info = self._get_resize_info()
        min_size = self._extract_min_size(resize_info)
        if min_size is None:
            raise FSError("Unable to discover minimum size of filesystem on %s" % self.fs.device)
        return min_size


class UnimplementedFSMinSize(fstask.UnimplementedFSTask):
    pass
