import torch
from typing import Union, Sequence, Dict
from math import sqrt
from nequip.data import AtomicDataDict


class AvgNumNeighborsNorm(torch.nn.Module):
    def __init__(
        self,
        type_names: Sequence[str],
        avg_num_neighbors: Union[float, Dict[str, float]],
    ) -> None:
        """
        Module to normalize features during training using per type edge sum normalization.

        Args:
            type_names (Sequence[str]): list of atom type names
            avg_num_neighbors (float/Dict[str, float]): used to normalize edge sums for better numerics
        """
        super().__init__()
        assert avg_num_neighbors is not None, "avg_num_neighbors must be specified"

        self.in_field = self.out_field = AtomicDataDict.NODE_FEATURES_KEY
        self.norm_key = AtomicDataDict.FEATURE_NORM_FACTOR_KEY

        # Put avg_num_neighbors in a list (global or per type)
        if isinstance(avg_num_neighbors, (float, int)):
            avg_num_neighbors = [avg_num_neighbors]
        elif isinstance(avg_num_neighbors, dict):
            assert set(type_names) == set(avg_num_neighbors.keys())
            avg_num_neighbors = [avg_num_neighbors[k] for k in type_names]
        else:
            raise RuntimeError(
                "Unrecognized format for `avg_num_neighbors`, only floats or dicts allowed."
            )
        assert isinstance(avg_num_neighbors, list)

        # Tensorize avg_num_neighbors and register as buffer
        norm_const = torch.tensor([(1.0 / sqrt(N)) for N in avg_num_neighbors])
        norm_const = norm_const.reshape(-1, 1)
        # Persistent=False to ensure backwards compatibility of FMs.
        # TODO remove this once we're sure FMs are not using this anymore
        self.register_buffer("norm_const", norm_const, persistent=False)

        # If global avg_num_neighbors or only one type, no need to do embedding lookup in forward
        self.norm_shortcut = self.norm_const.numel() == 1

    def forward(self, data: AtomicDataDict.Type) -> AtomicDataDict.Type:
        features = data[self.in_field]
        norm_size = features.size(0)

        if self.norm_key in data and data[self.norm_key].size(0) == norm_size:
            norm_factor = data[self.norm_key]
        else:
            # Compute norm factor for the first time
            if self.norm_shortcut:
                # No need to do embedding lookup in forward
                norm_factor = self.norm_const.expand(norm_size, -1)
            else:
                # Embed each avg_num_neighbors value per type
                norm_factor = torch.nn.functional.embedding(
                    data[AtomicDataDict.ATOM_TYPE_KEY][:norm_size],
                    self.norm_const,
                )
            data[self.norm_key] = norm_factor  # shape: (num_local_nodes, 1)

        data[self.out_field] = norm_factor * features
        return data
