//////// Reference 01
// IDEA 9101 - week 4 - example - Sending MQTT, by Luke Hespanhol
// A template used in IDEA lab, to allow p5.js sketch to send message via MQTT
////////

//////// Reference 02
// IDEA 9101 - week 8 - example by Luke Hespanhol
// Neural network training for classification
// We learn from the code about how to detect gesture duration and train model
////////

//////// Reference 03
// Ml5.js: Train your own neural network, by Daniel Shiffman
// We learn from the code about how to detect X Y positions and add musical notes
// https://thecodingtrain.com/Courses/ml5-beginners-guide/6.1-ml5-train-your-own.html
// https://youtu.be/8HEgeAbYphA
// https://editor.p5js.org/codingtrain/sketches/zwGahux8a
////////

//////// Reference 04
// Mist, by 平金凌 from openprocessing
// The example Draws many ellipses to simulate the fog effect
// We didn't use the original code directly, instead, we rearrange the original code into a class
// https://openprocessing.org/sketch/1573235
//////// 

//////// Reference 05
// Font: Anton, by Vernon Adams
// Retrieved March-2022 from: https://www.fontspace.com/swansea-font-f5873
//////// 

//////// Reference 06
// JAPAN o'clock, by Shio from openprocessing
// The example Uses particle system to create bouncing flowers, the pattern of flowers will change based on seasons
// We only use the flower's particle system of this example. 
// https://openprocessing.org/sketch/810159
//////// 

//////// Reference 07
// Easiest Gradient Effect by Kazuki Umeda from YouTube
// The example shows how to draw beautiful gradient effects 
// We use it in the ActionBubbleResult Class
// https://www.youtube.com/watch?v=-MUOweQ6wac
//////// 

//////// Inspiration 01
// Undertale firefly, by yetirom from openprocessing
// The example draws many yellow fireflies
// We didn't use the original code. 
// We learn from the logic and the visual output to create simple star effects
// https://openprocessing.org/sketch/834840
////////


document.addEventListener('touchstart', function (e) {
  document.documentElement.style.overflow = 'hidden';
});

document.addEventListener('touchend', function (e) {
  document.documentElement.style.overflow = 'auto';
});

//////////////////////////////////////////////////
//FIXED SECTION: DO NOT CHANGE THESE VARIABLES
//////////////////////////////////////////////////
var HOST = window.location.origin;
let xmlHttpRequest = new XMLHttpRequest();

////////////////////////////
// CLASS: CONTROLBUTTON
////////////////////////////
// A bespoke button to handle selection of categories
// and trogger training, during the 'collection' state.
// Example code starts from here
class ControlButton {
  constructor(label, x, y, w, h) {
    this.label = label;
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.clicked = false;
  }

  // Check if mouse is over the button
  isMouseOver() {
    return (
      state == "collection" &&
      abs(mouseX - this.x) < this.w / 2 &&
      abs(mouseY - this.y) < this.h / 2
    );
  }

  // Sets the button to clicked, and retuen its label
  click() {
    this.clicked = true;
    return this.label;
  }

  // Sets the button to unclicked
  unclick() {
    this.clicked = false;
  }

  // Draws the button to the screen
  display() {
    if (this.clicked) {
      fill(181, 80, 29);
    } else {
      fill(250, 168, 127);
    }
    rectMode(CENTER);
    rect(this.x, this.y, this.w, this.h);
    fill(255);
    textSize(40);
    textAlign(CENTER, CENTER);
    text(this.label, this.x, this.y);
  }
}

////////////////////////////////////////////////////////
// SKETCH VARIABLES
////////////////////////////////////////////////////////

// Icons and bubbles before training
let actionBubbles = [];
// Icons and bubbles after training
let actionBubbleResults = [];
// Images of 4 icons
let behaviors = [];
// Variables of four simple effects
let raindrops = [];
let stars = [];
let mists = [];
const NUM = 100;
let flowers = [];
let pNum = 15;

// Variables for training model
let model;
let targetLabel = ''; // The selected category, used for collection and prediction
let state = 'collection'; // State can be 'collection', 'training' and 'prediction'

