/*

Charles Fried - 2017
ANN Tutorial
Part #2

NETWORK

This class is for the neural network, which is hard coded with three layers: input, hidden and output

*/

ArrayList connec = new ArrayList();
ArrayList conStr = new ArrayList();

class Network {

  Neuron [] input_layer;
  Neuron [] hidden_layer;
  Neuron [] output_layer;

  int w;
  int h;
  float wheelX = 0;
  float wheelY = 0;
  int bestIndex = 0;
  float steeringAverage =0;

  Network(int inputs, int hidden, int outputs) {

    input_layer = new Neuron [inputs];
    hidden_layer = new Neuron [hidden];
    output_layer = new Neuron [outputs];

    for (int i = 0; i < input_layer.length; i++) {
      PVector pos =  new PVector(networkW * 0.15, i * ( networkH / 2));
      input_layer[i] = new Neuron(pos);
    }

    for (int j = 0; j < hidden_layer.length; j++) {
      PVector pos =  new PVector(networkW * 0.5, j * ( networkH / 4));
      hidden_layer[j] = new Neuron(input_layer, pos);
    }

    for (int k = 0; k < output_layer.length; k++) {
      PVector pos =  new PVector(networkW * 0.85, k * ( networkH / 2));
      output_layer[k] = new Neuron(hidden_layer,pos);

    }
    // steering wheel possition
    wheelX = networkW * 0.85;
    wheelY = networkH / 2;
  }

  void respond(float[] inputs) {

    for (int i = 0; i < input_layer.length; i++) {
      input_layer[i].output = inputs[i];
    }
    //now feed forward through the hidden layer
    for (int j = 0; j < hidden_layer.length; j++) {
      hidden_layer[j].respond();
    }
    for (int k = 0; k < output_layer.length; k++) {
      output_layer[k].respond();
    }
  }

  void zeroAll() {
    for (int i = 0; i < hidden_layer.length; i++) {
      for (int j = 0; j < input_layer.length; j++) {
        hidden_layer[i].weights[j] = 0;
      }
    }
    for (int i = 0; i < output_layer.length; i++) {
      for (int j = 0; j < hidden_layer.length; j++) {
        output_layer[i].weights[j] = 0;
      }
    }
  }
  void randomAll() {
    for (int i = 0; i < hidden_layer.length; i++) {
      for (int j = 0; j < input_layer.length; j++) {
        hidden_layer[i].weights[j] = random(-1.0,1.0);
      }
    }
    for (int i = 0; i < output_layer.length; i++) {
      for (int j = 0; j < hidden_layer.length; j++) {
        output_layer[i].weights[j] = random(-1.0,1.0);
      }
    }
  }
  // void respond(Card card) {
  //
  //   for (int i = 0; i < input_layer.length; i++) {
  //     input_layer[i].output = card.inputs[i];
  //   }
  //   // now feed forward through the hidden layer
  //   for (int j = 0; j < hidden_layer.length; j++) {
  //     hidden_layer[j].respond();
  //   }
  //   for (int k = 0; k < output_layer.length; k++) {
  //     output_layer[k].respond();
  //   }
  // }

  void display() {
    pushMatrix();
    pushStyle();
    strokeWeight(1);
    translate(gameW,gameH/4);
    drawCon();
    // Draw the input layer
    for (int i = 0; i < input_layer.length; i++) {
      pushMatrix();
      translate(input_layer[i].x,  input_layer[i].y);
      input_layer[i].display();
      if (i%2 == 0) {
        text("Left Eye",-50, 5);
      } else {
        text("Right Eye",-50, 5);
      }
      popMatrix();
    }

    // Draw the hidden layer
    for (int j = 0; j < hidden_layer.length; j++) {
      pushMatrix();
      translate(hidden_layer[j].x,  hidden_layer[j].y);
      hidden_layer[j].display();
      popMatrix();
    }

    // Draw the output layer
    float [] resp = new float [output_layer.length];
    float respTotal = 0.0;
    for (int k = 0; k < output_layer.length; k++) {
      resp[k] = output_layer[k].output;
      respTotal += resp[k]+1;
    }
    float steering = 0;

    for (int k = 0; k < output_layer.length; k++) {
      pushMatrix();

      translate(output_layer[k].x,  output_layer[k].y);
      //normalise output
      if (output_layer[k].output<0) {
        output_layer[k].output = 0;
      }
      output_layer[k].display();
      fill(255);

      String percent = nfc(((output_layer[k].output+1)/respTotal)*100, 1);
      //  float steering = ((output_layer[k].output+1)/respTotal)-.5;
      float steerTemp = output_layer[k].output;
      //  if (steerTemp>0) {

      //  }
      textAlign(CORNER);
      if (k%2 == 0) {
        //  text("Left: "+percent + "%",60, 5);
        steering -= steerTemp/10;
        text("Left",30, 5);
      } else {
        //  text("Righ  t: "+percent + "%",60, 5);
        steering += steerTemp/10;
        text("Right",30, 5);
      }
      textAlign(CENTER);
      popMatrix();
      // strokeWeight(1);
    }
    steering = steering/2;
    steeringAverage = steeringAverage*.6;
    steeringAverage += steering*.4;
    gameCar.steer(steeringAverage);

    //(gameCar.updateAngle();
    //
    float best = -1.0;
    for (int i =0; i < resp.length; i++) {
      if (resp[i]>best) {
        best = resp[i];
        bestIndex = i;
      }
    }
    stroke(255, 0, 0);
    noFill();
    // indicate output
    //  ellipse(
    //    width * 0.85, (bestIndex%10) * height / 15.0 + height * 0.2,
    //  25, 25);

    popStyle();
    popMatrix();
    pushMatrix();
    //steering wheel
    fill(255);
    translate(gameW+wheelX+60,gameH/2);
    rotate(steeringAverage*20);
    ellipse(0,0,50,50);
    strokeWeight(2);
    stroke(0);
    line(0,0,25,0);
    popMatrix();
  }

