from typing import Tuple, List, Callable, Generator
from import_data import test_x_y, train_x_y, IMAGE_SIZE, print_img_to_console, show_picture
import numpy as np


class MulticlassPerceptron:
    class Perceptron:
        def __init__(self, input_size, learn_rate: float = 0.0001):
            self.weights = np.random.rand(input_size + 1) * 2 - 1
            self.learn_rate = learn_rate

        def train(self, data_lbl_pairs: Callable[[], Generator], positive_label: int, iterations: int = 10):
            for num_iter in range(iterations):
                print(f"Iteration number: {num_iter + 1}")
                for i, (x, y) in enumerate(data_lbl_pairs()):
                    prediction = self.output(x)
                    label = 1 if y == positive_label else -1
                    correct = prediction >= 0 and label >= 0 or prediction < 0 and label < 0
                    if not correct:
                        self.weights = self.weights + self.learn_rate * label * np.array([1.0] + x)

        def output(self, datum):
            return np.dot(self.weights, np.array([1.0] + datum))

        def get_normalised_weight_array(self):
            return [int(x) for x in (((self.weights / np.max(self.weights) + 1) / 2) * 255)]

    def __init__(self, input_size: int, num_classes: int, learn_rate: float = 0.001):
        self.classifiers = [self.Perceptron(input_size, learn_rate) for _ in range(num_classes)]

    def train(self, data_lbl_pairs: Callable[[], Generator], iterations: int = 10):
        for i, classifier in enumerate(self.classifiers):
            print(f"Training classifier for class {i}...")
            classifier.train(data_lbl_pairs, i, iterations)
            print()

    def output(self, datum: List[int]):
        return list(map(lambda x: x.output(datum), self.classifiers))

    def prediction(self, datum):
        return max(list(range(10)), key=lambda x: self.output(datum)[x])

    def view_for_classifier(self, classifier_index: int):
        return self.classifiers[classifier_index].get_normalised_weight_array()


def train_and_test_multiclass_perceptron(iterations: int = 5, training_inputs: int = 5000, test_inputs: int = 1000):
    print("Loading data")
    training_data_gen = train_x_y(training_inputs)
    print("Begin training model!")
    model = MulticlassPerceptron(IMAGE_SIZE, 10)
    model.train(training_data_gen, iterations)
    print("Model successfully trained.")
    print("Testing model...")
    test_data = list(test_x_y(test_inputs)())
    n_correct = sum(model.prediction(x) == y for x, y in test_data)
    accuracy = n_correct / len(test_data)
    print(f"Accuracy: {accuracy} ({n_correct} correctly classified out of {len(test_data)} total test inputs.)")
    for i in range(10):
        print_img_to_console(model.view_for_classifier(i))


def get_trained_digit_model(iterations: int = 5, training_inputs: int = 5000, test_inputs: int = 1000):
    training_data_gen = train_x_y(training_inputs)
    model = MulticlassPerceptron(IMAGE_SIZE, 10)
    model.train(training_data_gen, iterations)
    return model


if __name__ == "__main__":
    train_and_test_multiclass_perceptron()