// Control buttons
let behavior1;
let behavior2;
let behavior3;
let behavior4;
let trainButton;

// Variables to control the general logic
let collectingGesture = false;
let timeGestureStarted;
let gestureDuration;

// Preload variables
let font;
let bgImg;
let settings;

// Musical notes
// Refer to Reference 02, by Daniel Shiffman
// Adding musical notes
let notes = {
  C: 261.6256,
  D: 293.6648,
  E: 329.6276,
  F: 349.2282,
  G: 391.9954,
  A: 440.0000,
  B: 493.8833
}
let env, wave;


////////////////////////////
// PRELOAD FUNCTION
////////////////////////////
function preload() {
  settings = loadJSON('./settings/settings.json');
  console.log('settings file loaded');
  bgImg = loadImage("assets/BackgroundImg.png");
  for (let i = 0; i < 4; i++) {
    behaviors[i] = loadImage("assets/behavior" + i + ".png");
  }
}

////////////////////////////
// SETUP FUNCTION
////////////////////////////
function setup() {
  createCanvas(windowWidth, windowHeight);

  // Example code starts from here
  env = new p5.Envelope();
  env.setADSR(0.05, 0.1, 0.5, 1);
  env.setRange(1.2, 0);
  wave = new p5.Oscillator();

  wave.setType("sine");
  wave.start();
  wave.freq(440);
  wave.amp(env);

  // Font: Anton, by Vernon Adams, retrieved May-2022 from https://fonts.google.com/specimen/Anton
  font = loadFont('assets/Anton-Regular.ttf');

  // Set options used for collection or prediction
  let options = {
    inputs: ['x', 'y', 'gestureDuration'],
    outputs: ['label'],
    task: 'classification',
    debug: 'true',
  };
  model = ml5.neuralNetwork(options);

  // Load an existing model, if settings configures as such
  if (settings.loadExistingModel == "yes") {
    console.log('LOAD MODEL');
    const modelInfo = {
      model: 'model/model.json',
      metadata: 'model/model_meta.json',
      weights: 'model/model.weights.bin'
    }

    model.load(modelInfo, modelLoaded);
  }

  // Create the control buttons for training
  let buttonWidth = int(windowWidth / 5);
  let buttonHeight = int(windowHeight / 15);
  let firstButtonX = int(buttonWidth / 2.5);
  let buttonY = int(buttonHeight / 2);

  behavior1 = new ControlButton('Fog', firstButtonX, buttonY, buttonWidth, buttonHeight);
  behavior2 = new ControlButton('Rain', firstButtonX + buttonWidth, buttonY, buttonWidth, buttonHeight);
  behavior3 = new ControlButton('Flower', firstButtonX + 2 * buttonWidth, buttonY, buttonWidth, buttonHeight);
  behavior4 = new ControlButton('Star', firstButtonX + 3 * buttonWidth, buttonY, buttonWidth, buttonHeight);
  trainButton = new ControlButton('TRAIN', firstButtonX + 4 * buttonWidth, buttonY, buttonWidth, buttonHeight);
}


