#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "matrix.h"

// Reserviert dynamischen Speicher für eine Matrix der Größe rows x cols und initialisiert sie mit Nullen.
Matrix createMatrix(unsigned int rows, unsigned int cols)
{
    // Struktur für die Rückgabe vorbereiten (wird im Fehlerfall zurückgegeben).
    Matrix matrix;
    matrix.rows = 0;
    matrix.cols = 0;
    matrix.buffer = NULL;

    if (rows != 0 && cols != 0)
    {
        matrix.rows = rows;
        matrix.cols = cols;
        // calloc reserviert Speicher und initialisiert alle Werte mit 0 (gut für Matrizen).
        matrix.buffer = (MatrixType*) calloc((size_t)rows * cols, sizeof(MatrixType));

        if (matrix.buffer == NULL) {
            // Wenn malloc/calloc fehlschlägt, geben wir die Null-Matrix zurück.
            // Die Dimensionen sind bereits auf 0 gesetzt.
        }
        return matrix;
    }
    // Wenn Dimensionen 0 sind, geben wir die initialisierte Null-Matrix zurück.
    return matrix;
}

// Gibt den dynamisch reservierten Speicher der Matrix frei und setzt die Pointer auf NULL.
void clearMatrix(Matrix *matrix)
{
    if (matrix != NULL)
    {
        // Puffer nur freigeben, wenn er nicht NULL ist (Schutz vor double free).
        if (matrix->buffer != NULL) {
            free(matrix->buffer);
        }

        // Zustand auf 'leer' setzen, um spätere Fehler zu vermeiden.
        matrix->buffer = NULL;
        matrix->rows = 0;
        matrix->cols = 0;
    }
}

// Setzt den Wert an einer bestimmten Position (rowIdx, colIdx) in der Matrix.
void setMatrixAt(MatrixType value, Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{
    // WICHTIG: Die Matrix wird in Row-Major-Order gespeichert.
    // Index = Reihe * Anzahl der Spalten + Spalte.
    // Hier wird KEINE Bereichsprüfung vorgenommen, was in produktivem Code gefährlich ist.
    matrix.buffer[(size_t)rowIdx * matrix.cols + colIdx] = value;
}

// Gibt den Wert an einer bestimmten Position (rowIdx, colIdx) zurück.
MatrixType getMatrixAt(const Matrix matrix, unsigned int rowIdx, unsigned int colIdx)
{
    // Nur Zugriff, wenn die Indizes innerhalb des gültigen Bereichs liegen.
    if(rowIdx < matrix.rows && colIdx < matrix.cols){
        // Berechnung des flachen 1D-Indexes.
        return matrix.buffer[(size_t)rowIdx * matrix.cols + colIdx];
    }else{
        // Rückgabe des undefinierten Werts (0 in diesem Fall).
        return UNDEFINED_MATRIX_VALUE;
    }
}

// Addiert zwei Matrizen. Unterstützt elementweise Addition und Bias-Broadcasting.
Matrix add(const Matrix matrix1, const Matrix matrix2)
{
    // --- Case A: Elementweise Addition (gleiche Form) ---
    if (matrix1.rows == matrix2.rows && matrix1.cols == matrix2.cols)
    {
        Matrix result = createMatrix(matrix1.rows, matrix1.cols);
        if (result.buffer == NULL) return result; // Fehler bei Speicherreservierung.

        size_t n = (size_t)result.rows * result.cols;
        for (size_t i = 0; i < n; i++)
        {
            result.buffer[i] = matrix1.buffer[i] + matrix2.buffer[i];
        }
        return result;
    }

    // --- Case B: Bias-Broadcasting (Matrix + Vektor (rows x 1)) ---
    // Bias-Vektor (matrix2) wird über alle Spalten von matrix1 addiert.
    if (matrix1.rows == matrix2.rows && matrix2.cols == 1 && matrix1.cols > 1)
    {
        Matrix result = createMatrix(matrix1.rows, matrix1.cols);
        if (result.buffer == NULL) return result;

        for (unsigned int r = 0; r < matrix1.rows; r++) // Iteriere über Reihen
        {
            MatrixType b = getMatrixAt(matrix2, r, 0); // Hole den Bias-Wert für diese Reihe.
            for (unsigned int c = 0; c < matrix1.cols; c++) // Iteriere über Spalten
            {
                MatrixType val = getMatrixAt(matrix1, r, c);
                setMatrixAt(val + b, result, r, c);
            }
        }
        return result;
    }

    // --- Case C: Umgekehrtes Bias-Broadcasting (Vektor + Matrix) ---
    // (Wird im NN-Kontext oft nicht benötigt, aber der Vollständigkeit halber)
    if (matrix2.rows == matrix1.rows && matrix1.cols == 1 && matrix2.cols > 1)
    {
        Matrix result = createMatrix(matrix2.rows, matrix2.cols);
        if (result.buffer == NULL) return result;

        for (unsigned int r = 0; r < matrix2.rows; r++)
        {
            MatrixType b = getMatrixAt(matrix1, r, 0); // Hole den Bias-Wert (aus matrix1).
            for (unsigned int c = 0; c < matrix2.cols; c++)
            {
                MatrixType val = getMatrixAt(matrix2, r, c);
                setMatrixAt(val + b, result, r, c);
            }
        }
        return result;
    }

    // Wenn Formate nicht unterstützt werden.
    Matrix result;
    result.rows = 0;
    result.cols = 0;
    result.buffer = NULL;
    return result;
}

// Multipliziert zwei Matrizen (Standard Matrix-Matrix-Multiplikation).
Matrix multiply(const Matrix matrix1, const Matrix matrix2)
{
    // Voraussetzung: Spalten von matrix1 müssen gleich den Reihen von matrix2 sein.
    if (matrix1.cols != matrix2.rows)
    {
        Matrix result;
        result.rows = 0;
        result.cols = 0;
        result.buffer = NULL;
        return result;
    }

    // Ergebnis-Matrix hat Dimension: (matrix1.rows) x (matrix2.cols)
    Matrix result = createMatrix(matrix1.rows, matrix2.cols);
    if (result.buffer == NULL) return result; // Fehler bei Speicherreservierung.

    // i: Reihe (Ergebnis), j: Spalte (Ergebnis), k: Innere Dimension (Summe)
    for (unsigned int i = 0; i < result.rows; i++)
    {
        for (unsigned int j = 0; j < result.cols; j++)
        {
            MatrixType summe = 0;
            for (unsigned int k = 0; k < matrix1.cols; k++)
            {
                // Element (i, j) = Summe von (matrix1[i, k] * matrix2[k, j])
                summe += getMatrixAt(matrix1, i, k) * getMatrixAt(matrix2, k, j);
            }
            setMatrixAt(summe, result, i, j);
        }
    }
    return result;
}