"""Build factory instances."""

import collections

from . import declarations, enums, errors, utils

DeclarationWithContext = collections.namedtuple(
    'DeclarationWithContext',
    ['name', 'declaration', 'context'],
)


class DeclarationSet:
    """A set of declarations, including the recursive parameters.

    Attributes:
        declarations (dict(name => declaration)): the top-level declarations
        contexts (dict(name => dict(subfield => value))): the nested parameters related
            to a given top-level declaration

    This object behaves similarly to a dict mapping a top-level declaration name to a
    DeclarationWithContext, containing field name, declaration object and extra context.
    """

    def __init__(self, initial=None):
        self.declarations = {}
        self.contexts = collections.defaultdict(dict)
        self.update(initial or {})

    @classmethod
    def split(cls, entry):
        """Split a declaration name into a (declaration, subpath) tuple.

        Examples:
        >>> DeclarationSet.split('foo__bar')
        ('foo', 'bar')
        >>> DeclarationSet.split('foo')
        ('foo', None)
        >>> DeclarationSet.split('foo__bar__baz')
        ('foo', 'bar__baz')
        """
        if enums.SPLITTER in entry:
            return entry.split(enums.SPLITTER, 1)
        else:
            return (entry, None)

    @classmethod
    def join(cls, root, subkey):
        """Rebuild a full declaration name from its components.

        for every string x, we have `join(split(x)) == x`.
        """
        if subkey is None:
            return root
        return enums.SPLITTER.join((root, subkey))

    def copy(self):
        return self.__class__(self.as_dict())

    def update(self, values):
        """Add new declarations to this set/

        Args:
            values (dict(name, declaration)): the declarations to ingest.
        """
        for k, v in values.items():
            root, sub = self.split(k)
            if sub is None:
                self.declarations[root] = v
            else:
                self.contexts[root][sub] = v

        extra_context_keys = set(self.contexts) - set(self.declarations)
        if extra_context_keys:
            raise errors.InvalidDeclarationError(
                "Received deep context for unknown fields: %r (known=%r)" % (
                    {
                        self.join(root, sub): v
                        for root in extra_context_keys
                        for sub, v in self.contexts[root].items()
                    },
                    sorted(self.declarations),
                )
            )

    def filter(self, entries):
        """Filter a set of declarations: keep only those related to this object.

        This will keep:
        - Declarations that 'override' the current ones
        - Declarations that are parameters to current ones
        """
        return [
            entry for entry in entries
            if self.split(entry)[0] in self.declarations
        ]

    def sorted(self):
        return utils.sort_ordered_objects(
            self.declarations,
            getter=lambda entry: self.declarations[entry],
        )

    def __contains__(self, key):
        return key in self.declarations

    def __getitem__(self, key):
        return DeclarationWithContext(
            name=key,
            declaration=self.declarations[key],
            context=self.contexts[key],
        )

    def __iter__(self):
        return iter(self.declarations)

    def values(self):
        """Retrieve the list of declarations, with their context."""
        for name in self:
            yield self[name]

    def _items(self):
        """Extract a list of (key, value) pairs, suitable for our __init__."""
        for name in self.declarations:
            yield name, self.declarations[name]
            for subkey, value in self.contexts[name].items():
                yield self.join(name, subkey), value

    def as_dict(self):
        """Return a dict() suitable for our __init__."""
        return dict(self._items())

    def __repr__(self):
        return '<DeclarationSet: %r>' % self.as_dict()


def parse_declarations(decls, base_pre=None, base_post=None):
    pre_declarations = base_pre.copy() if base_pre else DeclarationSet()
    post_declarations = base_post.copy() if base_post else DeclarationSet()

    # Inject extra declarations, splitting between known-to-be-post and undetermined
    extra_post = {}
    extra_maybenonpost = {}
    for k, v in decls.items():
        if enums.get_builder_phase(v) == enums.BuilderPhase.POST_INSTANTIATION:
            if k in pre_declarations:
                # Conflict: PostGenerationDeclaration with the same
                # name as a BaseDeclaration
                raise errors.InvalidDeclarationError(
                    "PostGenerationDeclaration %s=%r shadows declaration %r"
                    % (k, v, pre_declarations[k])
                )
            extra_post[k] = v
        elif k in post_declarations:
            # Passing in a scalar value to a PostGenerationDeclaration
            # Set it as `key__`
            magic_key = post_declarations.join(k, '')
            extra_post[magic_key] = v
        elif k in pre_declarations and isinstance(
            pre_declarations[k].declaration, declarations.Transformer
        ):
            extra_maybenonpost[k] = pre_declarations[k].declaration.function(v)
        else:
            extra_maybenonpost[k] = v

    # Start with adding new post-declarations
    post_declarations.update(extra_post)

    # Fill in extra post-declaration context
    extra_pre_declarations = {}
    extra_post_declarations = {}
    post_overrides = post_declarations.filter(extra_maybenonpost)
    for k, v in extra_maybenonpost.items():
        if k in post_overrides: