function team_symbol(team) {
  if (team === "+")
    return "+";
  else
    return "o";
}

function undisplay(element) {
  element.style.display="none";
}

function add_message(severity, message) {
  message = `<div class="message ${severity}" onclick="undisplay(this)">
<span class="hide-button" onclick="undisplay(this.parentElement)">&times;</span>
${message}
</div>`;
  const message_area = document.getElementById('message-area');
  message_area.insertAdjacentHTML('beforeend', message);
}

/*********************************************************
 * Handling server-sent event stream                     *
 *********************************************************/

const events = new EventSource("events");

events.onerror = function(event) {
  if (event.target.readyState === EventSource.CLOSED) {
    setTimeout(() => {
      add_message("danger", "Connection to server lost.");
    }, 1000);
  }
};

events.addEventListener("game-info", event => {
  const info = JSON.parse(event.data);

  window.game.set_game_info(info);
});

events.addEventListener("player-info", event => {
  const info = JSON.parse(event.data);

  window.game.set_player_info(info);
});

events.addEventListener("player-enter", event => {
  const info = JSON.parse(event.data);

  window.game.set_other_player_info(info);
});

events.addEventListener("player-update", event => {
  const info = JSON.parse(event.data);

  if (info.id === window.game.state.player_info.id)
    window.game.set_player_info(info);
  else
    window.game.set_other_player_info(info);
});

events.addEventListener("move", event => {
  const move = JSON.parse(event.data);

  window.game.receive_move(move);
});

events.addEventListener("game-state", event => {
  const state = JSON.parse(event.data);

  window.game.reset_board();

  for (let square of state.moves) {
    window.game.receive_move(square);
  }
});

/*********************************************************
 * Game and supporting classes                           *
 *********************************************************/

const scribe_glyphs = [
  {
    name: "Single",
    squares: [1,0,0,
              0,0,0,
              0,0,0]
  },
  {
    name: "Double",
    squares: [1,1,0,
              0,0,0,
              0,0,0]
  },
  {
    name: "Line",
    squares: [1,1,1,
              0,0,0,
              0,0,0]
  },
  {
    name: "Pipe",
    squares: [0,0,1,
              1,1,1,
              0,0,0]
  },
  {
    name: "Squat-T",
    squares: [1,1,1,
              0,1,0,
              0,0,0]
  },
  {
    name: "4-block",
    squares: [1,1,0,
              1,1,0,
              0,0,0]
  },
  {
    name: "T",
    squares: [1,1,1,
              0,1,0,
              0,1,0]
  },
  {
    name: "Cross",
    squares: [0,1,0,
              1,1,1,
              0,1,0]
  },
  {
    name: "6-block",
    squares: [1,1,1,
              1,1,1,
              0,0,0]
  },
  {
    name: "Bomber",
    squares: [1,1,1,
              0,1,1,
              0,0,1]
  },
  {
    name: "Chair",
    squares: [0,0,1,
              1,1,1,
              1,0,1]
  },
  {
    name: "J",
    squares: [0,0,1,
              1,0,1,
              1,1,1]
  },
  {
    name: "Earring",
    squares: [0,1,1,
              1,0,1,
              1,1,1]
  },
  {
    name: "House",
    squares: [0,1,0,
              1,1,1,
              1,1,1]
  },
  {
    name: "H",
    squares: [1,0,1,
              1,1,1,
              1,0,1]
  },
  {
    name: "U",
    squares: [1,0,1,
              1,0,1,
              1,1,1]
  },
  {
    name: "Ottoman",
    squares: [1,1,1,
              1,1,1,
              1,0,1]
  },
  {
    name: "O",
    squares: [1,1,1,
              1,0,1,
              1,1,1]
  },
  {
    name: "9-block",
    squares: [1,1,1,
              1,1,1,
              1,1,1]
  }
];

function copy_to_clipboard(id)
{
  const tmp = document.createElement("input");
  tmp.setAttribute("value", document.getElementById(id).innerHTML);
  document.body.appendChild(tmp);
  tmp.select();
  document.execCommand("copy");
  document.body.removeChild(tmp);
}

