#
# Copyright (C) 2018 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.
#
import shlex
import glob

from collections import OrderedDict
from pyanaconda.core.constants import CMDLINE_APPEND, CMDLINE_LIST, CMDLINE_FILES

__all__ = ['KernelArguments', 'kernel_arguments']


# options might have the inst. prefix (used to differentiate
# boot options for the installer from other boot options)
BOOT_ARG_PREFIX = "inst."


class KernelArguments():
    """The kernel arguments.

    Hold boot arguments as an OrderedDict.
    """

    def __init__(self):
        self._data = OrderedDict()
        self._args_with_prefix = set()

    @classmethod
    def from_defaults(cls):
        """Load the default files.

        :return: an instance of KernelArguments
        """
        args = cls()
        args.read(CMDLINE_FILES)
        return args

    @classmethod
    def from_string(cls, cmdline):
        """Load the given cmdline.

        :param cmdline: a string with the kernel cmdline
        :return: an instance of KernelArguments
        """
        args = cls()
        args.read_string(cmdline)
        return args

    def read(self, filenames):
        """Read and parse a file name (or a list of file names).

        Files that can't be read are silently ignored. The names
        can contain \\*, ?, and character ranges expressed with [].

        :param filenames: a file name or a list of file names
        :return: a list of successfully read files
        """
        readfiles = []
        if isinstance(filenames, str):
            filenames = [filenames]

        # Expand any filename globs
        filenames = [f for g in filenames for f in glob.glob(g)]

        for f in filenames:
            try:
                self.read_string(open(f).read())
                readfiles.append(f)
            except OSError:
                continue

        return readfiles

    def read_string(self, cmdline):
        """Read and parse a string.

        :param cmdline: a string with the kernel command line
        """
        cmdline = cmdline.strip()
        # if the BOOT_IMAGE contains a space, pxelinux will strip one of the
        # quotes leaving one at the end that shlex doesn't know what to do
        # with
        (left, middle, right) = cmdline.rpartition("BOOT_IMAGE=")
        if right.count('"') % 2:
            cmdline = left + middle + '"' + right

        # shlex doesn't properly handle \\ (it removes them)
        # which scrambles the spaces used in labels so use underscores
        cmdline = cmdline.replace("\\x20", "_")

        lst = shlex.split(cmdline)

        for i in lst:
            prefix_used = False
            # drop the inst. prefix (if found)
            if i.startswith(BOOT_ARG_PREFIX):
                i = i[len(BOOT_ARG_PREFIX):]
                prefix_used = True

            if "=" in i:
                (key, val) = i.split("=", 1)
            else:
                key = i
                val = None

            if prefix_used:
                self._args_with_prefix.add(key)

            # Some duplicate args create a space separated string
            if key in CMDLINE_APPEND and self._data.get(key, None):
                if val:
                    self._data[key] = self._data[key] + " " + val
            # Some arguments can contain spaces so adding them in one string is not that helpful
            elif key in CMDLINE_LIST:
                if val:
                    if not self._data.get(key, None):
                        self._data[key] = []
                    self._data[key].append(val)
            else:
                self._data[key] = val

    def is_enabled(self, arg):
        """Return boolean value for the given argument.

        Rules:
        - 0, off, not present -> False
        - the rest -> True
        """
        # None is stored when arg is present but had no value when parsing.
        # So the "miss" value must be something else.
        val = self._data.get(arg, False)
        if val in ["0", "off", False]:
            return False
        else:
            return True

    def get(self, arg, default=None):
        """Return the value of the given argument.

        Propagates the call verbatim to the underlying dictionary.
        """
        return self._data.get(arg, default)

    def __contains__(self, arg):
        """Check for presence of an argument.

        Propagates the call verbatim to the underlying dictionary.
        """
        return arg in self._data

    def items(self):
        """Return an iterator over all arguments.

        Propagates the call verbatim to the underlying dictionary.
        """
        return self._data.items()

    def items_raw(self):
        """Return an iterator over all arguments in their raw form (with prefixes).

        TODO: DO NOT USE THIS METHOD! This workaround will be removed
              when lack of 'inst.' prefix is not supported.
        """
        for key, val in self._data.items():
            if key in self._args_with_prefix:
                yield ("{}{}".format(BOOT_ARG_PREFIX, key), val)
                continue

            yield (key, val)


kernel_arguments = KernelArguments.from_defaults()
