#include "tensor.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

void add_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->size; i++) {
        result_data[i] = tensor1->data[i] + tensor2->data[i];
    }
}

void add_broadcasted_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data, int* broadcasted_shape, int broadcasted_size) {
    int max_ndim = tensor1->ndim > tensor2->ndim ? tensor1->ndim : tensor2->ndim;

    // Calculate strides for broadcasting
    int* strides1 = (int*)malloc(max_ndim * sizeof(int));
    int* strides2 = (int*)malloc(max_ndim * sizeof(int));
    if (strides1 == NULL || strides2 == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        exit(1);
    }

    int stride1 = 1, stride2 = 1;
    for (int i = max_ndim - 1; i >= 0; i--) {
        int dim1 = i < tensor1->ndim ? tensor1->shape[tensor1->ndim - max_ndim + i] : 1;
        int dim2 = i < tensor2->ndim ? tensor2->shape[tensor2->ndim - max_ndim + i] : 1;
        strides1[i] = dim1 == broadcasted_shape[i] ? stride1 : 0;
        strides2[i] = dim2 == broadcasted_shape[i] ? stride2 : 0;
        stride1 *= (dim1 == broadcasted_shape[i]) ? dim1 : 1;
        stride2 *= (dim2 == broadcasted_shape[i]) ? dim2 : 1;
    }

    // Perform element-wise addition with broadcasting
    for (int i = 0; i < broadcasted_size; i++) {
        int index1 = 0, index2 = 0;
        int linear_index = i;
        for (int j = max_ndim - 1; j >= 0; j--) {
            int pos = linear_index % broadcasted_shape[j];
            linear_index /= broadcasted_shape[j];
            if (strides1[j] != 0) index1 += pos * strides1[j];
            if (strides2[j] != 0) index2 += pos * strides2[j];
        }
        result_data[i] = tensor1->data[index1] + tensor2->data[index2];
    }

    // Free strides
    free(strides1);
    free(strides2);
}


void sub_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->size; i++) {
        result_data[i] = tensor1->data[i] - tensor2->data[i];
    }
}

void sub_broadcasted_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data, int* broadcasted_shape, int broadcasted_size) {
    int max_ndim = tensor1->ndim > tensor2->ndim ? tensor1->ndim : tensor2->ndim;

    // Calculate strides for broadcasting
    int* strides1 = (int*)malloc(max_ndim * sizeof(int));
    int* strides2 = (int*)malloc(max_ndim * sizeof(int));
    if (strides1 == NULL || strides2 == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        exit(1);
    }

    int stride1 = 1, stride2 = 1;
    for (int i = max_ndim - 1; i >= 0; i--) {
        int dim1 = i < tensor1->ndim ? tensor1->shape[tensor1->ndim - max_ndim + i] : 1;
        int dim2 = i < tensor2->ndim ? tensor2->shape[tensor2->ndim - max_ndim + i] : 1;
        strides1[i] = dim1 == broadcasted_shape[i] ? stride1 : 0;
        strides2[i] = dim2 == broadcasted_shape[i] ? stride2 : 0;
        stride1 *= (dim1 == broadcasted_shape[i]) ? dim1 : 1;
        stride2 *= (dim2 == broadcasted_shape[i]) ? dim2 : 1;
    }

    // Perform element-wise addition with broadcasting
    for (int i = 0; i < broadcasted_size; i++) {
        int index1 = 0, index2 = 0;
        int linear_index = i;
        for (int j = max_ndim - 1; j >= 0; j--) {
            int pos = linear_index % broadcasted_shape[j];
            linear_index /= broadcasted_shape[j];
            if (strides1[j] != 0) index1 += pos * strides1[j];
            if (strides2[j] != 0) index2 += pos * strides2[j];
        }
        result_data[i] = tensor1->data[index1] - tensor2->data[index2];
    }

    // Free strides
    free(strides1);
    free(strides2);
}

void elementwise_mul_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->size; i++) {
        result_data[i] = tensor1->data[i] * tensor2->data[i];
    }
}

void scalar_mul_tensor_cpu(Tensor* tensor, float scalar, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = scalar * tensor->data[i];
    }
}

void scalar_div_tensor_cpu(float scalar, Tensor* tensor, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = scalar / tensor->data[i];
    }
}

void tensor_div_scalar_cpu(Tensor* tensor, float scalar, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = tensor->data[i] / scalar;
    }
}

void tensor_div_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->size; i++) {
        result_data[i] = tensor1->data[i] / tensor2->data[i];
    }
}

void matmul_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->shape[0]; i++) {
        for (int j = 0; j < tensor2->shape[1]; j++) {
            float sum = 0.0;
            for (int k = 0; k < tensor1->shape[1]; k++) {
                sum += tensor1->data[i * tensor1->shape[1] + k] * tensor2->data[k * tensor2->shape[1] + j];
            }
            result_data[i * tensor2->shape[1] + j] = sum;
        }
    }
}


