"""
    Code for making a dropout animation for the
    feed forward layers of a neural network. 
"""
from manim import *
import random

from manim_ml.neural_network.layers.feed_forward import FeedForwardLayer
from manim_ml.neural_network.layers.feed_forward_to_feed_forward import (
    FeedForwardToFeedForward,
)


class XMark(VGroup):
    def __init__(self, stroke_width=1.0, color=GRAY):
        super().__init__()
        line_one = Line(
            [-0.1, 0.1, 0],
            [0.1, -0.1, 0],
            stroke_width=1.0,
            stroke_color=color,
            z_index=4,
        )
        self.add(line_one)
        line_two = Line(
            [-0.1, -0.1, 0],
            [0.1, 0.1, 0],
            stroke_width=1.0,
            stroke_color=color,
            z_index=4,
        )
        self.add(line_two)


def get_edges_to_drop_out(layer: FeedForwardToFeedForward, layers_to_nodes_to_drop_out):
    """Returns edges to drop out for a given FeedForwardToFeedForward layer"""
    prev_layer = layer.input_layer
    next_layer = layer.output_layer
    # Get the nodes to dropout in previous layer
    prev_layer_nodes_to_dropout = layers_to_nodes_to_drop_out[prev_layer]
    next_layer_nodes_to_dropout = layers_to_nodes_to_drop_out[next_layer]
    # Compute the edges to dropout
    edges_to_dropout = []
    edge_indices_to_dropout = []
    for edge_index, edge in enumerate(layer.edges):
        prev_node_index = int(edge_index / next_layer.num_nodes)
        next_node_index = edge_index % next_layer.num_nodes
        # Check if the edges should be dropped out
        if (
            prev_node_index in prev_layer_nodes_to_dropout
            or next_node_index in next_layer_nodes_to_dropout
        ):
            edges_to_dropout.append(edge)
            edge_indices_to_dropout.append(edge_index)

    return edges_to_dropout, edge_indices_to_dropout


def make_pre_dropout_animation(
    neural_network,
    layers_to_nodes_to_drop_out,
    dropped_out_color=GRAY,
    dropped_out_opacity=0.2,
):
    """Makes an animation that sets up the NN layer for dropout"""
    animations = []
    # Go through the network and get the FeedForwardLayer instances
    feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardLayer)
    )
    # Go through the network and get the FeedForwardToFeedForwardLayer instances
    feed_forward_to_feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardToFeedForward)
    )
    # Get the edges to drop out
    layers_to_edges_to_dropout = {}
    for layer in feed_forward_to_feed_forward_layers:
        layers_to_edges_to_dropout[layer], _ = get_edges_to_drop_out(
            layer, layers_to_nodes_to_drop_out
        )
    # Dim the colors of the edges
    dim_edge_colors_animations = []
    for layer in layers_to_edges_to_dropout.keys():
        edges_to_drop_out = layers_to_edges_to_dropout[layer]
        # Make color dimming animation
        for edge_index, edge in enumerate(edges_to_drop_out):
            """
            def modify_edge(edge):
                edge.set_stroke_color(dropped_out_color)
                edge.set_stroke_width(0.6)
                edge.set_stroke_opacity(dropped_out_opacity)
                return edge

            dim_edge = ApplyFunction(
                modify_edge,
                edge
            )
            """

            dim_edge_colors_animations.append(FadeOut(edge))
    dim_edge_colors_animation = AnimationGroup(
        *dim_edge_colors_animations, lag_ratio=0.0
    )
    # Dim the colors of the nodes
    dim_nodes_animations = []
    x_marks = []
    for layer in layers_to_nodes_to_drop_out.keys():
        nodes_to_dropout = layers_to_nodes_to_drop_out[layer]
        # Make an X over each node
        for node_index, node in enumerate(layer.node_group):
            if node_index in nodes_to_dropout:
                x_mark = XMark()
                x_marks.append(x_mark)
                x_mark.move_to(node.get_center())
                create_x = Create(x_mark)
                dim_nodes_animations.append(create_x)

    dim_nodes_animation = AnimationGroup(*dim_nodes_animations, lag_ratio=0.0)

    animation_group = AnimationGroup(
        dim_edge_colors_animation,
        dim_nodes_animation,
    )

    return animation_group, x_marks


