import os
from typing import Optional, Tuple, List, Union, Callable
from tqdm import tqdm

import math
import argparse

import time
import numpy as np
import torch
from torch import nn
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
from tqdm import trange
import cv2 
import onnx
import itertools
from collections import defaultdict

from cifar10_resnet.resnet import resnet2b, resnet4b

from auto_LiRPA import BoundedModule, BoundedTensor, PerturbationLpNorm, PerturbationLinear

def classificaion_based_on_constant_bounds(model, x_L, x_U, batch_size, target_class):
    lirpa_model = BoundedModule(model, (torch.Tensor(x_L[0:1]).to('cuda').permute(0,3,1,2),))
    mask_verifeid_all = torch.zeros(x_L.shape[0], dtype=torch.bool, device='cuda')

    for i in range(x_L.shape[0] // batch_size+1):
        x_L_batch = x_L[i * batch_size:(i + 1) * batch_size]
        x_U_batch = x_U[i * batch_size:(i + 1) * batch_size]
        x_L_batch = torch.Tensor(x_L_batch).to('cuda')/255.0
        x_L_batch = x_L_batch.permute(0, 3, 1, 2)
        x_U_batch = torch.Tensor(x_U_batch).to('cuda')/255.0
        x_U_batch = x_U_batch.permute(0, 3, 1, 2)
        ptb = PerturbationLpNorm(x_L=x_L_batch, x_U=x_U_batch)
        bounded_x = BoundedTensor(x_L_batch, ptb)
        lb, ub = lirpa_model.compute_bounds(bounded_x, method="crown")

        mask_other = torch.ones(lb.size(1), dtype=torch.bool, device=lb.device)
        mask_other[target_class] = False
        
        mask_verified = (lb[:, target_class].unsqueeze(-1) > ub[:, mask_other]).all(dim=1)
        mask_verifeid_all[i * batch_size:(i + 1) * batch_size] = mask_verified
        print(f"Batch {i + 1}/{x_L.shape[0] // batch_size} processed.")
        print(f"Verified rate: {mask_verified.float().mean().item() * 100:.2f}%")
    return mask_verifeid_all

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('--model', type=str, default='resnet2b', help='model name')
    parser.add_argument('--data', type=str, default='res_classification.npz', help='dataset name')
    parser.add_argument('--target_class', type=int, default=0, help='target class for classification (0 for plane)')
    parser.add_argument('--batch_size', type=int, default=32, help='batch size')
    args = parser.parse_args()

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # Load the model
    if args.model == 'resnet2b':
        model = resnet2b()
    elif args.model == 'resnet4b':
        model = resnet4b()
    else:
        raise ValueError("Unsupported model")
    # Load the pre-trained weights
    model.load_state_dict(torch.load(f'./cifar10_resnet/{args.model}.pth')['state_dict'])
    model.to(device)

    # Load image bounds
    data = np.load(args.data)
    # data has shape (N, 32, 32, 3) range [0, 255]
    x_L = data['output_lb']
    x_U = data['output_ub']
    # x_L = torch.from_numpy(x_L).to(device) / 255.0
    # x_U = torch.from_numpy(x_U).to(device) / 255.0
    # x_L = x_L.permute(0, 3, 1, 2)  # Change to (N, C, H, W)
    # x_U = x_U.permute(0, 3, 1, 2)  # Change to (N, C, H, W)
    # x_L and x_U are now tensors of shape (N, 3, 32, 32) with values in [0, 1]

    # Perform classification based on constant bounds
    mask_verified = classificaion_based_on_constant_bounds(
        model, x_L, x_U, args.batch_size, args.target_class)
    print(mask_verified.shape)
    np.savez_compressed('all_results.npz', all_results=mask_verified.detach().cpu().numpy())
    print(f"\nTotal verified rate: {mask_verified.float().mean().item() * 100:.2f}%")
