# Source code for deeprobust.graph.global_attack.base_attack

    import os.path as osp

    import numpy as np
    import scipy.sparse as sp
    import torch
    from torch.nn.modules.module import Module

    from deeprobust.graph import utils

    class BaseAttack(Module):
        """Abstract base class for target attack classes.

        Parameters
        ----------
        model :
            model to attack
        nnodes : int
            number of nodes in the input graph
        attack_structure : bool
            whether to attack graph structure
        attack_features : bool
            whether to attack node features
        device: str
            'cpu' or 'cuda'

        """

        def __init__(self, model, nnodes, attack_structure=True, attack_features=False, device='cpu'):
            super(BaseAttack, self).__init__()

            self.surrogate = model
            self.nnodes = nnodes
            self.attack_structure = attack_structure
            self.attack_features = attack_features
            self.device = device
            self.modified_adj = None
            self.modified_features = None
            if model is not None:
                self.nclass = model.nclass
                self.nfeat = model.nfeat
                self.hidden_sizes = model.hidden_sizes

        def attack(self, ori_adj, n_perturbations, **kwargs):
            """Generate attacks on the input graph.

            Parameters
            ----------
            ori_adj : scipy.sparse.csr_matrix
                Original (unperturbed) adjacency matrix.
            n_perturbations : int
                Number of edge removals/additions.

            Returns
            -------
            None.

            """
            pass

        def check_adj(self, adj):
            """Check if the modified adjacency is symmetric and unweighted.
            """
            assert np.abs(adj - adj.T).sum() == 0, "Input graph is not symmetric"
            assert adj.tocsr().max() == 1, "Max value should be 1!"
            assert adj.tocsr().min() == 0, "Min value should be 0!"

        def check_adj_tensor(self, adj):
            """Check if the modified adjacency is symmetric, unweighted, all-zero diagonal.
            """
            assert torch.abs(adj - adj.t()).sum() == 0, "Input graph is not symmetric"
            assert adj.max() == 1, "Max value should be 1!"
            assert adj.min() == 0, "Min value should be 0!"
            diag = adj.diag()
            assert diag.max() == 0, "Diagonal should be 0!"
            assert diag.min() == 0, "Diagonal should be 0!"

        def save_adj(self, root=r'/tmp/', name='mod_adj'):
            """Save attacked adjacency matrix.

            Parameters
            ----------
            root :
                root directory where the variable should be saved
            name : str
                saved file name

            Returns
            -------
            None.

            """
            assert self.modified_adj is not None, \
                    'modified_adj is None! Please perturb the graph first.'
            name = name + '.npz'
            modified_adj = self.modified_adj

            if type(modified_adj) is torch.Tensor:
                sparse_adj = utils.to_scipy(modified_adj)
                sp.save_npz(osp.join(root, name), sparse_adj)
            else:
                sp.save_npz(osp.join(root, name), modified_adj)

        def save_features(self, root=r'/tmp/', name='mod_features'):
            """Save attacked node feature matrix.

            Parameters
            ----------
            root :
                root directory where the variable should be saved
            name : str
                saved file name

            Returns
            -------
            None.

            """

            assert self.modified_features is not None, \
                    'modified_features is None! Please perturb the graph first.'
            name = name + '.npz'
            modified_features = self.modified_features

            if type(modified_features) is torch.Tensor:
                sparse_features = utils.to_scipy(modified_features)
                sp.save_npz(osp.join(root, name), sparse_features)
            else:
                sp.save_npz(osp.join(root, name), modified_features)