def make_post_dropout_animation(
    neural_network,
    layers_to_nodes_to_drop_out,
    x_marks,
):
    """Returns the NN to normal after dropout"""
    # Go through the network and get the FeedForwardLayer instances
    feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardLayer)
    )
    # Go through the network and get the FeedForwardToFeedForwardLayer instances
    feed_forward_to_feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardToFeedForward)
    )
    # Get the edges to drop out
    layers_to_edges_to_dropout = {}
    for layer in feed_forward_to_feed_forward_layers:
        layers_to_edges_to_dropout[layer], _ = get_edges_to_drop_out(
            layer, layers_to_nodes_to_drop_out
        )
    # Remove the x marks
    uncreate_animations = []
    for x_mark in x_marks:
        uncreate_x_mark = Uncreate(x_mark)
        uncreate_animations.append(uncreate_x_mark)

    uncreate_x_marks = AnimationGroup(*uncreate_animations, lag_ratio=0.0)
    # Add the edges back
    create_edge_animations = []
    for layer in layers_to_edges_to_dropout.keys():
        edges_to_drop_out = layers_to_edges_to_dropout[layer]
        # Make color dimming animation
        for edge_index, edge in enumerate(edges_to_drop_out):
            edge_copy = edge.copy()
            edges_to_drop_out[edge_index] = edge_copy
            create_edge_animations.append(FadeIn(edge_copy))

    create_edge_animation = AnimationGroup(*create_edge_animations, lag_ratio=0.0)

    return AnimationGroup(uncreate_x_marks, create_edge_animation, lag_ratio=0.0)


def make_forward_pass_with_dropout_animation(
    neural_network,
    layers_to_nodes_to_drop_out,
):
    """Makes forward pass animation with dropout"""
    layer_args = {}
    # Go through the network and get the FeedForwardLayer instances
    feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardLayer)
    )
    # Go through the network and get the FeedForwardToFeedForwardLayer instances
    feed_forward_to_feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardToFeedForward)
    )
    # Iterate through network and get feed forward layers
    for layer in feed_forward_layers:
        layer_args[layer] = {"dropout_node_indices": layers_to_nodes_to_drop_out[layer]}
    for layer in feed_forward_to_feed_forward_layers:
        _, edge_indices = get_edges_to_drop_out(layer, layers_to_nodes_to_drop_out)
        layer_args[layer] = {"edge_indices_to_dropout": edge_indices}

    return neural_network.make_forward_pass_animation(layer_args=layer_args)


def make_neural_network_dropout_animation(
    neural_network, dropout_rate=0.5, do_forward_pass=True, last_layer_stable=False, first_layer_stable=False, seed=None
):
    """
    Makes a dropout animation for a given neural network.

    NOTE Does dropout only on feed forward layers.

    1. Does dropout
    2. If `do_forward_pass` then do forward pass animation
    3. Revert network to pre-dropout appearance
    """
    # Go through the network and get the FeedForwardLayer instances
    if seed is not None:
        random.seed(seed)
    feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardLayer)
    )
    # Go through the network and get the FeedForwardToFeedForwardLayer instances
    feed_forward_to_feed_forward_layers = neural_network.filter_layers(
        lambda layer: isinstance(layer, FeedForwardToFeedForward)
    )
    # Get random nodes to drop out for each FeedForward Layer
    layers_to_nodes_to_drop_out = {}
    for idx, feed_forward_layer in enumerate(feed_forward_layers):
        num_nodes = feed_forward_layer.num_nodes
        nodes_to_drop_out = []
        # Compute random probability that each node is dropped out
        for node_index in range(num_nodes):
            dropout_prob = random.random()
            if last_layer_stable and idx==len(feed_forward_layers)-1:
                    continue
            if first_layer_stable and idx==0:
                continue
            if dropout_prob < dropout_rate:
                nodes_to_drop_out.append(node_index)
        # Add the mapping to the dict
        layers_to_nodes_to_drop_out[feed_forward_layer] = nodes_to_drop_out
    # Make the animation
    pre_dropout_animation, x_marks = make_pre_dropout_animation(
        neural_network, layers_to_nodes_to_drop_out
    )
    if do_forward_pass:
        forward_pass_animation = make_forward_pass_with_dropout_animation(
            neural_network, layers_to_nodes_to_drop_out
        )
    else:
        forward_pass_animation = AnimationGroup()

    post_dropout_animation = make_post_dropout_animation(
        neural_network, layers_to_nodes_to_drop_out, x_marks
    )
    # Combine the animations into one
    return Succession(
        pre_dropout_animation, forward_pass_animation, post_dropout_animation
    )