function GameInfo(props) {
  if (! props.id)
    return null;

  return (
    <div className="game-info">
      <span className="game-id">{props.id}</span>
      {" "}
      Share this link to invite a friend:{" "}
      <span id="game-share-url">{props.url}</span>
      {" "}
      <button
        className="inline"
        onClick={() => copy_to_clipboard('game-share-url')}
      >Copy Link</button>
    </div>
  );
}

function TeamButton(props) {
  return <button className="inline"
                 onClick={() => props.game.join_team(props.team)}>
           {props.label}
         </button>;
}

function TeamChoices(props) {
  let other_team;
  if (props.player.team === "+")
    other_team = "o";
  else
    other_team = "+";

  if (props.player.team === "") {
    if (props.first_move) {
      return null;
    } else {
      return [
        <TeamButton key="+" game={props.game} team="+" label="Join 🞥" />,
        " ",
        <TeamButton key="o" game={props.game} team="o" label="Join 🞇" />
      ];
    }
  } else {
    return <TeamButton game={props.game} team={other_team} label="Switch" />;
  }
}

function PlayerInfo(props) {
  if (! props.player.id)
    return null;

  const choices = <TeamChoices
                    game={props.game}
                    first_move={props.first_move}
                    player={props.player}
                  />;

  return (
    <div className="player-info">
      <span className="players-header">Players: </span>
      {props.player.name}
      {props.player.team ? ` (${props.player.team})` : ""}
      {props.first_move ? "" : " "}
      {choices}
      {props.other_players.map(other => (
        <span key={other.id}>
          {", "}
          {other.name}
          {other.team ? ` (${other.team})` : ""}
        </span>
      ))}
    </div>
  );
}

function Glyph(props) {

  const glyph_dots = [];

  let last_square = 0;
  for (let i = 0; i < 9; i++) {
    if (props.squares[i])
      last_square = i;
  }

  const height = Math.floor(20 * (Math.floor(last_square / 3) + 1));

  const viewbox=`0 0 60 ${height}`;

  for (let row = 0; row < 3; row++) {
    for (let col = 0; col < 3; col++) {
      if (props.squares[3 * row + col]) {
        let cy = 10 + 20 * row;
        let cx = 10 + 20 * col;
        glyph_dots.push(
          <circle
            key={3 * row + col}
            cx={cx}
            cy={cy}
            r="8"
          />
        );
      }
    }
  }

  return (<div className="glyph-and-name">
            {props.name}
            <div className="glyph">
              <svg viewBox={viewbox}>
                <g fill="#287789">
                  {glyph_dots}
                </g>
              </svg>
            </div>
          </div>
         );
}

function Square(props) {
  let className = "square";

  if (props.value.symbol) {
    className += " occupied";
  } else if (props.active) {
    className += " open";
  }

  if (props.value.glyph) {
    if (props.value.symbol === '+')
      className += " glyph-plus";
    else
      className += " glyph-o";
  }

  if (props.last_move) {
    className += " last-move";
  }

  const onClick = props.active ? props.onClick : null;

  return (
    <div className={className}
         onClick={onClick}>
      {props.value.symbol}
    </div>
  );
}

function MiniGrid(props) {

  const mini_grid = props.mini_grid;
  const squares = mini_grid.squares;

  function grid_square(j) {
    const value = squares[j];
    const last_move = props.last_moves.includes(j);

    /* Even if the grid is active, the square is only active if
     * unoccupied. */
    const square_active = (props.active && (value.symbol === null));

    return (
      <Square
        value={value}
        active={square_active}
        last_move={last_move}
        onClick={() => props.onClick(j)}
      />
    );
  }

  /* Even if my parent thinks I'm active because of the last move, I
   * might not _really_ be active if I'm full. */
  let occupied = 0;
  mini_grid.squares.forEach(element => {
    if (element.symbol)
      occupied++;
  });

  let class_name = "mini-grid";
  if (props.active && occupied < 9)
    class_name += " active";

  let winner = null;
  if (mini_grid.winner) {
    winner = <div className="winner">{mini_grid.winner}</div>;
  }

  return (
    <div className={class_name}>
      {grid_square(0)}
      {grid_square(1)}
      {grid_square(2)}
      {grid_square(3)}
      {grid_square(4)}
      {grid_square(5)}
      {grid_square(6)}
      {grid_square(7)}
      {grid_square(8)}
      {winner}
    </div>
  );
}

