p5.disableFriendlyErrors = true;
let N_PARTICLES = 50;
let particles = [];
let grid;
function setup() {
  createCanvas(windowWidth, windowHeight);
  grid = new Grid(windowWidth, windowHeight, 22);
  for (let n = 0; n < N_PARTICLES; n++) {
    let particle = new Particle(new createVector(random(windowWidth), random(windowHeight)))
    particles.push(particle);
    grid.addParticle(particle);
  }
}
function draw() {
  background(220);
  if (mouseIsPressed) {
    for(let n = 0; n < 5; n++){
			let particle = new Particle(new createVector(mouseX+random(-1,1), mouseY+random(-1,1)))
    	particles.push(particle);
    	grid.addParticle(particle);
		}
  }
  textSize(30)
  textAlign(LEFT, TOP)
  let numCollisionChecks = 0
  let startneigh = performance.now();
  for (let p of particles) {
    let neighbors = grid.getNeighbors(p)
    numCollisionChecks += p.checkCollision(neighbors)
		p.updateState();
  }
  let endneigh = performance.now();
  let coll = `${numCollisionChecks} collisions computed in ${ endneigh - startneigh} ms`
  let startup = Date.now();
  for (let p of particles) {
		p.checkEdges();
    grid.removeParticle(p)
    grid.addParticle(p)
  }
  let endup = Date.now();
  let up = 'Grid updated in ' + round(endup - startup, 2) + ' ms'
	noFill()
  let startd = Date.now();
  for (let p of particles) {
    p.display();
  }
  let endd = Date.now();
  let dr = 'Draw in ' + round(endd - startd, 2) + ' ms'
  fill(0)
  text('Particles: ' + particles.length, 10, 10)
  text(coll, 10, 50);
  text(up, 10, 90);
  text(dr, 10, 130);
}
class Particle {
  constructor(pos) {
    this.pos = pos;
    this.velocity = createVector(random(-1.25, 1.25), random(-1.25, 1.25));
    this.acceleration = createVector(0, 0);
    this.mass = random(3, 10)
    this.radius = this.mass;
    this.maxSpeed = 10;
  }
  updateState(newPos) {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxSpeed); // Limit the particle's speed
    this.pos.add(this.velocity);
  }
  checkEdges() {
    if (this.pos.x - this.radius < 0) {
      this.pos.x = this.radius; // Prevent from leaving the canvas from the left side
      this.velocity.x *= -1;
    } else if (this.pos.x + this.radius > width) {
      this.pos.x = width - this.radius; // Prevent from leaving the canvas from the right side
      this.velocity.x *= -1;
    }
    if (this.pos.y - this.radius < 0) {
      this.pos.y = this.radius; // Prevent from leaving the canvas from the top
      this.velocity.y *= -1;
    } else if (this.pos.y + this.radius > height) {
      this.pos.y = height - this.radius; // Prevent from leaving the canvas from the bottom
      this.velocity.y *= -1;
    }
  }
  checkCollision(otherParticles) {
		let counter = 0
    for (let other of otherParticles) {
      if (this != other) {
        let distance = this.pos.dist(other.pos);
        let minDistance = this.radius + other.radius + 1;
        if (distance <= minDistance) {
          // Calculate collision response
          let normal = p5.Vector.sub(other.pos, this.pos).normalize();
          let relativeVelocity = p5.Vector.sub(other.velocity, this.velocity);
          let impulse = p5.Vector.mult(normal, 2 * p5.Vector.dot(relativeVelocity, normal) / 2);
          // Apply repulsion force to prevent sticking
          let repulsion = p5.Vector.mult(normal, minDistance - distance + 2).mult(1);
          // Update velocities
          this.velocity.add(p5.Vector.div(impulse, this.mass));
          other.velocity.sub(p5.Vector.div(impulse, other.mass));
          // Apply repulsion force
          this.pos.sub(p5.Vector.div(repulsion, this.mass));
          other.pos.add(p5.Vector.div(repulsion, other.mass));
        }
				counter++
      }
    }
		return counter
  }
  display() {
    ellipse(this.pos.x, this.pos.y, this.radius * 2);
  }
}
/*
  The lookup grid
*/
class Grid {
  constructor(i, t, s) {
    (this.cellSize = s),
    (this.numCols = Math.ceil(i / s)),
    (this.numRows = Math.ceil(t / s)),
    (this.cells = []);
    for (let e = 0; e < this.numCols; e++) {
      this.cells[e] = [];
      for (let l = 0; l < this.numRows; l++) {
        this.cells[e][l] = [];
      }
    }
  }
  addParticle(i) {
    let t = Math.floor(i.pos.x / this.cellSize);
    let s = Math.floor(i.pos.y / this.cellSize);
    this.cells[t][s].push(i)
    i.gridCell = {
      col: t,
      row: s
    }
  }
  removeParticle(i) {
    let {
      col: t,
      row: s
    } = i.gridCell
    let e = this.cells[t][s];
    let l = e.indexOf(i);
    e.splice(l, 1);
  }
  determineCell(i) {
    let t = Math.floor(i.pos.x / this.cellSize);
    let s = Math.floor(i.pos.y / this.cellSize);
    return {
      col: t,
      row: s
    }
  }
  getNeighbors(particle) {
    let top_left = [
      floor((particle.pos.x - particle.radius) / this.cellSize),
      floor((particle.pos.y - particle.radius) / this.cellSize),
    ]
    let bottom_right = [
      floor((particle.pos.x + particle.radius) / this.cellSize),
      floor((particle.pos.y + particle.radius) / this.cellSize),
    ]
    let neighbors = []
    for (let i = top_left[0]; i <= bottom_right[0]; i++) {
      for (let j = top_left[1]; j <= bottom_right[1]; j++) {
        if (i < 0 || j < 0 || i >= this.numCols || j >= this.numRows) continue
        let c = this.cells[i][j]
        for (let p of c) {
          // don't add the particle itself
          if (p != particle) neighbors.push(p)
        }
      }
    }
    return neighbors
  }
}