////////////////////////////
// DRAW FUNCTION
////////////////////////////
function draw() {
  // Draw background gradient color
  background(30);

  // Draw heading
  fill(255);
  textSize(105);
  textFont('Anton-Regular');
  textAlign(CENTER, CENTER);
  text('LIGHT THE CITY', width / 2, height * 0.2);
  stroke(255);
  strokeWeight(3);
  line(width * 0.2, height * 0.24, width * 0.8, height * 0.24);


  // Draw the background image
  tint(255, 70);
  imageMode(CORNER);
  image(bgImg, 0, 0, windowWidth, windowHeight);

  // Background is white in the prediction stage
  if (!collectingGesture && (state != 'prediction')) {
    background(255);
  }

  noStroke();

  // Only displays the buttons during collection
  if (state == 'collection') {
    behavior1.display();
    behavior2.display();
    behavior3.display();
    behavior4.display();
    trainButton.display();
  }

  // Render action bubbles 
  // These are icon bubbles in the collection stage
  for (let i = actionBubbles.length - 1; i >= 0; i--) {
    actionBubbles[i].display();
  }

  // Render result bubbles 
  // These are icon bubbles in the prediction stage
  for (let i = actionBubbleResults.length - 1; i >= 0; i--) {
    actionBubbleResults[i].update();
    actionBubbleResults[i].display();
    if (actionBubbleResults[i].isFinished()) {
      actionBubbleResults.splice(i, 1);
    }
  }

  // Render simple raindrop effect
  for (let i = raindrops.length - 1; i > -1; i--) {
    raindrops[i].update();
    if (raindrops[i].isDead()) {
      raindrops.splice(i, 1);
    }
  }

  // Render simple star effect  
  for (var i = stars.length - 1; i > -1; i--) {
    stars[i].update();
    stars[i].draw();
    if (stars[i].isDead()) {
      stars.splice(i, 1);
    }
  }

  // Render simple flower effect
  for (let i = flowers.length - 1; i > -1; i--) {
    flowers[i].display();
    if (flowers[i].isFinished()) {
      flowers.splice(i, 1);
    }
  }

  // Render simple mist effect
  for (let i = mists.length - 1; i > -1; i--) {
    mists[i].show();
    mists[i].move();
    if (mists[i].isFinished()) {
      mists.splice(i, 1);
    }
  }
}


////////////////////////////
// MODEL LOADED FUNCTION
////////////////////////////
// Callback invoked when the model loading is completed.
// In this case, no training is needed, so go straight
// into prediction.
// Example code about load and train neural network starts from here
function modelLoaded() {
  console.log('model loaded');
  state = 'prediction';
  // draw background when the state is prediction
  background(255);
}

function startTraining() {
  state = "training";
  console.log("starting training");
  model.normalizeData();
  let options = {
    epochs: 200,
  };
  model.train(options, whileTraining, finishedTraining);
  console.log("training completed");
}

////////////////////////////
// Callback invoked on each staep of training
////////////////////////////
function whileTraining(epoch, loss) {
  console.log(epoch);
}

////////////////////////////
// Callback invoked when training is completed
////////////////////////////
function finishedTraining() {
  console.log('finished training.');
  state = 'prediction';
  model.save();
}

////////////////////////////
// MOUSE PRESSED
////////////////////////////
// Toggle buttons accordingly or start training (during collection),
// and start collecting a gesture, if one not yet being collected.
// Example code starts from here
function mousePressed() {
  // Modify Luke's example code
  if (behavior1.isMouseOver()) {
    targetLabel = behavior1.click();
    behavior2.unclick();
    behavior3.unclick();
    behavior4.unclick();
    trainButton.unclick();
  } else if (behavior2.isMouseOver()) {
    behavior1.unclick();
    targetLabel = behavior2.click();
    behavior3.unclick();
    behavior4.unclick();
    trainButton.unclick();
  } else if (behavior3.isMouseOver()) {
    behavior1.unclick();
    behavior2.unclick();
    targetLabel = behavior3.click();
    behavior4.unclick();
    trainButton.unclick();
  } else if (behavior4.isMouseOver()) {
    behavior1.unclick();
    behavior2.unclick();
    behavior3.unclick();
    targetLabel = behavior4.click();
    trainButton.unclick();
  } else if (trainButton.isMouseOver()) {
    behavior1.unclick();
    behavior2.unclick();
    behavior3.unclick();
    behavior4.unclick();
    targetLabel = trainButton.click();
    startTraining();
  } else {
    // Collect gesture
    if (!collectingGesture) {
      startProcessingSingleData();
    }
  }

  // Input actionBubbles (fog, rain, flower, star)
  if (mouseY > (windowHeight / 15) + 50) {
    switch (targetLabel) {
      case "Fog":
        actionBubbles.push(new ActionBubble(mouseX, mouseY, 0, 50, 100, 100));
        break;
      case "Rain":
        actionBubbles.push(new ActionBubble(mouseX, mouseY, 1, 50, 100, 100));
        break;
      case "Flower":
        actionBubbles.push(new ActionBubble(mouseX, mouseY, 2, 50, 100, 100));
        break;
      case "Star":
        actionBubbles.push(new ActionBubble(mouseX, mouseY, 3, 50, 100, 100));
        break;
      default:
      //
    }
  }
}