class Board extends React.Component {
  mini_grid(i) {
    /* This mini grid is active only if both:
     *
     * 1. It is our turn (this.props.active === true)
     *
     * 2. One of the following conditions is met:
     *
     *    a. This is this players first turn (last_two_moves[0] === null)
     *    b. This mini grid corresponds to this players last turn
     *    c. The mini grid that corresponds to the players last turn is full
     */
    let grid_active = false;
    if (this.props.active) {
      grid_active = true;
      if (this.props.last_two_moves.length > 1) {
        /* First index (0) gives us our last move, (that is, of the
         * last two moves, it's the first one, so two moves ago).
         *
         * Second index (1) gives us the second number from that move,
         * (that is, the index within the mini-grid that we last
         * played).
         */
        const target = this.props.last_two_moves[0][1];
        let occupied = 0;
        this.props.mini_grids[target].squares.forEach(element => {
          if (element.symbol)
            occupied++;
        });
        /* If the target mini-grid isn't full then this grid is
         * only active if it is that target. */
        if (occupied < 9)
          grid_active = (i === target);
      }
    }

    /* We want to highlight each of the last two moves (both "+" and
     * "o"). So we filter the last two moves that have a first index
     * that matches this mini_grid and pass down their second index
     * be highlighted.
     */
    const last_moves = this.props.last_two_moves.filter(move => move[0] === i)
          .map(move => move[1]);

    const mini_grid = this.props.mini_grids[i];
    return (
      <MiniGrid
        mini_grid={mini_grid}
        active={grid_active}
        last_moves={last_moves}
        onClick={(j) => this.props.onClick(i,j)}
      />
    );
  }

  render() {
    return (
      <div className="board-container">
        <div className="board">
          {this.mini_grid(0)}
          {this.mini_grid(1)}
          {this.mini_grid(2)}
          {this.mini_grid(3)}
          {this.mini_grid(4)}
          {this.mini_grid(5)}
          {this.mini_grid(6)}
          {this.mini_grid(7)}
          {this.mini_grid(8)}
        </div>
      </div>
    );
  }
}

function fetch_method_json(method, api = '', data = {}) {
  const response = fetch(api, {
    method: method,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });
  return response;
}

function fetch_post_json(api = '', data = {}) {
  return fetch_method_json('POST', api, data);
}

