import numpy as np

def load_data(filename):
    with open(filename, 'r') as file:
        data = [list(map(float, line.strip().split())) for line in file]
    
    data = np.array(data)
    X = data[:, :-1]
    y = data[:, -1]
    return X, y

def get_classes_from_file(filename):
    classes = []
    with open(filename, 'r') as file:
        for line in file:
            data = line.strip().split()  # Splitting the data by spaces
            classes.append(float(data[-1]))  # Adding the last element of each line to the classes list
    return list(set(classes))  # Using set to get unique classes and then converting it back to a list

def get_data_for_class(filename, target_class=5.0):
    data_for_class = []

    # Read the file and filter rows for the target class
    with open(filename, 'r') as file:
        for line in file:
            row = [float(x) for x in line.strip().split()]
            if row[-1] == target_class:  # Checking if the last column matches the target class
                data_for_class.append(row)

    return data_for_class

def get_mean_for_class(filename, target_class=1.0):
    # Initialize lists to store data
    data_for_class = []

    # Read the file and filter data
    with open(filename, 'r') as file:
        for line in file:
            row = [float(x) for x in line.strip().split()]
            if row[-1] == target_class:  # Checking if the last column matches the target class
                data_for_class.append(row[:-1])  # Exclude the last column (class label)

    # Convert to a 2D list for easier manipulation
    data_matrix = [list(i) for i in zip(*data_for_class)]

    # Compute mean for each column and format to ensure two decimal places
    means = ["{:.2f}".format(sum(column) / len(column)) for column in data_matrix]

    return means

def get_std_for_class(filename, target_class=1.0):
    # Initialize lists to store data
    data_for_class = []

    # Read the file and filter data
    with open(filename, 'r') as file:
        for line in file:
            row = [float(x) for x in line.strip().split()]
            if row[-1] == target_class:  # Checking if the last column matches the target class
                data_for_class.append(row[:-1])  # Exclude the last column (class label)

    # Convert to a 2D list for easier manipulation
    data_matrix = [list(i) for i in zip(*data_for_class)]

    # Compute mean for each column
    means = [sum(column) / len(column) for column in data_matrix]

    # Compute variance for each column
    variances = []
    for i, column in enumerate(data_matrix):
        mean = means[i]
        variance = sum([(x - mean)**2 for x in column]) / (len(column)-1)
        variances.append(variance)
    
    # Compute standard deviation from variance, 
    # replace values less than 0.01 with 0.01 and format to ensure two decimal places
    std_devs = []
    for variance in variances:
        std_dev = max(variance**0.5, 0.01)
        std_devs.append("{:.2f}".format(std_dev))

    return std_devs

def classify(X_test, mean_dict, std_dict, class_probs):
    results = []
    all_probs = []
    
    for x in X_test:
        probs = {}
        for c in class_probs:
            prob_x_given_c = 1
            
            for i in range(len(x)):
                # Convert mean and std from string to float
                mean_val = float(mean_dict[c][i])
                std_val = float(std_dict[c][i])
                
                exponent = -((x[i] - mean_val)**2) / (2 * (std_val**2))
                base = np.e
                prob = (base**exponent) / ((2 * np.pi)**0.5 * std_val)
                prob_x_given_c *= prob
                
            probs[c] = prob_x_given_c * class_probs[c]
        
        total_prob = sum(probs.values())
        probs = {key: val/total_prob for key, val in probs.items()}
        
        predicted_class = max(probs, key=probs.get)
        predicted_prob = probs[predicted_class]
        results.append((predicted_class, predicted_prob))
        all_probs.append(probs)
    
    return results, all_probs


def output(training_file, test_file):
    """
    Processes classification data from training and test files and outputs classification results.

    This function performs several key steps in a classification task:
    1. Loading of training and test data.
    2. Calculation of mean, standard deviation, and class probabilities for each class in the training data.
    3. Classification of test data based on the calculated statistics.
    4. Printing of the calculated statistics for each class.
    5. Printing of the classification results for each instance in the test data, including the predicted class, its probability, the true class, and the calculated accuracy.
    6. Computing and printing the overall classification accuracy.

    Parameters:
    - training_file (str): Path to the file containing training data.
    - test_file (str): Path to the file containing test data.

    Returns:
    None. The function outputs results directly to the console.

    Notes:
    - The function assumes the existence of several helper functions and appropriate data format in the provided files.
    - The accuracy calculation accounts for ties in predicted probabilities.
    - Functions used within this function include `load_data`, `get_classes_from_file`, `get_mean_for_class`, `get_std_for_class`, and `classify`.
    - The function prints detailed statistics and results for each class and test instance to the console.
    """
    # Load data
    X_train, y_train = load_data(training_file)
    X_test, y_test = load_data(test_file)

    class_list = get_classes_from_file(training_file)
    mean_dict = {}
    std_dict = {}
    for cls in class_list:
        mean_dict[cls] = get_mean_for_class(training_file, cls)
    for cls in class_list:
        std_dict[cls] = get_std_for_class(training_file, cls)

    class_probs = {}
    for cls in class_list:
        class_data = [y for y in y_train if y == cls]
        class_probs[cls] = len(class_data) / len(y_train)
    
    # Classify
    results, all_instance_probs = classify(X_test, mean_dict, std_dict, class_probs)

    
    #print training results
    for cls, means in mean_dict.items():
            int_class = int(cls)  # Convert the class value to an integer
            stds = std_dict[cls]
            for index, (mean, std) in enumerate(zip(means, stds)):
                line = f"Class {int_class}, attribute {index + 1}, mean = {mean}, std = {std}\n"
                print(line, end="")  # Print to console
                


        # Print classification results
    accuracies = []
    for i, ((predicted_class, predicted_prob), true_class) in enumerate(zip(results, y_test)):
            if predicted_class == true_class:
                accuracy = 1.0
            else:
                tied_classes = [c for c, p in all_instance_probs[i].items() if p == predicted_prob]
                if true_class in tied_classes:
                    accuracy = 1.0 / len(tied_classes)
                else:
                    accuracy = 0.0
            accuracies.append(accuracy)
          
            print(f"ID={i+1:5d}, predicted={int(predicted_class):3d}, probability = {predicted_prob:.4f}, true={int(true_class):3d}, accuracy={accuracy:.2f}")
        # Print overall accuracy
    print(f"classification accuracy={np.mean(accuracies):.4f}")