void broadcasted_batched_matmul_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {

    int result_data_stride = tensor1->shape[0] * tensor2->shape[2];

    for (int batch = 0; batch < tensor2->shape[0]; batch++) {
    
        for (int i = 0; i < tensor1->shape[0]; i++) {
            for (int j = 0; j < tensor2->shape[2]; j++) {
                float sum = 0.0;
                for (int k = 0; k < tensor1->shape[1]; k++) {
                    sum += tensor1->data[i * tensor1->shape[1] + k] * tensor2->data[batch*tensor2->strides[0] + (k * tensor2->shape[2] + j)];
                }
                result_data[(batch * result_data_stride) + (i * tensor2->shape[2] + j)] = sum;
            }
        }
    }
} 

void batched_matmul_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    int result_data_stride = tensor1->shape[1] * tensor2->shape[2];

    for (int batch = 0; batch < tensor2->shape[0]; batch++) {
    
        for (int i = 0; i < tensor1->shape[1]; i++) {
            for (int j = 0; j < tensor2->shape[2]; j++) {
                float sum = 0.0;
                for (int k = 0; k < tensor1->shape[2]; k++) {
                    sum += tensor1->data[(batch * tensor1->strides[0]) + i * tensor1->shape[2] + k] * tensor2->data[batch*tensor2->strides[0] + (k * tensor2->shape[2] + j)];
                }
                result_data[(batch * result_data_stride) + (i * tensor2->shape[2] + j)] = sum;
            }
        }
    }
} 

void scalar_pow_tensor_cpu(float base, Tensor* tensor, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = powf(base, tensor->data[i]);
    }
}

void tensor_pow_scalar_cpu(Tensor* tensor, float exponent, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = powf(tensor->data[i], exponent);
    }
}

void log_tensor_cpu(Tensor* tensor, float* result_data) {
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = logf(tensor->data[i]);
    }
}

void sum_tensor_cpu(Tensor* tensor, float* result_data, int size, int* result_shape, int axis) {
    if (axis == -1) {
        // Sum over all elements
        float sum = 0.0;
        for (int i = 0; i < tensor->size; i++) {
            sum += tensor->data[i];
        }
        *result_data = sum;
    } else {
        if (axis < 0 || axis >= tensor->ndim) {
            printf("Invalid axis");
            return;
        }
        
        int axis_stride = tensor->strides[axis];

        for (int i = 0; i < tensor->shape[axis]; i++) {
            for (int j = 0; j < size; j++) {
                int index = 0;
                int remainder = j;
                for (int k = tensor->ndim - 2; k >= 0; k--) {
                    index += (remainder % result_shape[k]) * tensor->strides[k < axis ? k : k + 1];     
                    remainder /= result_shape[k];
                }
                result_data[j] += tensor->data[index + i * axis_stride];
            }
        }
    }
}

void max_tensor_cpu(Tensor* tensor, float* result_data, int size, int* result_shape, int axis) {
    if (axis == -1) {
        float max_value = -INFINITY;
        for (int i = 0; i < tensor->size; i++) {
            max_value = fmax(max_value, tensor->data[i]);
        }
        *result_data = max_value;
    } else {
        for (int i = 0; i < size; i++) {
            result_data[i] = -INFINITY;
        }
        if (axis < 0 || axis >= tensor->ndim) {
            printf("Invalid axis");
            return;
        }
        
        int axis_stride = tensor->strides[axis];

        for (int i = 0; i < tensor->shape[axis]; i++) {
            for (int j = 0; j < size; j++) {
                int index = 0;
                int remainder = j;
                for (int k = tensor->ndim - 2; k >= 0; k--) {
                    index += (remainder % result_shape[k]) * tensor->strides[k < axis ? k : k + 1];     
                    remainder /= result_shape[k];
                }
                result_data[j] = fmax(result_data[j], tensor->data[index + i * axis_stride]);
            }
        }
    }
}

void min_tensor_cpu(Tensor* tensor, float* result_data, int size, int* result_shape, int axis) {
    if (axis == -1) {
        float min_value = INFINITY;
        for (int i = 0; i < tensor->size; i++) {
            min_value = fmin(min_value, tensor->data[i]);
        }
        *result_data = min_value;
    } else {
        for (int i = 0; i < size; i++) {
            result_data[i] = INFINITY;
        }
        if (axis < 0 || axis >= tensor->ndim) {
            printf("Invalid axis");
            return;
        }
        
        int axis_stride = tensor->strides[axis];

        for (int i = 0; i < tensor->shape[axis]; i++) {
            for (int j = 0; j < size; j++) {
                int index = 0;
                int remainder = j;
                for (int k = tensor->ndim - 2; k >= 0; k--) {
                    index += (remainder % result_shape[k]) * tensor->strides[k < axis ? k : k + 1];     
                    remainder /= result_shape[k];
                }
                result_data[j] = fmin(result_data[j], tensor->data[index + i * axis_stride]);
            }
        }
    }
}