async function fetch_put_json(api = '', data = {}) {
  return fetch_method_json('PUT', api, data);
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      game_info: {},
      player_info: {},
      other_players: [],
      mini_grids: [...Array(9)].map(() => ({
        score_plus: null,
        score_o: null,
        winner: null,
        squares: Array(9).fill({
          symbol: null,
          glyph: false
        })
      })),
      moves: [],
      next_to_play: "+",
    };
  }

  set_game_info(info) {
    this.setState({
      game_info: info
    });
  }

  set_player_info(info) {
    this.setState({
      player_info: info
    });
  }

  set_other_player_info(info) {
    const other_players_copy = [...this.state.other_players];
    const idx = other_players_copy.findIndex(o => o.id === info.id);
    if (idx >= 0) {
      other_players_copy[idx] = info;
    } else {
      other_players_copy.push(info);
    }
    this.setState({
      other_players: other_players_copy
    });
  }

  reset_board() {
    this.setState({
      next_to_play: "+"
    });
  }

  find_connected_recursive(recursion_state, position) {

    if (position < 0 || position >= 9)
      return;

    if (recursion_state.visited[position])
      return;

    recursion_state.visited[position] = true;

    if (recursion_state.mini_grid[position].symbol !== recursion_state.target)
      return;

    recursion_state.connected[position] = true;

    /* Left */
    if (position % 3 !== 0)
      this.find_connected_recursive(recursion_state, position - 1);
    /* Right */
    if (position % 3 !== 2)
      this.find_connected_recursive(recursion_state, position + 1);
    /* Up */
    this.find_connected_recursive(recursion_state, position - 3);
    /* Down */
    this.find_connected_recursive(recursion_state, position + 3);
  }

  /* Find all cells within a mini-grid that are 4-way connected to the
   * given cell. */
  find_connected(mini_grid, position) {
    const connected = Array(9).fill(false);

    /* If the given cell is empty then there is nothing connected. */
    if (mini_grid[position] === null)
      return connected;

    const cell = mini_grid[position].symbol;

    let recursion_state = {
      mini_grid: mini_grid,
      connected: connected,
      visited: Array(9).fill(false),
      target: cell,
    };
    this.find_connected_recursive(recursion_state, position);

    return connected;
  }

  /* Determine whether a connected group of cells is a glyph.
   *
   * Here, 'connected' is a length-9 array of Booleans, true
   * for the connected cells in a mini-grid.
   */
  is_glyph(connected) {

    /* Now that we have a set of connected cells, let's collect some
     * stats on them, (width, height, number of cells, configuration
     * of corner cells, etc.).
     */
    let min_row = 2;
    let min_col = 2;
    let max_row = 0;
    let max_col = 0;
    let count = 0;

    for (let i = 0; i < 9; i++) {
      const row = Math.floor(i/3);
      const col = i % 3;

      if (! connected[i])
        continue;

      count++;

      min_row = Math.min(row, min_row);
      min_col = Math.min(col, min_col);
      max_row = Math.max(row, max_row);
      max_col = Math.max(col, max_col);
    }

    const width = max_col - min_col + 1;
    const height = max_row - min_row + 1;

    /* Corners, (top-left, top-right, bottom-left, and bottom-right) */
    const tl = connected[3 * min_row + min_col];
    const tr = connected[3 * min_row + max_col];
    const bl = connected[3 * max_row + min_col];
    const br = connected[3 * max_row + max_col];

    const count_true = (acc, val) => acc + (val ? 1 : 0);
    const corners_count = [tl, tr, bl, br].reduce(count_true, 0);
    const top_corners_count = [tl, tr].reduce(count_true, 0);
    const bottom_corners_count = [bl, br].reduce(count_true, 0);
    const left_corners_count = [tl, bl].reduce(count_true, 0);
    const right_corners_count = [tr, br].reduce(count_true, 0);

    let two_corners_in_a_line = false;
    if (top_corners_count    === 2 ||
        bottom_corners_count === 2 ||
        left_corners_count   === 2 ||
        right_corners_count  === 2)
    {
      two_corners_in_a_line = true;
    }

    let zero_corners_in_a_line = false;
    if (top_corners_count    === 0 ||
        bottom_corners_count === 0 ||
        left_corners_count   === 0 ||
        right_corners_count  === 0)
    {
      zero_corners_in_a_line = true;
    }

    /* Now we have the information we need to determine glyphs. */
    switch (count) {
    case 1:
      /* Single */
      return true;
    case 2:
      /* Double */
      return true;
    case 3:
      /* Line */
      return (width === 3 || height === 3);
    case 4:
      /* Pipe, Squat-T, and 4-block, but not Tetris S */
      return two_corners_in_a_line;
    case 5:
      if (width !== 3 || height !== 3 || ! connected[4])
      {
        /* Pentomino P and U are not glyphs (not 3x3) */
        /* Pentomino V is not a glyph (center not connected) */
        return false;
      }
      else if (corners_count === 0 || two_corners_in_a_line)
      {
        /* Pentomino X is glyph Cross (no corners) */
        /* Pentomino T is glyph T (has a row or column with 2 corners) */
        return true;
      } else {
        /* The corner counting above excludes pentomino F, W, and Z
         * which are not glyphs. */
        return false;
      }
      break;
    case 6:
      /* 6-Block has width or height of 2. */
      /* Bomber, Chair, and J have 3 corners occupied. */
      if (width === 2 || height === 2 || corners_count === 3)
        return true;
      return false;
    case 7:
      /* Earring and U have no center square occupied */
      /* H has 4 corners occupied */
      /* House has a row or column with 0 corners occupied */
      if ((! connected[4]) || corners_count === 4 || zero_corners_in_a_line)
        return true;
      return false;
    case 8:
      /* Ottoman or O */
      if (corners_count === 4)
        return true;
      return false;
    case 9:
      return true;
    }

    /* Should be unreachable */
    return false;
  }

  receive_move(move) {
    const mini_grid_index = move[0];
    const position = move[1];

    /* Don't allow any moves after the board is full */
    if (this.state.moves.length === 81) {
      return;
    }

    /* Set the team's symbol into the board state. */
    const symbol = team_symbol(this.state.next_to_play);
    const new_mini_grids = this.state.mini_grids.map(obj => {
      const new_obj = {...obj};
      new_obj.squares = obj.squares.slice();
      return new_obj;
    });
    const new_mini_grid = new_mini_grids[mini_grid_index];
    new_mini_grid.squares[position] = {
      symbol: symbol,
      glyph: false
    };

    /* With the symbol added to the squares, we need to see if this
     * newly-placed move forms a glyph or not. */
    const connected = this.find_connected(new_mini_grid.squares, position);
    const is_glyph = this.is_glyph(connected);

    /* Either set (or clear) the glyph Boolean for each connected square. */
    for (let i = 0; i < 9; i++) {
      if (connected[i])
        new_mini_grid.squares[i].glyph = is_glyph;
    }

    /* If this is the last cell of played in a mini-grid then it's
     * time to score it. */
    const occupied = new_mini_grid.squares.reduce(
      (acc, val) => acc + (val.symbol !== null ? 1 : 0), 0);
    if (occupied === 9) {
      for (let i = 0; i < 9; i++) {
        if (new_mini_grid.squares[i].glyph) {
          if (new_mini_grid.squares[i].symbol === '+')
            new_mini_grid.score_plus++;
          else
            new_mini_grid.score_o++;
        }
      }
      if (new_mini_grid.score_plus > new_mini_grid.score_o)
        new_mini_grid.winner = '+';
      else
        new_mini_grid.winner = 'o';
    }

    /* And append the move to the list of moves. */
    const new_moves = [...this.state.moves, move];

    /* Finally, compute the next player to move. */
    let next_to_play;
    if (this.state.next_to_play === "+")
      next_to_play = "o";
    else
      next_to_play = "+";

    /* And shove all those state modifications toward React. */
    this.setState({
      mini_grids: new_mini_grids,
      moves: new_moves,
      next_to_play: next_to_play
    });
  }

  async handle_click(i, j, first_move) {
    let move = {
      move: [i, j]
    };
    if (first_move) {
      move.assert_first_move = true;
    }
    const response = await fetch_post_json("move", move);
    if (response.status == 200) {
      const result = await response.json();
      if (! result.legal)
        add_message("danger", result.message);
    } else {
      add_message("danger", `Error occurred sending move`);
    }
  }

  join_team(team) {
    fetch_put_json("player", {team: team});
  }

  render() {
    const state = this.state;
    const first_move = state.moves.length === 0;
    const my_team = state.player_info.team;
    var board_active;

    let status;
    if (this.state.moves.length === 81)
    {
      status = "Game over";
      board_active = false;
    }
    else if (first_move)
    {
      if (state.other_players.length == 0) {
        status = "You can move or wait for another player to join.";
      } else {
        let qualifier;
        if (state.other_players.length == 1) {
          qualifier = "Either";
        } else {
          qualifier = "Any";
        }
        status = `${qualifier} player can make the first move.`;
      }
      board_active = true;
    }
    else if (my_team === "")
    {
      status = "You're just watching the game.";
      board_active = false;
    }
    else if (my_team === state.next_to_play)
    {
      status = "Your turn. Make a move.";
      board_active = true;
    }
    else
    {
      status = "Waiting for another player to ";
      if (state.other_players.length == 0) {
        status += "join.";
      } else {
        status += "move.";
      }
      board_active = false;
    }

    return [
      <GameInfo
        key="game-info"
        id={state.game_info.id}
        url={state.game_info.url}
      />,
      <PlayerInfo
        key="player-info"
        game={this}
        first_move={first_move}
        player={state.player_info}
        other_players={state.other_players}
      />,
      <div key="game" className="game">
        <div>{status}</div>
        <div className="game-board">
          <Board
            active={board_active}
            mini_grids={state.mini_grids}
            last_two_moves={state.moves.slice(-2)}
            onClick={(i,j) => this.handle_click(i, j, first_move)}
          />
        </div>
      </div>,
      <div key="glyphs" className="glyphs">
        {
          scribe_glyphs.map(glyph => {
            return (
              <Glyph
                key={glyph.name}
                name={glyph.name}
                squares={glyph.squares}
              />
            );
          })
        }
      </div>
    ];
  }
}

ReactDOM.render(<Game
                  ref={(me) => window.game = me}
                />, document.getElementById("scribe"));
