"""GNN 模型：GIN，移植自 TorchEEG，主要用于图结构 EEG 表征的分类任务。"""

from typing import Optional

import torch
import torch.nn as nn
import torch.nn.functional as F
from zyplib.nn import BaseModule


class LazyLoader:
    def __init__(self, lib_name):
        self._lib_name = lib_name
        self._module = None

    def __getattr__(self, name):
        if self._module is None:
            try:
                self._module = __import__(self._lib_name)
            except ImportError:
                raise ImportError(
                    f"To use this functionality, you need to install `{self._lib_name}`. "
                    f"Please refer to https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html"
                )
        return getattr(self._module, name)


pyg = LazyLoader('torch_geometric')


def _batch_edge_index(edge_index: torch.Tensor, num_nodes: int, batch_size: int) -> torch.Tensor:
    offsets = torch.arange(batch_size, device=edge_index.device).repeat_interleave(edge_index.shape[1]) * num_nodes
    return edge_index.repeat(1, batch_size) + offsets.unsqueeze(0)


def _unpack_graph_input(
    data_or_x,
    edge_index: Optional[torch.Tensor] = None,
    batch: Optional[torch.Tensor] = None,
):
    if edge_index is None and hasattr(data_or_x, 'x') and hasattr(data_or_x, 'edge_index'):
        data = data_or_x
        x = data.x
        edge_index = data.edge_index
        batch = getattr(data, 'batch', None)
        num_graphs = getattr(data, 'num_graphs', None)
        return x, edge_index, batch, num_graphs

    if edge_index is None:
        raise ValueError('edge_index is required when the input is not a PyG Batch/Data object.')

    x = data_or_x
    if x.ndim == 3:
        batch_size, num_nodes, num_features = x.shape
        if edge_index.numel() > 0 and int(edge_index.max().item()) < num_nodes:
            edge_index = _batch_edge_index(edge_index, num_nodes, batch_size)
        x = x.reshape(batch_size * num_nodes, num_features)
        if batch is None:
            batch = torch.arange(batch_size, device=x.device).repeat_interleave(num_nodes)
        return x, edge_index, batch, batch_size

    if x.ndim == 2:
        if batch is None:
            raise ValueError('batch is required when x is already flattened to shape [num_nodes_total, num_features].')
        num_graphs = int(batch.max().item()) + 1 if batch.numel() > 0 else 0
        return x, edge_index, batch, num_graphs

    raise ValueError('Expected x to have shape [batch, num_nodes, num_features] or [num_nodes_total, num_features].')


class GIN(BaseModule):
    r'''
    A simple but effective graph isomorphism network (GIN) structure from the book of Zhang et al. For more details, please refer to the following information.

    - Book: Zhang X, Yao L. Deep Learning for EEG-Based Brain-Computer Interfaces: Representations, Algorithms and Applications[M]. 2021.
    - URL: https://www.worldscientific.com/worldscibooks/10.1142/q0282#t=aboutBook
    - Related Project: https://github.com/xiangzhang1015/Deep-Learning-for-BCI/blob/master/pythonscripts/4-3_GIN.py

    Below is a recommended suite for use in emotion recognition tasks:

    .. code-block:: python

        from torcheeg.datasets import DEAPDataset
        from torcheeg import transforms
        from torcheeg.transforms.pyg import ToG
        from torcheeg.datasets.constants import SEED_STANDARD_ADJACENCY_MATRIX
        from torcheeg.models import GIN
        from torch_geometric.data import DataLoader

        dataset = DEAPDataset(root_path='./data_preprocessed_python',
                              offline_transform=transforms.BandDifferentialEntropy(),
                              online_transform=ToG(SEED_STANDARD_ADJACENCY_MATRIX),
                              label_transform=transforms.Compose([
                                  transforms.Select('valence'),
                                  transforms.Binary(5.0),
                              ]))

        model = GIN(in_channels=4, hid_channels=64, num_classes=2)

        x, y = next(iter(DataLoader(dataset, batch_size=64)))
        model(x)

    Args:
        in_channels (int): The feature dimension of each electrode. (default: :obj:`4`)
        hid_channels (int): The number of hidden nodes in the GRU layers and the fully connected layer. (default: :obj:`64`)
        num_classes (int): The number of classes to predict. (default: :obj:`2`)
    '''

    def __init__(self,
                 in_channels: int = 4,
                 hid_channels: int = 64,
                 num_classes: int = 3):
        super(GIN, self).__init__()

        self.in_channels = in_channels
        self.hid_channels = hid_channels
        self.num_classes = num_classes

        nn1 = nn.Sequential(nn.Linear(in_channels, hid_channels), nn.ReLU(),
                            nn.Linear(hid_channels, hid_channels))
        self.conv1 = pyg.nn.GINConv(nn1)
        self.bn1 = nn.BatchNorm1d(hid_channels)

        nn2 = nn.Sequential(nn.Linear(hid_channels, hid_channels), nn.ReLU(),
                            nn.Linear(hid_channels, hid_channels))
        self.conv2 = pyg.nn.GINConv(nn2)
        self.bn2 = nn.BatchNorm1d(hid_channels)

        nn3 = nn.Sequential(nn.Linear(hid_channels, hid_channels), nn.ReLU(),
                            nn.Linear(hid_channels, hid_channels))
        self.conv3 = pyg.nn.GINConv(nn3)
        self.bn3 = nn.BatchNorm1d(hid_channels)

        self.fc1 = nn.Linear(hid_channels, hid_channels)
        self.fc2 = nn.Linear(hid_channels, num_classes)

    def forward(
        self,
        data_or_x,
        edge_index: Optional[torch.Tensor] = None,
        batch: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        r'''
        Args:
            data_or_x: PyG 的 Batch/Data，或张量形式的节点特征。
            edge_index (torch.Tensor | None): 当 data_or_x 为张量时，图边索引，shape 为 :obj:`[2, num_edges]`。
            batch (torch.Tensor | None): 当 data_or_x 为扁平节点张量时，对应每个节点所属图的 batch 索引。

        Returns:
            torch.Tensor[number of sample, number of classes]: the predicted probability that the samples belong to the classes.
        '''
        x, edge_index, batch, num_graphs = _unpack_graph_input(data_or_x, edge_index, batch)

        x = x.reshape([-1, self.in_channels])

        x = F.relu(self.conv1(x, edge_index))
        x = self.bn1(x)
        x = F.relu(self.conv2(x, edge_index))
        x = self.bn2(x)
        x = F.relu(self.conv3(x, edge_index))
        x = self.bn3(x)

        if batch is None:
            raise ValueError('batch information is required for graph-level pooling.')

        x = pyg.nn.global_add_pool(x, batch, size=num_graphs)
        x = F.dropout(x, p=0.3, training=self.training)
        x = self.fc2(x)

        return x
