ArrayList<Tile> tiles = new ArrayList<Tile>();
ArrayList<Tile> moving = new ArrayList<Tile>();
boolean hasWon = false;
boolean allValidPositions = false;
void setup() {
  size(480, 800);
  int tileSize = 80;

  for (int y = 0; y < height; y+=tileSize) {
    for (int x = 0; x < width; x+=tileSize) {
      //println("[" + x + "," + y + "]: " + randomRotation);
      Tile tmp = new Tile(x, y, tileSize);
      tmp.shuffle();
      tiles.add(tmp);
    }
  }
  noStroke();
}
void draw() {
  background(128, 128, 128);
  allValidPositions = true;
  for (Tile tile : tiles) {
    if (!hasWon) {
      tile.update();
    }
    tile.draw();
    if (tile.getRotation() != 0) {
      allValidPositions = false;
    }
  }
  if (allValidPositions) {
    hasWon = true;
    textSize(48);
    String message = "You Win!";
    float offset = textWidth(message);
    float ascent = textAscent();
    fill(255, 255, 255);
    stroke(0, 0, 0);
    rect(width / 4, (height / 2) - ascent * 2, width / 2, ascent *2);
    fill(0, 0, 0);
    text(message, (width - offset) / 2, (height - ascent) / 2);
  }
}

void shuffleTiles() {
  for (Tile tile : tiles) {
    tile.shuffle();
  }
}

void mouseReleased() {
  if (allValidPositions && hasWon) {
    allValidPositions = false;
    hasWon = false;
    shuffleTiles();
  } else {

    for (Tile tile : tiles) {
      if (tile.isHovering(mouseX, mouseY)) {
        tile.setRotationTarget(tile.getRotationTarget() + 90);
        moving.add(tile);
      }
    }

    for (Tile tile : moving) {
      tiles.remove(tile);
      tiles.add(tile);
    }
    moving.clear();
  }
}

class Tile {
  private PVector position = new PVector();
  private PVector size = new PVector();
  private color tileColor;
  private int rotation;
  private int rotationTarget;
  private color lineColor;
  private int rotationSpeed;

  public Tile(int x, int y, int tileSize) {
    this.position.x = x;
    this.position.y = y;
    this.size.x = tileSize;
    this.size.y = tileSize;
    this.tileColor = color(255, 255, 255);
    this.rotation = 0;
    this.rotationTarget = 0;
    this.lineColor = color(255, 255, 0);
    this.rotationSpeed = 5;
  }
  public void update() {
    if (this.isHovering(mouseX, mouseY)) {
      this.setColor(color(0, 255, 0));
    } else {
      this.tileColor = color(100, 100, 100);
    }
    if (this.isAnimating()) {
      this.rotation += this.rotationSpeed;
      this.tileColor = color(255, 0, 0);
    }
  }

  public void draw() {
    noStroke();
    fill(this.tileColor);
    pushMatrix();
    translate(this.position.x + this.size.x / 2, this.position.y + this.size.y / 2);
    if (this.rotation >= 360) {
      this.rotation = 0;
    }
    rotate(this.rotation * (PI/180));
    rect(-this.size.x / 2, -this.size.y / 2, this.size.x, this.size.y);
    stroke(this.lineColor);
    line(0, 0, this.size.x / 2, 0);
    popMatrix();
  }

  public boolean isHovering(int mouseX, int mouseY) {
    if (mouseX >= this.position.x && mouseX < this.position.x + this.size.x) {
      //hovering within x
      if (mouseY >= this.position.y && mouseY < this.position.y + this.size.y) {
        return true;
      }
    }
    return false;
  }

  public void setColor(int newColor) {
    this.tileColor = newColor;
  }

  public void addRotation(int newRotation) {
    this.rotation += newRotation;
  }

  public void setRotationTarget(int newTarget) {
    this.rotationTarget = newTarget;
    while (this.rotationTarget >= 360) {
      this.rotationTarget -= 360;
      if (this.rotationTarget <= 0) {
        this.rotationTarget = 0;
      }
    }
  }

  public int getRotationTarget() {
    return this.rotationTarget;
  }

  public boolean isAnimating() {
    return this.rotation != this.rotationTarget;
  }

  public int getRotation() {
    return this.rotation;
  }

  public void shuffle() {
    int randomRotation = 90 * (int)random(0, 4);
    this.rotation = randomRotation;
    this.rotationTarget = randomRotation;
  }
}