void equal_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data) {
    
    for (int i = 0; i < tensor1->size; i++) {
        result_data[i] = (tensor1->data[i] == tensor2->data[i]) ? 1.0f : 0.0f;
    }
}

void equal_broadcasted_tensor_cpu(Tensor* tensor1, Tensor* tensor2, float* result_data, int* broadcasted_shape, int broadcasted_size) {
    int max_ndim = tensor1->ndim > tensor2->ndim ? tensor1->ndim : tensor2->ndim;

    // Calculate strides for broadcasting
    int* strides1 = (int*)malloc(max_ndim * sizeof(int));
    int* strides2 = (int*)malloc(max_ndim * sizeof(int));
    if (strides1 == NULL || strides2 == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        exit(1);
    }

    int stride1 = 1, stride2 = 1;
    for (int i = max_ndim - 1; i >= 0; i--) {
        int dim1 = i < tensor1->ndim ? tensor1->shape[tensor1->ndim - max_ndim + i] : 1;
        int dim2 = i < tensor2->ndim ? tensor2->shape[tensor2->ndim - max_ndim + i] : 1;
        strides1[i] = dim1 == broadcasted_shape[i] ? stride1 : 0;
        strides2[i] = dim2 == broadcasted_shape[i] ? stride2 : 0;
        stride1 *= (dim1 == broadcasted_shape[i]) ? dim1 : 1;
        stride2 *= (dim2 == broadcasted_shape[i]) ? dim2 : 1;
    }

    // Perform element-wise equal with broadcasting
    for (int i = 0; i < broadcasted_size; i++) {
        int index1 = 0, index2 = 0;
        int linear_index = i;
        for (int j = max_ndim - 1; j >= 0; j--) {
            int pos = linear_index % broadcasted_shape[j];
            linear_index /= broadcasted_shape[j];
            if (strides1[j] != 0) index1 += pos * strides1[j];
            if (strides2[j] != 0) index2 += pos * strides2[j];
        }
        result_data[i] = (tensor1->data[index1] == tensor2->data[index2]) ? 1.0f : 0.0f;
    }

    // Free strides
    free(strides1);
    free(strides2);
}


void ones_like_tensor_cpu(Tensor* tensor, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = 1.0;
    }
}

void zeros_like_tensor_cpu(Tensor* tensor, float* result_data) {
    
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = 0.0;
    }
}

void transpose_1D_tensor_cpu(Tensor* tensor, float* result_data) {

    for (int i = 0; i < tensor->shape[0]; i++) {
        result_data[i] = tensor->data[i];
    }
}

void transpose_2D_tensor_cpu(Tensor* tensor, float* result_data) {
    int rows = tensor->shape[0];
    int cols = tensor->shape[1];

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            result_data[j * rows + i] = tensor->data[i * cols + j];
        }
    }
}

void transpose_3D_tensor_cpu(Tensor* tensor, float* result_data) {
    int batch = tensor->shape[0];
    int rows = tensor->shape[1];
    int cols = tensor->shape[2];

    for (int i = 0; i < batch; i++) {
        for (int j = 0; j < rows; j++) {
            for (int k = 0; k < cols; k++) {
                result_data[k * rows * batch + j * batch + i] = tensor->data[i * rows * cols + j * cols + k];
            }
        }
    }
}

void assign_tensor_cpu(Tensor* tensor, float* result_data) {

    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = tensor->data[i];
    }
}

void make_contiguous_tensor_cpu(Tensor* tensor, float* result_data, int* new_strides) {
    
    for (int i = 0; i < tensor->size; i++) {
        int index = 0;
        int offset = i;
        for (int j = 0; j < tensor->ndim; j++) {
            index += (offset / new_strides[j]) * tensor->strides[j];
            offset %= new_strides[j];
        }
        result_data[i] = tensor->data[index];
    }

    // Free old data and update tensor properties
    free(tensor->data);
    free(tensor->strides);
    tensor->data = result_data;
    tensor->strides = new_strides;
}

void sin_tensor_cpu(Tensor* tensor, float* result_data) {
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = sinf(tensor->data[i]);
    }
}

void sigmoid_tensor_cpu(Tensor* tensor, float* result_data) {
    for (int i = 0; i < tensor->size; i++) {
        // avoid overflow
        if (tensor->data[i] >= 0) {

            float z = expf(-tensor->data[i]);
            result_data[i] = 1 / (1 + z);

        } else {

            float z = expf(tensor->data[i]);
            result_data[i] = z / (1 + z);
        }
    }
}

void cos_tensor_cpu(Tensor* tensor, float* result_data) {
    for (int i = 0; i < tensor->size; i++) {
        result_data[i] = cosf(tensor->data[i]);
    }
}