  // void train(float [] outputs) {
  //   // adjust the output layer
  //   for (int k = 0; k < output_layer.length; k++) {
  //     output_layer[k].setError(outputs[k]);
  //     output_layer[k].train();
  //   }
  //   float best = -1.0;
  //   for (int i = 0; i < output_layer.length; i++) {
  //     if (output_layer[i].output > best) bestIndex = i;
  //   }
  //   // propagate back to the hidden layer
  //   for (int j = 0; j < hidden_layer.length; j++) {
  //     hidden_layer[j].train();
  //   }

  // The input layer doesn't learn: it is the input and only that
  //  }

  void drawCon() {

    for (int i = 0; i < hidden_layer.length; i++) {
      for (int j = 0; j < input_layer.length; j++) {
        float weight = hidden_layer[i].weights[j];
        stroke(255);
        strokeWeight(pow(10, abs(weight))/10);
        line(input_layer[j].x,input_layer[j].y,hidden_layer[i].x,hidden_layer[i].y);
        //  PVector midPoint = new PVector((input_layer[j].x+hidden_layer[i].x)*.5,(input_layer[j].y+hidden_layer[i].y)*.5);
        PVector midPoint = new PVector((hidden_layer[i].x-input_layer[j].x),(hidden_layer[i].y-input_layer[j].y));
        midPoint.mult(.3);
        midPoint.add(input_layer[j].x,input_layer[j].y);
        //ellipse(midPoint.x,midPoint.y,4,4);
        hidden_layer[i].weights[j] = weightDisplay(midPoint.x,midPoint.y,weight);
      }
    }

    for (int i = 0; i < output_layer.length; i++) {
      for (int j = 0; j < hidden_layer.length; j++) {
        float weight = output_layer[i].weights[j];
        stroke(255);
        strokeWeight(pow(10, abs(weight))/10);
        line(hidden_layer[j].x,hidden_layer[j].y,output_layer[i].x,output_layer[i].y);
        //  find location to place slider
        PVector midPoint = new PVector((output_layer[i].x-hidden_layer[j].x),(output_layer[i].y-hidden_layer[j].y));
        midPoint.mult(.6);
        midPoint.add(hidden_layer[j].x,hidden_layer[j].y);
        // update weight
        output_layer[i].weights[j] = weightDisplay(midPoint.x,midPoint.y,weight);
      }
    }
    strokeWeight(1);
  }

  float weightDisplay(float x, float y, float weight) {
    noStroke();
    float newWeight;
    int width = 55;
    int height = 15;
    y -= height/2;
    x -= width/2;
    pushMatrix();

    translate(x,y);
    x+= gameW;
    y+= gameH/4;
    fill(0,45,90);
    //&& mousePressed
    rect(0,0,55,15);
    if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
      if (mousePressed) {
        weight = map(mouseX-x, 0,width, -1.0,1.0);
      }
      fill(0, 116, 217);
      float length = map(weight,-1.0,1.0, 0, 55);
      rect(0,0,length,15);
    }
    fill(255);
    //  rectMode(CORNER);
    text("w: "+nfc(weight,2),5,0,45,15);
    popMatrix();
    newWeight = weight;
    return newWeight;
  }

}
