import torch
import torch.nn.functional as F
from torch import optim
from torch import nn
from torchvision import datasets, transforms, models
#The dataset is cats vs dogs from Kaggle
train_data_direction = '/home/hoangminhq/TestFolder/dogs-vs-cats/Cat_Dog_data/Cat_Dog_data/train'
test_data_direction  = '/home/hoangminhq/TestFolder/dogs-vs-cats/Cat_Dog_data/Cat_Dog_data/test'
train_transform = transforms.Compose([transforms.RandomRotation(30),
                                      transforms.RandomResizedCrop(224),
                                      transforms.RandomHorizontalFlip(),
                                      transforms.ToTensor(),
                                      transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])])

test_transform = transforms.Compose([transforms.Resize(255),
                                     transforms.CenterCrop(224),
                                     transforms.ToTensor(),
                                     transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])])

train_dataset = datasets.ImageFolder(train_data_direction, transform = train_transform)
test_dataset  = datasets.ImageFolder(test_data_direction, transform = test_transform)

train_loader   = torch.utils.data.DataLoader(train_dataset, batch_size = 128, shuffle = True)
test_loader    = torch.utils.data.DataLoader(test_dataset, batch_size = 128)

class AlexNet(nn.Module):

    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        x = F.log_softmax(x,dim=1)
        return x
model = AlexNet()
#Modify Alexnet abit
for params in model.parameters():
    params.requires_grad = False
from collections import OrderedDict
classifier = nn.Sequential(OrderedDict([
                          ('fc1', nn.Linear(9216, 4096)),
                          ('relu', nn.ReLU()),
                          ('fc2', nn.Linear(4096, 2)),
                          ('output', nn.LogSoftmax(dim=1))
                          ]))
    
model.classifier = classifier

criterion = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9, weight_decay=0.0005)
def train(model, train_loader, criterion, optimizer, device, epoch):
    model.train()
    for idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output,target)
        loss.backward()
        optimizer.step()

def prune_filter(model, layer, next_layer, indexes):     
    new_conv = torch.nn.Conv2d(in_channels=model.features[layer].in_channels, 
                               out_channels=model.features[layer].out_channels-1,
                               kernel_size=model.features[layer].kernel_size,
                               stride=model.features[layer].stride,
                               padding=model.features[layer].padding)
    new_conv.weight[0:indexes,:,:,:] = model.features[layer].weight[0:indexes,:,:,:]
    new_conv.weight[indexes:,:,:,:] = model.features[layer].weight[indexes+1,:,:,:]
    new_conv.bias[0:indexes] = model.features[layer].bias[0:indexes]
    new_conv.bias[indexes:] = model.features[layer].bias[indexes+1:]
    model.features[layer] = new_conv
    next_new_conv = torch.nn.Conv2d(in_channels=model.features[next_layer].in_channels-1, 
                                        out_channels=model.features[next_layer].out_channels,
                                        kernel_size=model.features[next_layer].kernel_size,
                                        stride=model.features[next_layer].stride,
                                        padding=model.features[next_layer].padding)
        next_new_conv.weight[:,0:indexes,:,:] = model.features[next_layer].weight[:,0:indexes,:,:]
        next_new_conv.weight[:,indexes:,:,:] = model.features[next_layer].weight[:,indexes+1:,:,:]
        model.features[next_layer] = next_new_conv
def main(model, train_loader,test_loader,criterion,optimizer,
         pretrained=False,prune=False,save=False,pruneFilter=False):
    device = 'cuda'
    if pruneFilter == True:
        #conv0:
        for num_filters_pruned in range(16):
            model=prune_filter(model=model, layer=0, next_layer=3, indexes=index_pruned(model,0))
            if num_filters_pruned %4 == 0:
                model=model.cuda()
                train(model, train_loader, criterion, optimizer, device, 1)
                test(model,test_loader,criterion,device)

main(model,train_loader,test_loader,criterion,optimizer,pretrained=True,prune=False,save=False,pruneFilter=True)

#This should be enough