<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,
    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,
        },
      };
    },
    computed: {
      ...mapState(globalStore, {
        cellProperties: "cellProperties",
        ruleset: "ruleset1d",
        refreshRate: "refreshRate",
        initial1dState: "initial1dState",
        drawingDirection: "drawingDirection",
        canDraw: "canDraw",
        getDraw1d: "draw1d",
        getDraw2d: "draw2d",
        getDraw2dLast: "draw2dLast",
        getDraw2dPicture: "draw2dpicture",
        boardWidth: "boardWidth",
        boardHeight: "boardHeight",
        picture: "picture",
        selected2dRules: "selected2dRules",
      }),
      ...mapWritableState(globalStore, {
        lastBoard: "lastBoard",
        canvasWidth: "canvasWidth",
        canvasHeight: "canvasHeight",
        getReset: "reset",
      }),
      // used to determine the dimensions of the board
      max() {
        return Math.max(this.boardWidth, this.boardHeight);
      },
    },
    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) {
        const props = this.cellProperties;
        board.map((row, y) => {
          const d = props.size;
          return row.map((cell, x) => {
            this.ctx.fillStyle = (() => {
              if (cell === 1) return props.liveColor;
              return props.deadColor;
            })();
            if (this.drawingDirection === "x")
              this.ctx.fillRect(y * d, x * d, d, d);
            else this.ctx.fillRect(x * d, y * d, d, d);
            return cell;
          });
        });
      },
      // 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]);
      },
      // 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.toggleStop();
      },
      // draw 2D automaton on the canvas in a loop
      draw2d(board) {
        if (!this.canDraw) return;
        const draw2dNext = async (b) => {
          if (!this.canDraw) return;
          const newBoard = evolve2d(
            b,
            this.available2dRules[this.selected2dRules.id]
          );
          this.drawCanvas(b, this.cellProperties);
          this.lastBoard = Object.freeze(newBoard);
          await sleep(this.refreshRate);
          draw2dNext(newBoard);
        };
        return draw2dNext(board);
      },
      // draw 2d automaton from a new state
      draw2dNew() {
        const initialState = create2dState(
          this.boardWidth,
          this.boardHeight,
          getRandomInt,
          [0, 2]
        );
        const board = evolve2d(initialState, conwayRules);
        this.lastBoard = Object.freeze(board);
        this.draw2d(board);
      },
      // draw 2d automaton from the last known generated board
      draw2dLast() {
        if (this.lastBoard != undefined) this.draw2d(this.lastBoard);
      },
      // 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.workCtx.drawImage(
          this.picture,
          0,
          0,
          this.canvasWidth,
          this.canvasHeight
        );
        const imgData = this.workCtx.getImageData(
          0,
          0,
          this.canvasWidth,
          this.canvasHeight
        );

        // convert the image to black and white
        const black = picToBlackAndWhite(
          imgData.data,
          this.canvasWidth,
          this.canvasHeight
        );

        // draw it back on the canvas
        this.ctx.putImageData(black, 0, 0);

        // draw the image back on the work canvas with the dimensions of the board
        this.workCtx.drawImage(
          this.picture,
          0,
          0,
          this.boardWidth,
          this.boardHeight
        );

        const resized = this.workCtx.getImageData(
          0,
          0,
          this.boardWidth,
          this.boardHeight
        );

        // convert the resized image into a 2D board of boolean based on pixel value
        this.lastBoard = Object.freeze(
          picToBoard(resized.data, this.boardWidth, this.boardHeight)
        );

        this.toggleStop();
      },
      // stop drawing routines and clear the canvas
      reset() {
        this.toggleStop();
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.getReset = 0;
      },
    },
  };
</script>
<style>
  #canvas-board {
    flex: 1;
    margin: 0 auto;
  }
</style>
