import mpq_quantize
import pareto_sols
import configure_ibex
import simulate_ibex
import os
import sys
import numpy as np

def create_ibex_qnn(net, name, device, X_train, y_train, X_test, y_test, X_val = None, y_val = None, 
         BATCH_SIZE = 32, epochs = 20, lr = 0.0001, max_acc_drop = None):
    
    net = net.to(device)

    in_shape = mpq_quantize.net_input_size(X_train)

    macc_per_layer, _ = mpq_quantize.display_model_info(net, in_shape)

    train_loader, val_loader, test_loader = mpq_quantize.create_dataloaders(BATCH_SIZE, X_train, y_train, X_test, y_test,
                                                                            X_val, y_val)

    if isinstance(epochs, (int, float)):
        tr_epochs = [epochs]
    else:
        tr_epochs = epochs

    if isinstance(lr, (int, float)):
        tr_lr = [lr]
    else:
        tr_lr = lr
    
    if len(tr_epochs) != len(tr_lr):
        print("Error: The lengths of 'epochs' and 'lr' are not equal.")
        sys.exit(1)

    print('\nFULL PRECISION MODEL TRAINING ...')

    for i in range(len(tr_epochs)):
        print(f'Round No{i+1}')
        fp_net = mpq_quantize.fp_train(net, train_loader, val_loader, device, 
                                       epochs = tr_epochs[i], lr = tr_lr[i])

    fp_accuracy = mpq_quantize.fp_evaluate(net, test_loader, device)

    weights_per_layer, total_macc_opt_sorted = mpq_quantize.create_weight_confs(macc_per_layer)

    if(max_acc_drop is None):
        
        quant_net, accuracy = mpq_quantize.dse(fp_net, max_acc_drop, weights_per_layer, fp_accuracy, train_loader,
                                        test_loader, val_loader, device, tr_epochs, tr_lr)
        
        optimal_config = pareto_sols.pareto_space(fp_accuracy, accuracy, weights_per_layer, macc_per_layer,
                                            total_macc_opt_sorted, name)
        
        layer_mapping = mpq_quantize.create_layer_mapping(optimal_config)
        
        quant_net = mpq_quantize.Quant_Model(fp_net, optimal_config, layer_mapping, 
                                            mpq_quantize.calculate_minimum(train_loader) >= 0)
        
        quant_net = mpq_quantize.train_quant_model(quant_net, train_loader, val_loader, device, epochs, lr)
        
        mpq_quantize.quant_net_evaluation(quant_net, test_loader, device)

    else:
        quant_net, optimal_config = mpq_quantize.dse(fp_net, max_acc_drop, weights_per_layer, fp_accuracy, train_loader, 
                                        test_loader, val_loader, device, tr_epochs, tr_lr)

    print('\nCREATING FILES FOR SIMULATION ON IBEX CORE ...')
    mode_per_layer, layer_type = configure_ibex.decide_mode(fp_net, 
                                    optimal_config, mpq_quantize.calculate_minimum(train_loader) >= 0)

    int_weights, int_og_bias, int_biases, shift_biases, mul_vals, shift_vals = configure_ibex.get_int_params(quant_net)

    input, padded_input, padded_w, padded_b, padded_sb_v, padded_mv, padded_sv = configure_ibex.pad_inputs_weights(quant_net, 
                                                    test_loader, mode_per_layer, int_weights, 
                                                    int_biases, shift_biases, mul_vals, shift_vals)

    combined_input, new_int_w, new_int_b, shift_b, mul_v, shift_v = configure_ibex.concat_inputs_weights(mode_per_layer, 
                                                    padded_input, padded_w, padded_b, 
                                                    padded_sb_v, padded_mv, padded_sv)

    optimized_path = '../inference_codes/' + name + '/optimized'
    original_path = '../inference_codes/' + name + '/original'

    os.makedirs(optimized_path, exist_ok = True)
    os.makedirs(original_path,  exist_ok = True)

    configure_ibex.generate_Makefile(original_path, name)

    if np.unique(layer_type)[0] =='Linear':
        configure_ibex.save_1d_inputs(original_path, input)
        configure_ibex.save_1d_inputs(optimized_path, combined_input)
        
        configure_ibex.save_mlp_net_params(original_path, int_weights, int_og_bias, mul_vals, shift_vals)
        configure_ibex.save_mlp_net_params(optimized_path, new_int_w, new_int_b, mul_v, shift_v, shift_b)
        
        configure_ibex.generate_og_c_code_mlp(original_path, name, int_weights, optimal_config, layer_type)  
        configure_ibex.generate_opt_c_code_mlp(optimized_path, name, new_int_w, optimal_config, layer_type)

    else:
        configure_ibex.save_2d_inputs(original_path, input)
        configure_ibex.save_2d_inputs(optimized_path, combined_input)

        configure_ibex.save_cnn_net_params(original_path, int_weights, int_og_bias, mul_vals, shift_vals)
        configure_ibex.save_cnn_net_params(optimized_path, new_int_w, new_int_b, mul_v, shift_v, shift_b)

        cnn_details = configure_ibex.get_cnn_details(fp_net)

        configure_ibex.generate_og_c_code_cnn(original_path, name, input, cnn_details, int_weights)
        configure_ibex.generate_opt_c_code_cnn(optimized_path, name, combined_input, cnn_details, 
                                            new_int_w, optimal_config)

    print('FINISHED ...')

    if(name == 'elderly_fall'):
        print('\nSIMULATING MODEL ON IBEX CORE\nUSE THE OUTPUTS TO VERIFY THAT THE RESULTS ARE CORRECT !!')
        ibex_model = simulate_ibex.create_fann_model(int_weights, int_og_bias, mul_vals, shift_vals)
        simulate_ibex.eval_sim_model(quant_net, ibex_model, test_loader)
    
    elif(name == 'uci_fann_net'):
        print('\nSIMULATING MODEL ON IBEX CORE\nUSE THE OUTPUTS TO VERIFY THAT THE RESULTS ARE CORRECT !!')
        ibex_model = simulate_ibex.create_uci_model(int_weights, int_og_bias, mul_vals, shift_vals)
        simulate_ibex.eval_sim_model(quant_net, ibex_model, test_loader)
    
    elif(name == 'lenet5_mnist'):
        print('\nSIMULATING MODEL ON IBEX CORE\nUSE THE OUTPUTS TO VERIFY THAT THE RESULTS ARE CORRECT !!')
        ibex_model = simulate_ibex.create_lenet_model(int_weights, int_og_bias, mul_vals, shift_vals)
        simulate_ibex.eval_sim_model(quant_net, ibex_model, test_loader)
        
    elif(name == 'cmsis_cnn'):
        print('\nSIMULATING MODEL ON IBEX CORE\nUSE THE OUTPUTS TO VERIFY THAT THE RESULTS ARE CORRECT !!')
        ibex_model = simulate_ibex.create_cmsis_cnn_model(int_weights, int_og_bias, mul_vals, shift_vals)
        simulate_ibex.eval_sim_model(quant_net, ibex_model, test_loader)

    elif(name == 'cifar10_dws_cnn'):
        print('\nSIMULATING MODEL ON IBEX CORE\nUSE THE OUTPUTS TO VERIFY THAT THE RESULTS ARE CORRECT !!')
        ibex_model = simulate_ibex.create_ibex_dws_model(int_weights, int_og_bias, mul_vals, shift_vals)
        simulate_ibex.eval_sim_model(quant_net, ibex_model, test_loader)
