console.clear()
  const Matrix = require('./Matrix')


  // Create the neural network
  class NeuralNetwork {
    constructor(input_nodes, hidden_nodes, output_nodes) {
      this.input_nodes = input_nodes;
      this.hidden_nodes = hidden_nodes;
      this.output_nodes = output_nodes;
  
      this.weights_ih = new Matrix(this.hidden_nodes, this.input_nodes);
      this.weights_ho = new Matrix(this.output_nodes, this.hidden_nodes);
      this.bias_h = new Matrix(this.hidden_nodes, 1);
      this.bias_o = new Matrix(this.output_nodes, 1);
    }
  
    train(input_array, target_array) {
      // Generating the Hidden Outputs
      let inputs = Matrix.fromArray(input_array);
      let hidden = this.weights_ih.multiply(inputs);
      hidden.add(this.bias_h);
      hidden.map(Matrix.sigmoid); // activation function

      // Generating the output"s output!
      let outputs = this.weights_ho.multiply(hidden);
      outputs.add(this.bias_o);
      outputs.map(Matrix.sigmoid); // activation function
  
      // // Convert array to matrix object
      let targets = Matrix.fromArray(target_array);
  
      // // Calculate the error
      // ERROR = TARGETS - OUTPUTS
      let output_errors = Matrix.subtract(targets, outputs);
  
      // // Calculate gradient
      let gradients = Matrix.map(outputs, Matrix.dsigmoid);
      gradients.multiply(output_errors);
      gradients.multiply(0.1);

      // // Calculate deltas
      let hidden_T = Matrix.transpose(hidden);
      let weights_ho_deltas = gradients.multiply(hidden_T);

      // // Adjust the weights by deltas
      this.weights_ho.add(weights_ho_deltas);
      // // Adjust the bias by its deltas (which is just the gradients)
      this.bias_o.add(gradients);

      // // Calculate the hidden layer errors
      let who_t = Matrix.transpose(this.weights_ho);
      let hidden_errors = who_t.multiply(output_errors);
  
      // // Calculate hidden gradient
      let hidden_gradient = Matrix.map(hidden, Matrix.dsigmoid);
      hidden_gradient.multiply(hidden_errors);
      hidden_gradient.multiply(0.1);
  
      console.log(`weights_ho`,this.weights_ho.data)
      console.log(`hidden`,hidden.data)
      console.log(`targets`,targets.data)
      console.log(`outputs`,outputs.data)
      console.log(`output_errors`,output_errors.data)
      console.log(`gradients`,gradients.data)
      console.log(`hidden_T`,hidden_T.data)
      console.log(`weights_ho_deltas`,weights_ho_deltas.data)
      console.log(`who_t`,who_t.data)
      console.log(`hidden_errors`,hidden_errors.data)
      console.log(`hidden_gradient`,hidden_gradient.data)

      // Calcuate input->hidden deltas
      let inputs_T = Matrix.transpose(inputs);
      let weights_ih_deltas = hidden_gradient.multiply(inputs_T);
  
      this.weights_ih.add(weights_ih_deltas);
      // Adjust the bias by its deltas (which is just the gradients)
      this.bias_h.add(hidden_gradient);
    }
  
    predict(input_array) {
      // Generating the Hidden Outputs
      let inputs = Matrix.fromArray(input_array);
      let hidden = this.weights_ih.multiply(inputs);
      hidden.add(this.bias_h);
      hidden.map(Matrix.sigmoid); // activation function
  
      // Generating the output"s output!
      let outputs = this.weights_ho.multiply(hidden);
      outputs.add(this.bias_o);
      outputs.map(Matrix.sigmoid); // activation function
  
      // Sending back to the caller!
      return outputs.toArray();
    }
  }
  
  // XOR problem data
  const trainingData = [
    // { input: [0, 0], output: [0] },
    { input: [0, 1], output: [0] },
    // { input: [1, 0], output: [1] },
    // { input: [1, 1], output: [0] }
  ];
  
  let nn = new NeuralNetwork(2, 2, 1);
  





  // Train the network
  for (let i = 0; i < 1; i++) {
    let data = trainingData[i % trainingData.length];
    nn.train(data.input, data.output);
  }
  
  // Test the network
  // console.log(nn.predict([0, 0]));
  // console.log(nn.predict([0, 1]));
  // console.log(nn.predict([1, 0]));
  // console.log(nn.predict([1, 1]));

// MATRIX CLASS
class Matrix {
    constructor(rows, cols) {
      this.rows = rows;
      this.cols = cols;
      this.data = Array.from({ length: rows }, () => Array.from({ length: cols }, () => Math.random() * 2 - 1));
    }
  
    static fromArray(arr) {      
      return new Matrix(arr.length, 1).map((value, row) => arr[row]);
    }
  
    toArray() {
      let arr = [];
      for (let i = 0; i < this.rows; i++) {
        for (let j = 0; j < this.cols; j++) {
          arr.push(this.data[i][j]);
        }
      }
      return arr;
    }
  
    static transpose(matrix) {
      let result = new Matrix(matrix.cols, matrix.rows);
      for (let i = 0; i < matrix.rows; i++) {
        for (let j = 0; j < matrix.cols; j++) {
          result.data[j][i] = matrix.data[i][j];
        }
      }
      return result; 
    }
  
    static subtract(a, b) {
      let result = new Matrix(a.rows, a.cols);
      for (let i = 0; i < a.rows; i++) {
        for (let j = 0; j < a.cols; j++) {
          result.data[i][j] = a.data[i][j] - b.data[i][j];
        }
      }
      return result;
    }
  
    add(other) {
      if (other instanceof Matrix) {
        if (this.rows !== other.rows || this.cols !== other.cols) {
          throw new Error("Sizes do not match.");
        }
        return this.map((value, i, j) => value + other.data[i][j]);
      } else {
        return this.map(value => value + other);
      }
    }
  
    multiply(other) {
      if (other instanceof Matrix) {
        if (this.cols !== other.rows) {
          throw new Error("Sizes do not match.");
        }
        return new Matrix(this.rows, other.cols)
            .map((_, i, j) => {
                let sum = 0;
                for (let k = 0; k < this.cols; k++) {
                    sum += this.data[i][k] * other.data[k][j];
                }
                return sum;
            });
      } else {
        return this.map(value => value * other);
      }
    }
  
    map(callback) {
      for (let i = 0; i < this.rows; i++) {
        for (let j = 0; j < this.cols; j++) {          
          this.data[i][j] = callback(this.data[i][j], i, j);
        }
      }
      return this;
    }
  
    forEach(callback) {
      for (let i = 0; i < this.rows; i++) {
        for (let j = 0; j < this.cols; j++) {
          callback(this.data[i][j], i, j);
        }
      }
    }
  
    static sigmoid(x) {
      return 1 / (1 + Math.exp(-x));
    }
  
    static dsigmoid(y) {
      // Return the derivative of the sigmoid function
      // Note: we"re assuming that y is already the result of a sigmoid function.
      return y * (1 - y);
    }
  
    static map(matrix, callback) {
      let result = new Matrix(matrix.rows, matrix.cols);
      for (let i = 0; i < matrix.rows; i++) {
        for (let j = 0; j < matrix.cols; j++) {
          result.data[i][j] = callback(matrix.data[i][j], i, j);
        }
      }
      return result;
    }
  }

module.exports = Matrix 

ERROR IN       hidden_gradient.multiply(hidden_errors);
      hidden_gradient.multiply(0.1);