import torch.nn as nn
import torch.nn.functional as F
import torch as th
import numpy as np
import os

class UniDeepFsmn(nn.Module):
    """UniDeepFsmn is a neural network module implementing a unified deep
    Feedforward Sequential Memory Network (FSMN) for sequence-to-sequence tasks.

    Args:
        input_dim (int): The number of input features.
        output_dim (int): The number of output features.
        lorder (int, optional): The order of the linear filter. If None, the filter order is not used.
        hidden_size (int, optional): The number of hidden units in the first linear layer.

    Attributes:
        input_dim (int): The dimension of the input features.
        output_dim (int): The dimension of the output features.
        lorder (int): The order of the linear filter.
        hidden_size (int): The number of hidden units.
        linear (nn.Linear): Linear transformation layer from input_dim to hidden_size.
        project (nn.Linear): Linear transformation layer from hidden_size to output_dim without bias.
        conv1 (nn.Conv2d): Convolutional layer for processing the output of the project layer.
    """

    def __init__(self, input_dim, output_dim, lorder=None, hidden_size=None):
        """Initializes the UniDeepFsmn model.

        Args:
            input_dim (int): The number of input features.
            output_dim (int): The number of output features.
            lorder (int, optional): The order of the linear filter. Default is None.
            hidden_size (int, optional): The number of hidden units. Default is None.
        """
        super(UniDeepFsmn, self).__init__()

        self.input_dim = input_dim  # Store input dimension
        self.output_dim = output_dim  # Store output dimension

        if lorder is None:
            return  # Exit if no filter order is specified

        self.lorder = lorder  # Store the linear filter order
        self.hidden_size = hidden_size  # Store hidden layer size

        # Define linear transformation from input to hidden layer
        self.linear = nn.Linear(input_dim, hidden_size)

        # Define linear transformation from hidden layer to output
        self.project = nn.Linear(hidden_size, output_dim, bias=False)

        # Define convolutional layer for processing output with specified filter order
        self.conv1 = nn.Conv2d(output_dim, output_dim, [lorder + lorder - 1, 1], [1, 1], groups=output_dim, bias=False)

    def forward(self, input):
        """Forward pass of the UniDeepFsmn model.

        Args:
            input (torch.Tensor): The input tensor with shape (batch_size, input_dim).

        Returns:
            torch.Tensor: The output tensor after processing through the model.
        """
        # Apply ReLU activation after linear transformation
        f1 = F.relu(self.linear(input))

        # Project to output dimension
        p1 = self.project(f1)

        # Unsqueeze to add a new dimension for the convolution operation
        x = th.unsqueeze(p1, 1)

        # Permute dimensions for the convolutional layer input
        x_per = x.permute(0, 3, 2, 1)

        # Pad the input for convolutional layer
        y = F.pad(x_per, [0, 0, self.lorder - 1, self.lorder - 1])

        # Apply convolution and add the result back to the original input
        out = x_per + self.conv1(y)

        # Permute dimensions back to original format for output
        out1 = out.permute(0, 3, 2, 1)

        # Return the combined output
        return input + out1.squeeze()
