#
#  Copyright (C) 2018 Codethink Limited
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
#
#  This library 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
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
#  Authors:
#        Jonathan Maw <jonathan.maw@codethink.co.uk>

"""
filter - Extract a subset of files from another element
=======================================================
This filters another element by producing an output that is a subset of
the filtered element.

To specify the element to filter, specify it as the one and only build
dependency to filter. See :ref:`Dependencies <format_dependencies>`
for what dependencies are and how to specify them.

Dependencies aside from the filtered element may be specified, but
they must be runtime dependencies only. This can be useful to propagate
runtime dependencies forward from this filter element onto its reverse
dependencies.

When workspaces are opened, closed or reset on this element, or this
element is tracked, instead of erroring due to a lack of sources, this
element will transparently pass on the command to its sole build-dependency.

The default configuration and possible options are as such:
  .. literalinclude:: ../../../buildstream/plugins/elements/filter.yaml
     :language: yaml
"""

from buildstream import Element, ElementError, Scope


class FilterElement(Element):
    # pylint: disable=attribute-defined-outside-init

    BST_ARTIFACT_VERSION = 1

    # The filter element's output is its dependencies, so
    # we must rebuild if the dependencies change even when
    # not in strict build plans.
    BST_STRICT_REBUILD = True

    # This element ignores sources, so we should forbid them from being
    # added, to reduce the potential for confusion
    BST_FORBID_SOURCES = True

    def configure(self, node):
        self.node_validate(node, [
            'include', 'exclude', 'include-orphans'
        ])

        self.include = self.node_get_member(node, list, 'include')
        self.exclude = self.node_get_member(node, list, 'exclude')
        self.include_orphans = self.node_get_member(node, bool, 'include-orphans')

    def preflight(self):
        # Exactly one build-depend is permitted
        build_deps = list(self.dependencies(Scope.BUILD, recurse=False))
        if len(build_deps) != 1:
            detail = "Full list of build-depends:\n"
            deps_list = "  \n".join([x.name for x in build_deps])
            detail += deps_list
            raise ElementError("{}: {} element must have exactly 1 build-dependency, actually have {}"
                               .format(self, type(self).__name__, len(build_deps)),
                               detail=detail, reason="filter-bdepend-wrong-count")

        # That build-depend must not also be a runtime-depend
        runtime_deps = list(self.dependencies(Scope.RUN, recurse=False))
        if build_deps[0] in runtime_deps:
            detail = "Full list of runtime depends:\n"
            deps_list = "  \n".join([x.name for x in runtime_deps])
            detail += deps_list
            raise ElementError("{}: {} element's build dependency must not also be a runtime dependency"
                               .format(self, type(self).__name__),
                               detail=detail, reason="filter-bdepend-also-rdepend")

    def get_unique_key(self):
        key = {
            'include': sorted(self.include),
            'exclude': sorted(self.exclude),
            'orphans': self.include_orphans,
        }
        return key

    def configure_sandbox(self, sandbox):
        pass

    def stage(self, sandbox):
        pass

    def assemble(self, sandbox):
        with self.timed_activity("Staging artifact", silent_nested=True):
            for dep in self.dependencies(Scope.BUILD, recurse=False):
                dep.stage_artifact(sandbox, include=self.include,
                                   exclude=self.exclude, orphans=self.include_orphans)
        return ""

    def _get_source_element(self):
        # Filter elements act as proxies for their sole build-dependency
        build_deps = list(self.dependencies(Scope.BUILD, recurse=False))
        assert len(build_deps) == 1
        output_elm = build_deps[0]._get_source_element()
        return output_elm


def setup():
    return FilterElement