////////////////////////////
// MOUSE RELEASED
////////////////////////////
// End processing the entering of a gesture
function mouseReleased() {
  endProcessingSingleData();
}

////////////////////////////
// If collection is being performed, then set up 
// variables to start collecting a new gesture
////////////////////////////
function startProcessingSingleData() {
  timeGestureStarted = millis();
  collectingGesture = true;
  minX = mouseX;
  maxX = minX;
  minY = mouseY;
  maxY = minY;
}

////////////////////////////
// If collection is being performed, then gather values 
// for each variable of interest, and add them to
// the model.
//
// Otherwoise, if prediction is being performed,
// then classify the data entered.
////////////////////////////
function endProcessingSingleData() {

  if (state == 'collection') {
    if (collectingGesture && targetLabel.trim() != "") {
      gestureDuration = int(millis() - timeGestureStarted);
    }
    let inputs = {
      x: mouseX,
      y: mouseY,
      gestureDuration: gestureDuration,
    };

    let target = {
      // global variable, read the current target label
      label: targetLabel,
    };


    // var jsonstr = JSON.stringify(inputs);
    // sendMessage(jsonstr); 
    // console.log("message jsonstr is sent to server: " + jsonstr);
    model.addData(inputs, target); //add data
    console.log(
      "Data added: targetLabel: " +
      targetLabel +
      ", gestureDuration: " +
      gestureDuration +
      ", x: " +
      mouseX +
      ", y: " +
      mouseY
    );

    // load sound
    // wave.freq(notes[targetLabel]);
    // env.play();

  } else if (state == "prediction") {
    // Detect the touch time
    gestureDuration = int(millis() - timeGestureStarted);
    console.log("timeGestureStarted:" + timeGestureStarted)
    let inputs = {
      x: mouseX,
      y: mouseY,
      gestureDuration: gestureDuration,
    }

    // Classify the inputs and get results
    model.classify(inputs, gotResults);

    // var jsonstr = JSON.stringify("x: " + inputs.x + ", y: " + inputs.y);
    // var gesture_Duration = JSON.stringify({gestureDuration}); 

    // Send gestureDuration's value to the server
    var gesture_Duration = "gesture_Duration," + inputs.gestureDuration;
    sendMessage(gesture_Duration);
    console.log("message gesture Duration is sent to server: " + gesture_Duration);

    console.log(
      "Data added: targetLabel: " +
      targetLabel +
      ", gestureDuration: " +
      gestureDuration +
      ", x: " +
      mouseX +
      ", y: " +
      mouseY
    );
  }
  collectingGesture = false;
}


////////////////////////////////////////////////////////
// ---> TRAINING FUNCTIONS
////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////
// Function called when starting training the model,
// once data collection is completed (in the case of 
// this example, when the user clicks the 'Train' button).
////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////
// ---> PREDICTION FUNCTIONS
////////////////////////////////////////////////////////

