<template>
  <canvas
    id="board-canvas"
    ref="board-canvas"
    :width="canvasWidth"
    :height="canvasHeight"
  />
  <canvas
    id="work-canvas"
    ref="work-canvas"
    :width="canvasWidth"
    :height="canvasHeight"
  />
</template>
<script>
  import { mapActions, mapState, mapWritableState } from "pinia";
  import { globalStore } from "../stores/index.js";
  import {
    create1dState,
    create1dStateOneCell,
    create2dState,
    createBoard,
    conwayRules,
    overpopulationRules,
    lonelinessRules,
    threebornRules,
    highLifeRules,
    servietteRules,
    evolve2d,
  } from "../modules/automata.js";
  import { getRandomInt, sleep } from "../modules/common.js";
  import { picToBoard, picToBlackAndWhite } from "../modules/picture.js";

  export default {
    name: "CanvasBoard",
    data() {
      return {
        canvas: null,
        workCanvas: null,
        workCtx: null,
        ctx: null,
        available2dRules: {
          conway: conwayRules,
          overpopulation: overpopulationRules,
          loneliness: lonelinessRules,
          threeborn: threebornRules,
          highlife: highLifeRules,
          serviette: servietteRules,
        },
      };
    },
    computed: {
      ...mapState(globalStore, {
        loop: "loop",
        cellProperties: "cellProperties",
        ruleset: "ruleset1d",
        refreshRate: "refreshRate",
        initial1dState: "initial1dState",
        drawingDirection: "drawingDirection",
        canDraw: "canDraw",
        getDraw1d: "draw1d",
        getDraw2d: "draw2d",
        getDraw2dLast: "draw2dLast",
        getDraw2dPicture: "draw2dpicture",
        boardWidth: "boardWidth",
        boardHeight: "boardHeight",
        selected2dRules: "selected2dRules",
      }),
      ...mapWritableState(globalStore, {
        lastBoard: "lastBoard",
        canvasWidth: "canvasWidth",
        canvasHeight: "canvasHeight",
        getReset: "reset",
        picture: "picture",
      }),
      // used to determine the dimensions of the board
      max() {
        return Math.max(this.boardWidth, this.boardHeight);
      },
      selectedRules() {
        return this.available2dRules[this.selected2dRules.id];
      },
    },
    watch: {
      getDraw1d(value) {
        if (value == true) this.draw1d();
      },
      getDraw2d(value) {
        if (value == true) this.draw2dNew();
      },
      getDraw2dLast(value) {
        if (value == true) this.draw2dLast();
      },
      getDraw2dPicture(value) {
        if (value == true) this.draw2dPicture();
      },
      getReset(value) {
        if (value == true) this.reset();
      },
    },
    mounted() {
      this.canvas = Object.freeze(document.getElementById("board-canvas"));
      this.workCanvas = Object.freeze(document.getElementById("work-canvas"));
      this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
      this.workCtx = this.workCanvas.getContext("2d", {
        willReadFrequently: true,
      });
      this.canvasWidth = this.canvas.parentElement.clientWidth;
      this.canvasHeight = this.canvas.parentElement.clientHeight;
      this.setBoardWidth();
      this.setBoardHeight();
    },
    methods: {
      ...mapActions(globalStore, [
        "toggleStop",
        "setBoardWidth",
        "setBoardHeight",
      ]),
      // draws the board on the canvas
      drawCanvas(board, width, height) {
        const d = this.cellProperties.size;
        // bool to RGBA colors
        const img = board.flat().reduce((acc, cell, index) => {
          const color = cell === 1 ? 0 : 255;
          const i = index * 4;
          acc.data[i] = color;
          acc.data[i + 1] = color;
          acc.data[i + 2] = color;
          acc.data[i + 3] = 255;
          return acc;
        }, new ImageData(width, height));
        // rescale and draw
        this.ctx.save();
        this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.workCtx.putImageData(img, 0, 0);
        this.ctx.imageSmoothingEnabled = false;
        if (this.getDraw2dPicture != true) this.ctx.scale(d, d);
        this.ctx.drawImage(
          this.workCanvas,
          0,
          0,
          this.canvasWidth,
          this.canvasHeight
        );
        this.ctx.restore();
      },
      // create a first state, either a single living cell
      // at the center or random ones
      compute1dInitialState() {
        if (this.initial1dState === "onecell")
          return create1dStateOneCell(this.boardWidth);
        return create1dState(this.boardWidth, getRandomInt, [0, 2]);
      },
      // initialize board with random cells
      randomInitialState() {
        return create2dState(
          this.boardWidth,
          this.boardHeight,
          getRandomInt,
          [0, 2]
        );
      },
      // draw elementary automaton on the canvas based on selected ruleset
      draw1d() {
        const initialState = this.compute1dInitialState();
        const board = createBoard(initialState, this.ruleset.rules, this.max);
        this.lastBoard = Object.freeze(board);
        this.drawCanvas(this.lastBoard, this.boardWidth, this.boardHeight);
        this.toggleStop();
      },
      // draw 2D automaton on the canvas in a loop
      draw2d(board) {
        const newBoard = evolve2d(board, this.selectedRules);
        this.drawCanvas(newBoard, this.boardWidth, this.boardHeight);
        this.lastBoard = Object.freeze(newBoard);
      },
      // draw 2d automaton in a loop, starting from passed state
      async draw2dNext(board) {
        requestAnimationFrame(() => {
          if (!this.canDraw) return;
          const newBoard = evolve2d(board, this.selectedRules);
          this.draw2d(board);
          this.lastBoard = Object.freeze(newBoard);
          /* await sleep(this.refreshRate) */
          return this.draw2dNext(newBoard);
        });
      },
      // draw 2d automaton from a new state
      async draw2dNew() {
        if (!this.canDraw) return;
        const initialState = this.randomInitialState();
        let board = evolve2d(initialState, conwayRules);
        if (this.loop) return this.draw2dNext(board);
        else this.draw2d(board);
        this.toggleStop();
      },
      // draw 2d automaton from the last known generated board
      async draw2dLast() {
        if (!this.canDraw) return;
        if (this.loop) return this.draw2dNext(this.lastBoard);
        else this.draw2d(this.lastBoard);
        this.toggleStop();
      },
      // draw 2d automaton from an uploaded picture.
      // use the picture representation as an initial state
      draw2dPicture() {
        // get image data by drawing it on a work canvas
        this.ctx.fillStyle = "black";
        this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.ctx.drawImage(
          this.picture,
          Math.floor((this.canvasWidth - this.picture.width) / 2),
          Math.floor((this.canvasHeight - this.picture.height) / 2),
          this.picture.width,
          this.picture.height
        );

        const imgData = this.ctx.getImageData(
          0,
          0,
          this.canvasWidth,
          this.canvasHeight
        );

        // convert the image into a 2D board of boolean based on pixel value
        this.lastBoard = Object.freeze(
          picToBoard(imgData.data, this.canvasWidth, this.canvasHeight)
        );
        this.toggleStop();
      },
      // stop drawing routines and clear the canvas
      reset() {
        this.toggleStop();
        this.lastBoard = {};
        this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.getReset = 0;
      },
    },
  };
</script>
<style>
  #canvas-board {
    flex: 1;
    margin: 0 auto;
  }
</style>
