# Containers for working with kickstart elements
#
# Copyright (C) 2017  Red Hat, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>.
#
from enum import Enum


class KickstartElement(object):
    """Stores element parsed from kickstart with reference to file.

    Element can be a command, a section or an addon. Addon is not considered
    as a section. The type is infered from args argument.
    """

    class KickstartElementType(Enum):
        COMMAND = "command"
        SECTION = "section"
        ADDON = "addon"

    def __init__(self, args, lines, lineno, filename):
        """Construct the element from data obtained by a KickstartParser.

        :param args: tokens parsed from a line by shlex in KickstartParser.
                     - for command the whole command
                     - for section its header
                     - for addon its header
        :type args: list(str)
        :param lines: lines parsed by KickstarParser
                     - for command the whole command
                     - for section its body
                     - for addons its body
        :type lines: list(str)
        :param lineno: reference to command, section header or addon header line
                       in kickstart file
        :type lineno: int
        :param filename: reference to a kickstart file
        :type filename: str
        """
        self._type = self._get_type(args)
        self._args = args
        self._lines = lines
        self._lineno = lineno
        self._filename = filename

        self._name = self._get_name(args)
        self._content = self._get_content(args, lines)

    @property
    def name(self):
        """Name of the element."""
        return self._name

    @property
    def content(self):
        """Full kickstart content of the element."""
        return self._content

    @property
    def lineno(self):
        """Kickstart file line number."""
        return self._lineno

    @property
    def filename(self):
        """Kickstart file name."""
        return self._filename

    @property
    def number_of_lines(self):
        """Returns number of kickstart lines of the element."""
        return self.content.count('\n')

    def is_command(self):
        """The element is a command."""
        return self._type == self.KickstartElementType.COMMAND

    def is_section(self):
        """The element is a section.

        Addon is not considered a section.
        """
        return self._type == self.KickstartElementType.SECTION

    def is_addon(self):
        """The element is an addon."""
        return self._type == self.KickstartElementType.ADDON

    def __repr__(self):
        return "KickstartElement(args={}, lines={}, lineno={}, filename={})".format(
            self._args, self._lines, self._lineno, self._filename)

    def __str__(self):
        return self.content

    def _get_type(self, args):
        e_type = self.KickstartElementType.COMMAND
        if args[0] == "%addon":
            e_type = self.KickstartElementType.ADDON
        elif args[0].startswith("%"):
            e_type = self.KickstartElementType.SECTION
        return e_type

    def _get_name(self, args):
        name = ""
        if self._type == self.KickstartElementType.COMMAND:
            name = args[0]
        elif self._type == self.KickstartElementType.ADDON:
            try:
                name = args[1]
            except IndexError:
                name = ""
        elif self._type == self.KickstartElementType.SECTION:
            name = args[0][1:]
        return name

    def _get_content(self, args, lines):
        content = ""
        if self._type == self.KickstartElementType.COMMAND:
            content = lines[0]
        elif self._type in (self.KickstartElementType.ADDON,
                            self.KickstartElementType.SECTION):
            body = "".join(self._lines)
            if body:
                content = "{}\n{}%end\n".format(" ".join(self._args), body)
            else:
                content = "{}\n%end\n".format(" ".join(self._args))
        return content


class KickstartElements(object):
    """Container for storing and filtering KickstartElement objects

    Preserves order of added elements.
    """

    def __init__(self):
        self._elements = []

    def append(self, element):
        """Appends KickstartElement to the container.

        :param element: element object to be appended to the container
        :type name: KickstartElement
        """
        self._elements.append(element)

    @property
    def all_elements(self):
        """List of all elements in the container.

        :return: list of all elements in the container
        :rtype: list(KickstartElement)
        """

        return list(self._elements)

    def get_elements(self, commands=None, sections=None, addons=None):
        """Returns selected elements.

        :param commands: names of commands to be returned
        :type commands: list
        :param sections: names of sections to be returned
        :type sections: list
        :param addons: names of addons to be returned
        :type addons: list

        :return: list of filtered elements
        :rtype: list(KickstartElement)
        """

        filtered_elements = []
        for element in self._elements:
            if element.is_command():
                if commands and element.name in commands:
                    filtered_elements.append(element)
            elif element.is_addon():
                if addons and element.name in addons:
                    filtered_elements.append(element)
            elif element.is_section():
                if sections and element.name in sections:
                    filtered_elements.append(element)
        return filtered_elements

    @staticmethod
    def get_kickstart_from_elements(elements=None):
        """Returns kickstart generated from elements.

        :param elements: list of kickstart elements for generated kickstart
        :type elements: list(KickstartElement)

        :return: kickstart
        :rtype: str
        """
        return "".join(element.content for element in elements)

    def __str__(self):
        return str(self._elements)

    @staticmethod
    def get_references_from_elements(elements=None):
        """Returns elements' references to kickstart file.

        :param elements: list of kickstart elements to get references from
        :type elements: list(KickstartElement)

        :return: list of (lineno, file name) element references indexed by
                 line in kickstart given by the elements
        :rtype: list((int, str))
        """
        refs = [(0, "")]
        for element in elements:
            element_refs = element.number_of_lines * [(element.lineno, element.filename)]
            refs.extend(element_refs)
        return refs


class TrackedKickstartElements(KickstartElements):
    """Container for kickstart elements with tracking."""
    def __init__(self):
        super().__init__()
        self._processed_elements = set()

    def get_and_process_elements(self, commands=None, sections=None, addons=None):
        """Returns selected elements and marks them as processed.

        :param commands: names of commands to be returned
        :type commands: list
        :param sections: names of sections to be returned
        :type sections: list
        :param addons: names of addons to be returned
        :type addons: list

        :return: list of filtered elements
        :rtype: list(KickstartElement)
        """
        elements = self.get_elements(commands, sections, addons)
        self._processed_elements.update(elements)
        return elements

    @property
    def unprocessed_elements(self):
        """List of all elements not tracked as processed.

        :return: list of all elements not tracked as processed
        :rtype: list(KickstartElement)
        """
        return [element for element in self._elements
                if element not in self._processed_elements]
