################################################################################
# Copyright (c) 2021 ContinualAI.                                              #
# Copyrights licensed under the MIT License.                                   #
# See the accompanying LICENSE file for terms.                                 #
#                                                                              #
# Date: 20-11-2020                                                             #
# Author(s): Vincenzo Lomonaco                                                 #
# E-mail: contact@continualai.org                                              #
# Website: avalanche.continualai.org                                           #
################################################################################

"""
This is the getting started example, without the use of the Avalanche framework.
This is useful to understand why Avalanche is such a wonderful tool.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from os.path import expanduser

import argparse
import torch
import torch.nn as nn
from torch.nn import CrossEntropyLoss
from torch.optim import SGD
from torchvision import transforms
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from torch.utils.data import DataLoader
import numpy as np


def main(args):

    # Config
    device = torch.device(f"cuda:{args.cuda}"
                          if torch.cuda.is_available() and
                          args.cuda >= 0 else "cpu")

    # model
    class SimpleMLP(nn.Module):

        def __init__(self, num_classes=10, input_size=28 * 28):
            super(SimpleMLP, self).__init__()

            self.features = nn.Sequential(
                nn.Linear(input_size, 512),
                nn.ReLU(inplace=True),
                nn.Dropout(),
            )
            self.classifier = nn.Linear(512, num_classes)
            self._input_size = input_size

        def forward(self, x):
            x = x.contiguous()
            x = x.view(x.size(0), self._input_size)
            x = self.features(x)
            x = self.classifier(x)
            return x

    model = SimpleMLP(num_classes=10)

    # CL Benchmark Creation
    print("Creating the benchmark...")
    list_train_dataset = []
    list_test_dataset = []
    rng_permute = np.random.RandomState(0)
    train_transform = transforms.Compose([
        ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    test_transform = transforms.Compose([
        ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])

    # for every incremental experience
    idx_permutations = []
    for i in range(2):
        idx_permutations.append(torch.from_numpy(
            rng_permute.permutation(784)).type(torch.int64))

        # add the permutation to the default dataset transformation
        train_transform_list = train_transform.transforms.copy()
        train_transform_list.append(
            transforms.Lambda(
                lambda x, i=i: x.view(-1)[idx_permutations[i]].view(1, 28, 28))
        )
        new_train_transform = transforms.Compose(train_transform_list)

        test_transform_list = test_transform.transforms.copy()
        test_transform_list.append(
            transforms.Lambda(
                lambda x, i=i: x.view(-1)[idx_permutations[i]].view(1, 28, 28))
        )
        new_test_transform = transforms.Compose(test_transform_list)

        # get the datasets with the constructed transformation
        permuted_train = MNIST(root=expanduser("~") + "/.avalanche/data/mnist/",
                               train=True,
                               download=True, transform=new_train_transform)
        permuted_test = MNIST(root=expanduser("~") + "/.avalanche/data/mnist/",
                              train=False,
                              download=True, transform=new_test_transform)

        list_train_dataset.append(permuted_train)
        list_test_dataset.append(permuted_test)

    # Train
    optimizer = SGD(model.parameters(), lr=0.001, momentum=0.9)
    criterion = CrossEntropyLoss()

    model.to(device)
    print("Starting training...")
    for task_id, train_dataset in enumerate(list_train_dataset):
        print("Starting task:", task_id)
        train_data_loader = DataLoader(
            train_dataset, batch_size=32, shuffle=False)

        for ep in range(1):
            print("Epoch: ", ep)
            for iteration, (train_mb_x, train_mb_y) in enumerate(
                    train_data_loader):
                optimizer.zero_grad()
                train_mb_x = train_mb_x.to(device)
                train_mb_y = train_mb_y.to(device)

                # Forward
                logits = model(train_mb_x)
                # Loss
                loss = criterion(logits, train_mb_y)
                if iteration % 100 == 0:
                    print("Iter: {}, Loss: {}".format(iteration, loss.item()))
                # Backward
                loss.backward()
                # Update
                optimizer.step()

        # Test
        acc_results = []
        print("Starting testing...")
        for task_id, test_dataset in enumerate(list_test_dataset):

            test_data_loader = DataLoader(
                test_dataset, batch_size=32)

            correct = 0
            for iteration, (test_mb_x, test_mb_y) in enumerate(
                    test_data_loader):

                # Move mini-batch data to device
                test_mb_x = test_mb_x.to(device)
                test_mb_y = test_mb_y.to(device)

                # Forward
                test_logits = model(test_mb_x)
                preds = torch.argmax(test_logits.long(), dim=1)

                # compute acc
                correct += (test_mb_y.eq(preds)).sum().item()

            print("Task:", task_id)
            acc = (correct / len(test_dataset)) * 100
            print("Accuracy results: ", acc)
            acc_results.append(acc)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--cuda', type=int, default=0,
                        help='Select zero-indexed cuda device. -1 to use CPU.')
    args = parser.parse_args()
    main(args)
