# %%
import pandas as pd
import numpy as np

# %%
import kagglehub
path = kagglehub.dataset_download("dhruvildave/english-handwritten-characters-dataset")

# %%
df = pd.read_csv("english.csv")
df

# %%
from PIL import Image
from tqdm import tqdm

images = []

for path in tqdm(df['image']):
    with Image.open(path) as img:
        img_grey = img.convert('L').resize((28, 28))
        normalised = 1.0 - (np.array(img_grey, dtype=np.float32).flatten() / 255.0)
        images.append(normalised)

# %%
X = np.array(images)
n = X.shape[0] # 28 * 28
X = np.c_[np.ones(n), X]

y, label = pd.factorize(df["label"])

# %%
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# %%
# perpcetron

def step_activation(z):
    return np.where(z >= 0, 1, -1)

def train_bin_pla(X, y, lr=0.1, epochs=50):
    n_samp, n_feat = X.shape

    w = np.zeros(n_feat)

    for epoch in range(epochs):
        errors = 0
        
        for i in range(n_samp):
            z = np.dot(X[i], w)
            y_pred = step_activation(z)

            if y_pred != y[i]:
                w += lr * y[i] * X[i]
                errors += 1
        
        # early stop
        if errors == 0:
            break
    
    return w

def fit_pla(X, y, n_class, lr=0.1, epochs=50):
    n_feat = X.shape[1]

    W = np.zeros((n_class, n_feat))

    for clas in tqdm(range(n_class)):
        y_binary = np.where(y == clas, 1, -1)
        W[clas] = train_bin_pla(X, y_binary)
    
    return W

def predict_pla(X, W):
    scores = np.dot(X, W.T)
    return np.argmax(scores, axis=1), scores

# %%
def plot_confusion_matrix(y_true, y_pred, labels, title="Confusion Matrix"):
    cm = confusion_matrix(y_true, y_pred)
    fig, ax = plt.subplots(figsize=(10, 10))
    ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels).plot(
        ax=ax, cmap="coolwarm", colorbar=False
    )
    plt.xticks(rotation=90)
    plt.title(title)
    plt.tight_layout()
    plt.show()

# %%
# Train One-vs-Rest PLA
num_classes = len(np.unique(y_train))
W = fit_pla(x_train, y_train, n_class=num_classes, lr=0.01, epochs=100)

train_preds_pla, _ = predict_pla(x_train, W)
test_preds_pla, test_scores_pla = predict_pla(x_test, W)

# %%
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
plot_confusion_matrix(y_test, test_preds_pla, label, title="Confusion Matrix - PLA")

# %%
from sklearn.neural_network import MLPClassifier

mlp_clf = MLPClassifier(
    hidden_layer_sizes=(64, 32),
    activation="relu",
    solver='adam',
    max_iter=500,
    random_state=42
)

mlp_clf.fit(x_train, y_train)

# %%
from sklearn.metrics import classification_report, accuracy_score

predictions = mlp_clf.predict(x_test)
print("Accuracy:", accuracy_score(y_test, predictions))
print(classification_report(y_test, predictions))

# %%
plot_confusion_matrix(y_test, predictions, label, title="Confusion Matrix - PLA")