////////////////////////////////////////////////////////
// Callback invoiked when results from prediction
// are return.
////////////////////////////////////////////////////////
function gotResults(error, results) {

  if (error) {
    console.error(error);
    return;
  }
  console.log(results);

  let label = results[0].label;

  // Fog
  if (label == "Fog") {
    actionBubbleResults.push(new ActionBubbleResult(mouseX, mouseY, 0, 249, 255, 255, 106, 109, 110));
    // Display simple fot effect on the mobile screen
    addMist();

    // Send the label to the server
    // If the label is Fog, the fog effect will be displayed on the server interface
    var fog_Label = "fog_Label," + label;
    sendMessage(fog_Label);
    console.log("message Fog is sent to the server: " + fog_Label);
  }

  // Rain
  if (label == "Rain") {
    actionBubbleResults.push(new ActionBubbleResult(mouseX, mouseY, 1, 249, 255, 255, 0, 120, 171));
    // Render raindrop effect on the mobile screen
    addRaindrops();

    // Send MQTT messages
    var rain_Label = "rain_Label," + label;
    sendMessage(rain_Label);
    console.log("message Rain is sent to the server: " + rain_Label);
  }

  // Flower
  if (label == "Flower") {
    actionBubbleResults.push(new ActionBubbleResult(mouseX, mouseY, 2, 249, 255, 255, 140, 3, 136));
    // Render simple flower effect on the mobile screen
    addPetals();

    // Send the label to the server
    var flower_Label = "flower_Label," + label;
    sendMessage(flower_Label);
    console.log("message Flower is sent to the server: " + flower_Label);
  }

  // Star
  if (label == "Star") {
    actionBubbleResults.push(new ActionBubbleResult(mouseX, mouseY, 3, 249, 255, 255, 199, 140, 2));
    // Render simple star effect on the mobile screen
    addStar();

    // Send the label to the server
    var star_Label = "star_Label," + label;
    sendMessage(star_Label);
    console.log("message Star is sent to the server: " + star_Label);
  }

  // Add musical notes
  let tone = ''
  switch (label) {
    case "Fog":
      tone = 'a'
      break;
    case "Rain":
      tone = 'b'
      break;
    case "Flower":
      tone = 'c'
      break;
    case "Star":
      tone = 'd'
      break;
    default:

  }

  wave.freq(notes[tone]);
  env.play();
}

////////////////////////////
// FOUR SIMPLE EFFECTS
////////////////////////////

// Add raindrop function
// Call it in gotResult function
function addRaindrops() {
  // raindrops.push(new Rainfall());
  for (let i = 0; i < 10; i++) {
    raindrops[i] = new Rainfall();
  }
}

// Add star function
// Call it in gotResult function
function addStar() {
  for (var i = 0; i < 30; i++) {
    stars[i] = new Star();
  }
}
// Aadd petal(flower) function
// Call it in gotResult function
function addPetals() {
  for (let i = 0; i < pNum; i++) {
    p = new FloatingFlower();
    flowers.push(p);
  }
}

// Add mist function
// Call it in gotResult function
function addMist() {
  for (let i = 0; i < NUM; i++) {
    mists[i] = new Mist(random(width), random(height));
  }
}

////////////////////////////
// MIST CLASS
////////////////////////////
// Create the Mist class to provide better indication
// Draw many circles, and adjust their transparency to create the "mist" effect
// The inspiration comes from Reference 04 - by 平金凌
// We didn't use the original code directly, but organize it into a Class
// And add a lifespan value to make fog disappear within 2-4 seconds,
// and delete disappeared ellipses from array
class Mist {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = createVector(-2, 2);
    this.diameter = 500;
    this.lifespan = 4;
  }

  // Example move function
  move() {
    if (this.pos.x - 100 < 0 || this.pos.x + 100 > width) {
      this.vel.x *= -1;
    }
    if (this.pos.y + 100 < 0 || this.pos.y - 100 > height) {
      this.vel.y *= -1;
    }
  }

  show() {
    this.lifespan = this.lifespan - 0.04;
    noStroke();
    // Add the lifespan value (decreasing alpha value) to make them disappear
    fill(255, 255, 255, this.lifespan);
    circle(this.pos.x, this.pos.y, this.diameter);
    this.pos.add(this.vel);
  }

  // Check if fog disappear, if it is, delete from the array
  isFinished() {
    if (this.lifespan < 0) {
      return true;
    } else {
      return false;
    }
  }
}

////////////////////////////
// RAINFALL CLASS
////////////////////////////
// Simple rainfall effect, raindrops will disappear over time
class Rainfall {
  constructor() {
    this.xpos = random(0, width);
    this.ypos = random(0, height);
    this.diameter = random(20, 23);
    this.lifespan = 255;
  }

  update() {
    this.lifespan = this.lifespan - 3;
    this.xpos = this.xpos + this.diameter / 10; //wind
    this.ypos = this.ypos + this.diameter / 3; //speed

    // check the edge
    if (this.ypos >= height) {
      this.ypos = 0;
    }

    if (this.xpos >= width) {
      this.xpos = 0;
    }

    noStroke();
    fill(250, this.lifespan);
    ellipse(this.xpos, this.ypos, this.diameter, this.diameter);
  }
  // check if the raindrop still useful
  isDead() {
    if (this.lifespan < 0) {
      return true;
    } else {
      return false;
    }
  }
}


