/****************************************************************************
 * apps/games/brickmatch/bm_main.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <math.h>

#include <nuttx/video/fb.h>
#include <nuttx/video/rgbcolors.h>

#ifdef CONFIG_GAMES_BRICKMATCH_USE_CONSOLEKEY
#include "bm_input_console.h"
#endif

#ifdef CONFIG_GAMES_BRICKMATCH_USE_DJOYSTICK
#include "bm_input_joystick.h"
#endif

#ifdef CONFIG_GAMES_BRICKMATCH_USE_GESTURE
#include "bm_input_gesture.h"
#endif

/****************************************************************************
 * Preprocessor Definitions
 ****************************************************************************/

#define BOARDX_SIZE 6
#define BOARDY_SIZE 6

#define ROW         8
#define COL         8

/* Difficult mode:  4, 5, 6 */

#define GAMEMODE    6

#define NCOLORS     GAMEMODE

/****************************************************************************
 * Private Types
 ****************************************************************************/

struct screen_state_s
{
  int fd_fb;
  struct fb_videoinfo_s vinfo;
  struct fb_planeinfo_s pinfo;
#ifdef CONFIG_FB_OVERLAY
  struct fb_overlayinfo_s oinfo;
#endif
  FAR void *fbmem;
};

struct game_screen_s
{
  int xres;     /* X display resolution */
  int yres;     /* Y display resolution */
  int xoff;     /* X offset to start drawing */
  int yoff;     /* Y offset to start drawing */
  int ncolors;  /* Supported number of colors */
  int blklen;   /* Size of side of the blocks */
  int steps;    /* Steps to slide a block for a now position */
  int stepinc;  /* Step increment during the sliding */
  int dir;      /* Direction to move the blocks */
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

static const char g_default_fbdev[] = "/dev/fb0";

/* The edge of the board is invisible to user */

int board[ROW][COL] =
{
  {1, 1, 1, 1, 1, 1, 1, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 1, 1, 1, 1, 1, 1, 1}
};

int prev_board[ROW][COL] =
{
  {1, 1, 1, 1, 1, 1, 1, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 0, 0, 0, 0, 0, 0, 1},
  {1, 1, 1, 1, 1, 1, 1, 1}
};

/* Colors used in the game plus Black */

static const uint16_t pallete[NCOLORS + 1] =
{
  RGB16_BLACK,
  RGB16_BLUE,
  RGB16_GREEN,
  RGB16_RED,
  RGB16_CYAN,
  RGB16_WHITE,
  RGB16_GOLD,
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static void draw_rect(FAR struct screen_state_s *state,
                      FAR struct fb_area_s *area, int color)
{
  FAR uint16_t *dest;
  FAR uint8_t *row;
  int x;
  int y;

  row = (FAR uint8_t *)state->fbmem + state->pinfo.stride * area->y;
  for (y = 0; y < area->h; y++)
    {
      dest = ((FAR uint16_t *)row) + area->x;
      for (x = 0; x < area->w; x++)
        {
          *dest++ = pallete[color];
        }

      row += state->pinfo.stride;
    }
}

void update_screen(FAR struct screen_state_s *state,
                   FAR struct fb_area_s *area)
{
  int ret;

#ifdef CONFIG_FB_UPDATE
  ret = ioctl(state->fd_fb, FBIO_UPDATE,
              (unsigned long)((uintptr_t)area));
  if (ret < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: ioctl(FBIO_UPDATE) failed: %d\n",
              errcode);
    }
#endif
}

/* Draw the user board without the edges */

void draw_board(FAR struct screen_state_s *state,
                FAR struct fb_area_s *area,
                FAR struct game_screen_s *screen)
{
  int row;
  int col;
  int x;
  int y;
  int i;
  int color;

  /* Clear the framebuffer, but don't update yet */

  area->x = 0;
  area->y = 0;
  area->w = screen->xres;
  area->h = screen->yres;

  draw_rect(state, area, 0);

  for (i = screen->steps; i > 0; i--)
    {
      for (row = 1; row <= BOARDX_SIZE; row++)
        {
          for (col = 1; col <= BOARDY_SIZE; col++)
            {
              color = board[row][col];

              x = (col - 1) * screen->blklen + screen->xoff;
              y = (row - 1) * screen->blklen + screen->yoff;

              /* Get original width and height of blocks */

              area->w = screen->blklen;
              area->h = screen->blklen;

              /* Only do the sliding for blocks that changed */

              if (prev_board[row][col] != board[row][col])
                {
                  if (screen->dir == DIR_NONE)
                    {
                      area->x = x;
                      area->y = y;
                    }

                  if (screen->dir == DIR_UP)
                    {
                      area->x = x;
                      area->y = y + ((i - 1) * screen->stepinc);
                    }

                  if (screen->dir == DIR_DOWN)
                    {
                      area->x = x;
                      area->y = y - ((i - 1) * screen->stepinc);
                    }

                  if (screen->dir == DIR_LEFT)
                    {
                      area->x = x + ((i - 1) * screen->stepinc);
                      area->y = y;
                    }

                  if (screen->dir == DIR_RIGHT)
                    {
                      area->x = x - ((i - 1) * screen->stepinc);
                      area->y = y;
                    }
                }
              else
                {
                  area->x = x;
                  area->y = y;
                }

              /* Drawn only part of block if above margin */

              if (area->x > (screen->xres - 2 * screen->xoff))
                {
                  area->x = (screen->xres - 2 * screen->xoff) + 1;
                  area->w = 1;
                }

              if (area->x < screen->xoff)
                {
                  area->x = screen->xoff;
                }

              if (area->y > (screen->yres - 2 * screen->yoff))
                {
                  area->y = (screen->yres - 2 * screen->yoff) + 1;
                  area->h = 1;
                }

              if (area->y < screen->yoff)
                {
                  area->y = screen->yoff;
                }

              draw_rect(state, area, color);
            }
        }

      area->x = 0;
      area->y = 0;
      area->w = screen->xres;
      area->h = screen->yres;

      update_screen(state, area);
      usleep(100000);
    }
}

/****************************************************************************
 * Name: print_board
 *
 * Description:
 *   Draw the board including the user non-visible border for debugging.
 ****************************************************************************/

#ifdef DEBUG_BRICKMATCH_GAME
void print_board(void)
{
  int row;
  int col;

  for (row = 0; row < ROW; row++)
    {
      printf("+---+---+---+---+---+---+---+---+\n");

      for (col = 0; col < COL; col++)
        {
          if (row == 0 || col == 0 || row == ROW - 1 || col == COL - 1)
            {
              printf("|>%c<", board[row][col] + 48);
            }
          else
            {
              printf("| %c ", board[row][col] + 48);
            }
        }

      printf("|\n");
    }

  printf("+---+---+---+---+---+---+---+---+\n\n\n");
}
#endif

/****************************************************************************
 * Name: move_board
 *
 * Description:
 *   Move the blocks (represented by numbers) to 'dir' direction (L,R,U,D).
 ****************************************************************************/

void move_board(int dir)
{
  int row;
  int row_ini;
  int col;
  int col_ini;
  int row_dir;
  int row_pos;
  int col_dir;
  int col_pos;
  int row_lim;
  int col_lim;

  if (dir == DIR_UP)
    {
      row_ini = 0;
      row_dir = 1;
      row_pos = 1;
      row_lim = BOARDY_SIZE + 1;

      col_ini = 0;
      col_dir = 1;
      col_pos = 0;
      col_lim = BOARDX_SIZE + 1;
    }

  if (dir == DIR_DOWN)
    {
      row_ini = BOARDY_SIZE + 1;
      row_dir = -1;
      row_pos = -1;
      row_lim = 0;

      col_ini = 0;
      col_dir = 1;
      col_pos = 0;
      col_lim = BOARDX_SIZE + 1;
    }

  if (dir == DIR_RIGHT)
    {
      row_ini = 0;
      row_dir = 1;
      row_pos = 0;
      row_lim = BOARDY_SIZE + 1;

      col_ini = BOARDX_SIZE + 1;
      col_dir = -1;
      col_pos = -1;
      col_lim = 0;
    }

  if (dir == DIR_LEFT)
    {
      row_ini = 0;
      row_dir = 1;
      row_pos = 0;
      row_lim = BOARDY_SIZE + 1;

      col_ini = 0;
      col_dir = 1;
      col_pos = 1;
      col_lim = BOARDX_SIZE + 1;
    }

  for (row = row_ini; row != row_lim; row += row_dir)
    {
      for (col = col_ini; col != col_lim; col += col_dir)
        {
          /* Never move item if we are in the cache border */

          if ((row != 0 && col != 0) &&
              (row != 0 && col != BOARDX_SIZE + 1) &&
              (row != BOARDY_SIZE + 1 && col != 0) &&
              (row != BOARDY_SIZE + 1 && col != BOARDX_SIZE + 1))
            {
              if (board[row][col] == 0)
                {
                  board[row][col] = board[row + row_pos][col + col_pos];
                  board[row + row_pos][col + col_pos] = 0;
                }
            }
        }
    }
}

/****************************************************************************
 * Name: fill_edge
 *
 * Description:
 *   Fill the invisible border with "colored" blocks
 ****************************************************************************/

void fill_edge(void)
{
  int i;

  for (i = 0; i <= BOARDX_SIZE + 1; i++)
    {
      board[0][i] = (random() % GAMEMODE) + 1;
      board[BOARDY_SIZE + 1][i] = (random() % GAMEMODE) + 1;
    }

  for (i = 0; i <= BOARDY_SIZE + 1; i++)
    {
      board[i][0] = (random() % GAMEMODE) + 1;
      board[i][BOARDY_SIZE + 1] = (random() % GAMEMODE) + 1;
    }
}

/****************************************************************************
 * Name: fill_border
 *
 * Description:
 *   Fill the visible border with "colored" blocks
 ****************************************************************************/

void fill_border(void)
{
  int i;

  for (i = 1; i < BOARDX_SIZE + 1; i++)
    {
      board[1][i] = (random() % GAMEMODE) + 1;
      board[BOARDY_SIZE][i] = (random() % GAMEMODE) + 1;
    }

  for (i = 1; i < BOARDY_SIZE + 1; i++)
    {
      board[i][1] = (random() % GAMEMODE) + 1;
      board[i][BOARDY_SIZE] = (random() % GAMEMODE) + 1;
    }
}

/****************************************************************************
 * Name: search_board_squares
 *
 * Description:
 *   Search for squares or triples of same colors.
 ****************************************************************************/

int search_board_squares(void)
{
  int row;
  int col;
  int found = 0;

  for (row = 1; row < BOARDY_SIZE; row++)
    {
      for (col = 1; col < BOARDX_SIZE; col++)
        {
          if (board[row][col] != 0)
            {
              if (fabs(board[row][col]) == fabs(board[row][col + 1]))
                {
                  /* Square */

                  if (fabs(board[row][col]) == fabs(board[row + 1][col]) &&
                      fabs(board[row][col + 1]) ==
                      fabs(board[row + 1][col + 1]))
                    {
                      found = 1;
                      board[row][col] = board[row][col] > 0 ?
                                        -board[row][col] : board[row][col];
                      board[row][col + 1] = board[row][col + 1] > 0 ?
                                            -board[row][col + 1] :
                                            board[row][col + 1];
                      board[row + 1][col] = board[row + 1][col] > 0 ?
                                            -board[row + 1][col] :
                                            board[row + 1][col];
                      board[row + 1][col + 1] = board[row + 1][col + 1] > 0 ?
                                                -board[row + 1][col + 1] :
                                                board[row + 1][col + 1];
                    }

                  /* |- */

                  if (fabs(board[row][col]) == fabs(board[row + 1][col]))
                    {
                      found = 1;
                      board[row][col] = board[row][col] > 0 ?
                                        -board[row][col] :
                                        board[row][col];
                      board[row][col + 1] = board[row][col + 1] > 0 ?
                                            -board[row][col + 1] :
                                            board[row][col + 1];
                      board[row + 1][col] = board[row + 1][col] > 0 ?
                                            -board[row + 1][col] :
                                            board[row + 1][col];
                    }

                  /* -| */

                  if (fabs(board[row][col + 1]) ==
                      fabs(board[row + 1][col + 1]))
                    {
                      found = 1;
                      board[row][col] = board[row][col] > 0 ?
                                        -board[row][col] : board[row][col];
                      board[row][col + 1] = board[row][col + 1] > 0 ?
                                            -board[row][col + 1] :
                                            board[row][col + 1];
                      board[row + 1][col + 1] = board[row + 1][col + 1] > 0 ?
                                                -board[row + 1][col + 1] :
                                                board[row + 1][col + 1];
                    }
                }

              if (fabs(board[row][col]) == fabs(board[row + 1][col]))
                {
                  /* |_ */

                  if (fabs(board[row + 1][col]) ==
                      fabs(board[row + 1][col + 1]))
                    {
                      found = 1;
                      board[row][col] = board[row][col] > 0 ?
                                        -board[row][col] : board[row][col];
                      board[row + 1][col] = board[row + 1][col] > 0 ?
                                            -board[row + 1][col] :
                                            board[row + 1][col];
                      board[row + 1][col + 1] = board[row + 1][col + 1] > 0 ?
                                                -board[row + 1][col + 1] :
                                                board[row + 1][col + 1];
                    }
                }
            }

          if (board[row + 1][col] != 0)
            {
              /* _|  */

              if (fabs(board[row + 1][col]) == fabs(board[row + 1][col + 1]))
                {
                  if (fabs(board[row + 1][col + 1]) ==
                      fabs(board[row][col + 1]))
                    {
                      found = 1;
                      board[row][col + 1] = board[row][col + 1] > 0 ?
                                            -board[row][col + 1] :
                                            board[row][col + 1];
                      board[row + 1][col] = board[row + 1][col] > 0 ?
                                            -board[row + 1][col] :
                                            board[row + 1][col];
                      board[row + 1][col + 1] = board[row + 1][col + 1] > 0 ?
                                                -board[row + 1][col + 1] :
                                                board[row + 1][col + 1];
                    }
                }
            }
        }
    }

  return found;
}

/****************************************************************************
 * Name: search_board_squares
 *
 * Description:
 *   Search for vertical/horizontal lines of same colours.
 ****************************************************************************/

int search_board_lines(void)
{
  int row;
  int col;
  int h = 0;
  int hpos = 0;
  int hfound = 0;
  int v = 0;
  int vpos = 0;
  int vfound = 0;
  int found = 0;
  int prev_sym = 0;

  /* Search horizontal lines */

  for (row = 1; row < BOARDY_SIZE + 1; row++)
    {
      prev_sym = board[row][1];

      for (col = 1; col < BOARDX_SIZE; col++)
        {
          if (board[row][col] != 0)
            {
              if (fabs(board[row][col]) == fabs(board[row][col + 1]))
                {
                  if (!hfound)
                    {
                      hpos = col;
                      hfound = 1;
                      prev_sym = board[row][col];
                    }

                  if (fabs(board[row][col]) == fabs(prev_sym))
                    {
                      h++;
                    }
                }
              else if (h < 2)
                {
                  h = 0;
                  hpos = 1;
                  hfound = 0;
                }
            }
        }

      if (h >= 2)
        {
          for (int i = hpos; i <= hpos + h; i++)
            {
              board[row][i] = board[row][i] > 0 ?
                              -board[row][i] : board[row][i];
              found = 1;
            }
        }

      h = 0;
      hpos = 1;
      hfound = 0;
    }

  /* Search vertical lines */

  for (col = 1; col < BOARDX_SIZE + 1; col++)
    {
      prev_sym = board[1][col];

      for (row = 1; row < BOARDY_SIZE; row++)
        {
          if (board[row][col] != 0)
            {
              if (fabs(board[row][col]) == fabs(board[row + 1][col]))
                {
                  if (!vfound)
                    {
                      vpos = row;
                      vfound = 1;
                      prev_sym = board[row][col];
                    }

                  if (fabs(board[row][col]) == fabs(prev_sym))
                    {
                      v++;
                    }
                }
              else if (v < 2)
                {
                  v = 0;
                  vpos = 1;
                  vfound = 0;
                }
            }
        }

      if (v >= 2)
        {
          for (int i = vpos; i <= vpos + v; i++)
            {
              board[i][col] = board[i][col] > 0 ?
                              -board[i][col] : board[i][col];
              found = 1;
            }
        }

      v = 0;
      vpos = 1;
      vfound = 0;
    }

  return found;
}

/****************************************************************************
 * Name: check_board
 *
 * Description:
 *   Verify if there are match lines/squares and remove them
 ****************************************************************************/

int check_board(void)
{
  int found = 0;
  int row;
  int col;

  found  = search_board_lines();
  found |= search_board_squares();

  /* Remove all negative values */

  for (row = 0; row < ROW; row++)
    {
      for (col = 0; col < COL; col++)
        {
          if (board[row][col] < 0)
            {
              board[row][col] = 0;
            }
        }
    }

  return found;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * brick_main
 ****************************************************************************/

int main(int argc, FAR char *argv[])
{
  FAR const char *fbdev = g_default_fbdev;
  struct game_screen_s screen;
  struct input_state_s input;
  struct screen_state_s state;
  struct fb_area_s area;
  int ret;

  /* Open the framebuffer driver */

  state.fd_fb = open(fbdev, O_RDWR);
  if (state.fd_fb < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: Failed to open %s: %d\n", fbdev, errcode);
      return EXIT_FAILURE;
    }

  /* Get the characteristics of the framebuffer */

  ret = ioctl(state.fd_fb, FBIOGET_VIDEOINFO,
              (unsigned long)((uintptr_t)&state.vinfo));
  if (ret < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: ioctl(FBIOGET_VIDEOINFO) failed: %d\n",
              errcode);
      close(state.fd_fb);
      return EXIT_FAILURE;
    }

  printf("VideoInfo:\n");
  printf("      fmt: %u\n", state.vinfo.fmt);
  printf("     xres: %u\n", state.vinfo.xres);
  printf("     yres: %u\n", state.vinfo.yres);
  printf("  nplanes: %u\n", state.vinfo.nplanes);

  /* Setup the screen information */

  screen.xres = state.vinfo.xres > state.vinfo.yres ?
                state.vinfo.yres : state.vinfo.xres;
  screen.yres = state.vinfo.yres > state.vinfo.xres ?
                state.vinfo.xres : state.vinfo.yres;
  screen.xoff = (screen.xres % NCOLORS) / 2;
  screen.yoff = (screen.yres % NCOLORS) / 2;
  screen.steps = 2;
  screen.stepinc = 1;
  screen.blklen = (screen.xres / NCOLORS);
  screen.ncolors = NCOLORS;

  /* Get the display planeinfo */

  ret = ioctl(state.fd_fb, FBIOGET_PLANEINFO,
              (unsigned long)((uintptr_t)&state.pinfo));
  if (ret < 0)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: ioctl(FBIOGET_PLANEINFO) failed: %d\n",
              errcode);
      close(state.fd_fb);
      return EXIT_FAILURE;
    }

  printf("PlaneInfo (plane 0):\n");
  printf("    fbmem: %p\n", state.pinfo.fbmem);
  printf("    fblen: %lu\n", (unsigned long)state.pinfo.fblen);
  printf("   stride: %u\n", state.pinfo.stride);
  printf("  display: %u\n", state.pinfo.display);
  printf("      bpp: %u\n", state.pinfo.bpp);

  state.fbmem = mmap(NULL, state.pinfo.fblen, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_FILE, state.fd_fb, 0);
  if (state.fbmem == MAP_FAILED)
    {
      int errcode = errno;
      fprintf(stderr, "ERROR: ioctl(FBIOGET_PLANEINFO) failed: %d\n",
              errcode);
      close(state.fd_fb);
      return EXIT_FAILURE;
    }

  printf("Mapped FB: %p\n", state.fbmem);

  /* Fill the edge with random values */

  fill_edge();
  fill_border();

  /* Draw the empty board to user */

  draw_board(&state, &area, &screen);

  dev_input_init(&input);

  while (1)
    {
      ret = dev_read_input(&input);

      screen.dir = input.dir;

#ifdef DEBUG_BRICKMATCH_GAME
      printf("Before moving:\n");
      print_board();
      usleep(2000000);
#endif

      /* Save a copy of the board before moving to new position */

      memcpy(prev_board, board, (sizeof(int) * ROW * COL));

      move_board(screen.dir);

      draw_board(&state, &area, &screen);

#ifdef DEBUG_BRICKMATCH_GAME
      printf("After moving:\n");
      print_board();
      usleep(1000000);
#endif

      /* Save a copy of the board after moving the pieces */

      memcpy(prev_board, board, (sizeof(int) * ROW * COL));

      screen.dir = DIR_NONE;

      /* If we found bricks with same colors */

      if (check_board() == 1)
        {
          int i;

          /* Lets do a blinking effect */

          for (i = 0; i < 3; i++)
            {
              /* Draw the board with the pieces */

              memcpy(board, prev_board, (sizeof(int) * ROW * COL));

              draw_board(&state, &area, &screen);
              usleep(100000);

              /* Draw the board without the pieces */

              check_board();
              draw_board(&state, &area, &screen);
              usleep(100000);
            }

          usleep(500000);
        }

#ifdef DEBUG_BRICKMATCH_GAME
      printf("After checking:\n");
      print_board();
      usleep(1000000);
#endif

      usleep(1000000);

      /* Add random pieces in the not visible border */

      fill_edge();
    }

  return 0;
}