from abc import ABC, abstractmethod
from wagtail.blocks.migrations.utils import formatted_list_child_generator
from django.utils.deconstruct import deconstructible


class BaseBlockOperation(ABC):
    def __init__(self):
        pass

    @abstractmethod
    def apply(self, block_value):
        pass

    @property
    @abstractmethod
    def operation_name_fragment(self):
        pass


@deconstructible
class RenameStreamChildrenOperation(BaseBlockOperation):
    """Renames all StreamBlock children of the given type

    Note:
        The `block_path_str` when using this operation should point to the parent StreamBlock
        which contains the blocks to be renamed, not the block being renamed.

    Attributes:
        old_name (str): name of the child block type to be renamed
        new_name (str): new name to rename to
    """

    def __init__(self, old_name, new_name):
        super().__init__()
        self.old_name = old_name
        self.new_name = new_name

    def apply(self, block_value):
        mapped_block_value = []
        for child_block in block_value:
            if child_block["type"] == self.old_name:
                mapped_block_value.append({**child_block, "type": self.new_name})
            else:
                mapped_block_value.append(child_block)
        return mapped_block_value

    @property
    def operation_name_fragment(self):
        return f"rename_{self.old_name}_to_{self.new_name}"


@deconstructible
class RenameStructChildrenOperation(BaseBlockOperation):
    """Renames all StructBlock children of the given type

    Note:
        The `block_path_str` when using this operation should point to the parent StructBlock
        which contains the blocks to be renamed, not the block being renamed.

    Attributes:
        old_name (str): name of the child block type to be renamed
        new_name (str): new name to rename to
    """

    def __init__(self, old_name, new_name):
        super().__init__()
        self.old_name = old_name
        self.new_name = new_name

    def apply(self, block_value):
        mapped_block_value = {}
        for child_key, child_value in block_value.items():
            if child_key == self.old_name:
                mapped_block_value[self.new_name] = child_value
            else:
                mapped_block_value[child_key] = child_value
        return mapped_block_value

    @property
    def operation_name_fragment(self):
        return f"rename_{self.old_name}_to_{self.new_name}"


@deconstructible
class RemoveStreamChildrenOperation(BaseBlockOperation):
    """Removes all StreamBlock children of the given type

    Note:
        The `block_path_str` when using this operation should point to the parent StreamBlock
        which contains the blocks to be removed, not the block being removed.

    Attributes:
        name (str): name of the child block type to be removed
    """

    def __init__(self, name):
        super().__init__()
        self.name = name

    def apply(self, block_value):
        return [
            child_block
            for child_block in block_value
            if child_block["type"] != self.name
        ]

    @property
    def operation_name_fragment(self):
        return f"remove_{self.name}"


@deconstructible
class RemoveStructChildrenOperation(BaseBlockOperation):
    """Removes all StructBlock children of the given type

    Note:
        The `block_path_str` when using this operation should point to the parent StructBlock
        which contains the blocks to be removed, not the block being removed.

    Attributes:
        name (str): name of the child block type to be removed
    """

    def __init__(self, name):
        super().__init__()
        self.name = name

    def apply(self, block_value):
        return {
            child_key: child_value
            for child_key, child_value in block_value.items()
            if child_key != self.name
        }

    @property
    def operation_name_fragment(self):
        return f"remove_{self.name}"


class StreamChildrenToListBlockOperation(BaseBlockOperation):
    """Combines StreamBlock children of the given type into a new ListBlock

    Note:
        The `block_path_str` when using this operation should point to the parent StreamBlock
        which contains the blocks to be combined, not the child block itself.

    Attributes:
        block_name (str): name of the child block type to be combined
        list_block_name (str): name of the new ListBlock type
    """

    def __init__(self, block_name, list_block_name):
        super().__init__()
        self.block_name = block_name
        self.list_block_name = list_block_name

    def apply(self, block_value):
        candidate_blocks = []
        mapped_block_value = []
        for child_block in block_value:
            if child_block["type"] == self.block_name:
                candidate_blocks.append(child_block)
            else:
                mapped_block_value.append(child_block)

        list_items = self.map_temp_blocks_to_list_items(candidate_blocks)

        if list_items:
            new_list_block = {"type": self.list_block_name, "value": list_items}
            mapped_block_value.append(new_list_block)

        return mapped_block_value

    def map_temp_blocks_to_list_items(self, blocks):
        list_items = []
        for block in blocks:
            list_items.append({**block, "type": "item"})
        return list_items

    @property
    def operation_name_fragment(self):
        return f"{self.block_name}_to_list_block_{self.list_block_name}"