////////////////////////////
// FLOATING FLOWER CLASS
////////////////////////////
// The particle system of flowers refers to Reference 06 - JAPAN o' clock
// This class is also used in the server interface
// Example code starts from here
class FloatingFlower {

  constructor() {
    let size = Math.floor(min(windowWidth, windowHeight) * 0.9);
    // position
    this.x = random(width);
    this.y = random(height);
    this.yA = random(0.009);

    // rotation
    this.spinX = random(360);
    this.spinY = random(360);
    this.rot = random(360);
    this.setVelocity(1);

    // size
    this.size = size / 50 + random(size / 60);
    this.lifespan = 255;
  }

  // set the velocity
  setVelocity(s) {
    this.xV = ((random(1) - 0.5) * s) / 2;
    this.yV = (random(1) * -1 - 0.1) * s;

    this.spinXV = random(2) + 1;
    this.spinYV = random(2) + 1;
    this.rotV = random(1) + 0.5;
  }

  // display flowers
  display() {

    this.spinX += this.spinXV + abs(this.yV);
    this.spinY += this.spinYV + abs(this.yV);
    this.rot += this.rotV + abs(this.yV) * 2;
    this.x += this.xV;
    this.yV += this.yA;
    this.y += this.yV;
    this.lifespan = this.lifespan - 3;


    // display
    push();
    translate(this.x, this.y);
    scale(sin(radians(this.spinX)), sin(radians(this.spinY)));
    rotate(radians(this.rot));
    noStroke();
    for (let i = 0; i < 10; i++) {
      fill(255 - i * 5, 153, 255, this.lifespan);
      ellipse(0, 0, this.size, this.size);
    }
    pop();


    // check edges
    if (this.x >= width || this.x <= 0) {
      this.xV *= -1;
    }

    if (this.y <= 0) {
      this.yV *= -0.5;
      this.y += 1;
    }

    // stop bouncing when flowers are on the ground
    if (this.y >= height) {
      this.xV = 0;
      this.y = height;
      this.yV = 0;
      this.rotV = 0;
      this.spinXV = 0;
      this.spinYV = 0;
    }
  }
  // add this function to make floating flowers disappear over time
  isFinished() {
    if (this.lifespan < 0) {
      return true;
    } else {
      return false;
    }
  }
}

////////////////////////////
// STAR CLASS
////////////////////////////
// Create simple star effect to better indicate the 'star' icon is touched 
// and link the server interface better
// This is inspired by Inspiration 01 - Undertale firefly, by yetirom
// We only draw inspirations from its visual output, and didn't use the original code
class Star {
  // Initial values
  constructor() {
    this.x = random(0, width);
    this.y = random(0, height);
    this.size = random(8, 16);
    this.blink = random(8, 10);
    this.lifespan = 255;
    this.scale = scale;
    this.mx = random(1, 3 / 10);
    this.my = random(1, 3 / 10);
  }
  // Add an alpha value to make stars disappear over time
  update() {
    this.lifespan = this.lifespan - 3;
    this.x = this.x + this.mx;
    this.y = this.y + this.my;

    if (random(100) < 50) {
      this.mx = this.mx * -0.1;
      this.my = this.my * -0.1;
    }
  }

  // Display stars
  draw() {
    rectMode(CENTER);
    this.blink += 0.1;
    this.scale = this.size + sin(this.blink) * 2;
    fill(255, 204, 4, this.lifespan);
    noStroke();
    // rotate(45);
    fill(255, 204, 4, this.lifespan - 100);
    rect(this.x, this.y, this.scale + 5, this.scale + 5, 8);

    fill(255, 204, 4, this.lifespan);
    rect(this.x, this.y, this.scale, this.scale, 8);
  }

  // Check if stars disappear
  isDead() {
    if (this.lifespan < 0) {
      return true;
    } else {
      return false;
    }
  }
}

////////////////////////////
// ACTION BUBBLE CLASS
////////////////////////////
// These are visual graphics (icons) before training
class ActionBubble {

  // id is used for switch between icons
  // 0 and 1 are negative behaviors (fog and rain)
  // 2 and 3 are positive behaviors (flower and star)
  constructor(x, y, id, hue, saturation, brightness) {
    this.x = x;
    this.y = y;
    // to switch between images
    this.id = id; 
    this.hue = hue;
    this.saturation = saturation;
    this.brightness = brightness;

    this.size = random(150, 153);
    this.lifespan1 = 100;
    this.lifespan2 = 30;
  }

  display() {
    //size = random(97, 100);
    let scaleImg = 80;
    noStroke();

    // outside
    fill(this.hue, this.saturation, this.brightness, this.lifespan2);
    ellipse(this.x, this.y, this.size + 40, this.size + 40);

    // inside
    fill(this.hue, this.saturation, this.brightness, this.lifespan1);
    ellipse(this.x, this.y, this.size, this.size);

    // load icon
    imageMode(CENTER);
    tint(255, 255);
    image(behaviors[this.id], this.x, this.y, scaleImg, scaleImg);
  }
}

////////////////////////////
// ACTION BUBBLE RESULT CLASS
////////////////////////////
// These are visual graphics after training
class ActionBubbleResult {
  
  // id is used for switch between icons
  // 0 and 1 are negative behaviors (fog and rain)
  // 2 and 3 are positive behaviors (flower and star)
  constructor(x, y, id, innerR, innerG, innerB, outerR, outerG, outerB) {
    this.x = x;
    this.y = y;
    this.id = id;

    this.size = random(150, 153);
    this.lifespan = 255;
    this.scaleImg = 80;

    // Gradient color
    this.innerR = innerR;
    this.innerG = innerG;
    this.innerB = innerB;

    this.outerR = outerR;
    this.outerG = outerG;
    this.outerB = outerB;

  }

  // Update the value
  update() {
    this.x = this.x;
    this.y = this.y;

    this.size = this.size + 2;
    this.scaleImg = this.scaleImg + 1;

    // If the size is over 150, then make icon bubbles disappear
    if (this.size > 150) {
      this.lifespan = this.lifespan - 3;
    }
    this.color1 = color(this.innerR, this.innerG, this.innerB, this.lifespan); // inner
    this.color2 = color(this.outerR, this.outerG, this.outerB, this.lifespan); // outer

  }

  // Make gradient effect
  // Learn from Reference 07 - Easiest Gradient Effect by Kazuki Umeda
  // Adaption of example code starts from here
  radialGradient(sX, sY, sR, eX, eY, eR) {
    let gradient = drawingContext.createRadialGradient(sX, sY, sR, eX, eY, eR);
    gradient.addColorStop(0, this.color1);
    gradient.addColorStop(1, this.color2);
    drawingContext.fillStyle = gradient;
  }


  display() {
    noStroke();
    // let colorInside = color(255,204,4,this.lifespan);
    // let colorOutside = color(0,180,0,this.lifespan);
    this.radialGradient(this.x, this.y, 0, this.x, this.y, 250);
    ellipse(this.x, this.y, this.size, this.size);

    // load icon
    imageMode(CENTER);
    // adjust the lifespan (transparency) of image
    tint(255, this.lifespan);
    image(behaviors[this.id], this.x, this.y, this.scaleImg, this.scaleImg);
  }

  isFinished() {
    if (this.lifespan < 0) {
      return true;
    } else {
      return false;
    }
  }
}


/***********************************************************************
  === PLEASE DO NOT CHANGE OR DELETE THIS SECTION ===
  This function sends a MQTT message to server
  ***********************************************************************/
function sendMessage(userDatas) {
  let postData = JSON.stringify({ id: 1, 'message': userDatas });

  xmlHttpRequest.open("POST", HOST + '/sendMessage', false);
  xmlHttpRequest.setRequestHeader("Content-Type", "application/json");
  xmlHttpRequest.send(postData);
}