class StreamChildrenToStreamBlockOperation(BaseBlockOperation):
    """Combines StreamBlock children of the given types into a new StreamBlock

    Note:
        The `block_path_str` when using this operation should point to the parent StreamBlock
        which contains the blocks to be combined, not the child block itself.

    Attributes:
        block_names (:obj:`list` of :obj:`str`): names of the child block types to be combined
        stream_block_name (str): name of the new StreamBlock type
    """

    def __init__(self, block_names, stream_block_name):
        super().__init__()
        self.block_names = block_names
        self.stream_block_name = stream_block_name

    def apply(self, block_value):
        mapped_block_value = []
        stream_value = []

        for child_block in block_value:
            if child_block["type"] in self.block_names:
                stream_value.append(child_block)
            else:
                mapped_block_value.append(child_block)

        if stream_value:
            new_stream_block = {"type": self.stream_block_name, "value": stream_value}
            mapped_block_value.append(new_stream_block)

        return mapped_block_value

    @property
    def operation_name_fragment(self):
        return "{}_to_stream_block".format("_".join(self.block_names))


class AlterBlockValueOperation(BaseBlockOperation):
    """Alters the value of each block to the given value

    Attributes:
        new_value : new value to change to
    """

    def __init__(self, new_value):
        super().__init__()
        self.new_value = new_value

    def apply(self, block_value):
        return self.new_value

    @property
    def operation_name_fragment(self):
        return "alter_block_value"


class StreamChildrenToStructBlockOperation(BaseBlockOperation):
    """Move each StreamBlock child of the given type inside a new StructBlock

    A new StructBlock will be created as a child of the parent StreamBlock for each child block of
    the given type, and then that child block will be moved from the parent StreamBlocks children
    inside the new StructBlock as a child of that StructBlock.

    Example:
        Consider the following StreamField definition::

            mystream = StreamField([("char1", CharBlock()) ...], ...)

        Then the stream data would look like the following::

            [
                ...
                { "type": "char1", "value": "Value1", ... },
                { "type": "char1", "value": "Value2", ... },
                ...
            ]

        And if we define the operation like this::

            StreamChildrenToStructBlockOperation("char1", "struct1")

        Our altered stream data would look like this::

            [
                ...
                { "type": "struct1", "value": { "char1": "Value1" } },
                { "type": "struct1", "value": { "char1": "Value2" } },
                ...
            ]

    Note:
        The `block_path_str` when using this operation should point to the parent StreamBlock
        which contains the blocks to be combined, not the child block itself.

    Note:
        Block ids are not preserved here since the new blocks are structurally different than the
        previous blocks.

    Attributes:
        block_names (str): names of the child block types to be combined
        struct_block_name (str): name of the new StructBlock type
    """

    def __init__(self, block_name, struct_block_name):
        super().__init__()
        self.block_name = block_name
        self.struct_block_name = struct_block_name

    def apply(self, block_value):
        mapped_block_value = []
        for child_block in block_value:
            if child_block["type"] == self.block_name:
                mapped_block_value.append(
                    {
                        **child_block,
                        "type": self.struct_block_name,
                        "value": {self.block_name: child_block["value"]},
                    }
                )
            else:
                mapped_block_value.append(child_block)
        return mapped_block_value

    @property
    def operation_name_fragment(self):
        return f"{self.block_name}_to_struct_block_{self.struct_block_name}"


class ListChildrenToStructBlockOperation(BaseBlockOperation):
    def __init__(self, block_name):
        super().__init__()
        self.block_name = block_name

    def apply(self, block_value):
        mapped_block_value = []

        # In case there is data from the old list format (wagtail < 2.16), we use the generator
        # to convert them into the new list format
        for child_block in formatted_list_child_generator(block_value):
            mapped_block_value.append(
                {**child_block, "value": {self.block_name: child_block["value"]}}
            )
        return mapped_block_value

    @property
    def operation_name_fragment(self):
        return f"list_block_items_to_{self.block_name}"
