//  Testbed for making a match3 board
//  Created: 28/10/2017 09:05:33
//  John Ryland (jryland@xiaofrog.com)
//  InvertedLogic, (C) Copyright 2017
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <pthread.h>
#include "board.h"
#include "windowing.h"
#include "game.h"
#include "curves.h"
#include "common.h"
#include "state.h"


const int star0 = 78;
const int star1 = 79;
const int star2 = 80;
const int ring = 81;
int letterIndex(char ch)
{
  const int startIdx = 9 + 4;
  if (ch >= '0' && ch <= '9')
    return ch - '0' + startIdx;
  if (ch == '@') return startIdx + 10;
  if (ch == '$') return startIdx + 11;
  if (ch == '%') return startIdx + 12;
  if (ch >= 'a' && ch <= 'z')
    return ch - 'a' + startIdx + 13;
  if (ch >= 'A' && ch <= 'Z')
    return ch - 'A' + startIdx + 13 + 13 + 13;
  return 0;
}

/*
class BaseState
{
public:
  BaseState(MyWindow& win);
};
*/

/*
  Add debug menu
     - add display of curves by graphing them
*/
class MyWindow : public CanvasWindow
{
  int splashFrame, newGameFrame;
  Image gridImage, bgImage, splashImage, titleImage;
  SpriteSheet dialogSheet;
  int spacing;
  int tileTypes;
  std::vector<vec2f> testCurve;
  SavedState state;
  void save() {
    if (!saveState(state))
      printf("error saving state\n");
  }
public:
  MyWindow()
  {
    state.m_highestScore = 0;
    if (!loadState(state))
      printf("error loading state\n");
    save();

    createNewGame(game, 0, 0, 0);
    selectedSprite = -1;
    otherSprite = -1;
    offX = 32 + 25;
    offY = 80 + 25;
    noMoreMoves = false;
    splashFrame = 0;
    spacing = 35;
    tileTypes = 12;

    loadPngImage(gridImage, "data/grid.png");
    loadPngImage(bgImage, "data/background.png");
    loadPngImage(titleImage, "data/title.png");
    loadPngImage(splashImage, "data/splash.png");

    // sprite sheet could come from DB
    loadSpriteSheet(spriteSheet, "data/sprites-32x32.txt");
    loadSpriteSheet(buttonSheet, "data/button.txt");
    loadSpriteSheet(dialogSheet, "data/dialog.txt");
  }
  ~MyWindow()
  {
    destroyGame(game);
  }
  void drawSparkles(int x, int y, int count, int frame, float radius)
  {
    float angle = 2.0 * 3.14 / (7.0 * count);
    for (int i = 0; i < count; i++) {
      int x1 = int(x + 5 + radius * 0.1 * cos(angle * ((frame + i) % (7*count)))); 
      int y1 = int(y - 8 + radius * 0.1 * sin(angle * ((frame + i) % (7*count)))); 
      drawSprite(buffer, x1, y1, spriteSheet.sprites[star0 + ((frame + i) % 3)], 255-(frame*25), 3);
    }
  }
  /*
  void drawCheatsMenu()
  {
    drawDialog((width()-200)/2, height()-120, 200, 110);
    drawButton((width()-128)/2, height()-65, 128, 50);
    drawText(40 + (width()-128)/2, 5 + height()-50, "New");
  }
  */
  void drawDialog(int x, int y, int w, int h)
  {
    drawNineSlice(x, y, w, h, dialogSheet);
  }
  void drawButton(int x, int y, int w, int h, const char* text)
  {
    int textWidth = strlen(text) * 14;
    drawNineSlice(x, y, w, h, buttonSheet);
    drawText(x + (w-textWidth)/2, y + h/2 - 11, text);
  }
  void drawNineSlice(int x, int y, int w, int h, const SpriteSheet& sheet)
  {
    if (sheet.sprites.size() != 9)
    {
      printf("not a 9-slice button\n");
      return;
    }
    int srcW[3] = { sheet.sprites[0].w, sheet.sprites[1].w, sheet.sprites[2].w };
    int srcH[3] = { sheet.sprites[0].h, sheet.sprites[3].h, sheet.sprites[6].h };
    if (w < (srcW[0] + srcW[1] + srcW[2]))
    {
      printf("width not wide enough\n");
      return;
    }
    if (h < (srcH[0] + srcH[1] + srcH[2]))
    {
      printf("height not high enough\n");
      return;
    }
    int midW = w - (srcW[0] + srcW[2]);
    int midH = h - (srcH[0] + srcH[2]);
    int dstW[3] = { srcW[0], midW, srcW[2] }; 
    int dstH[3] = { srcH[0], midH, srcH[2] }; 
    int dstY = y;
    // TODO: should turn this in to canvas items
    for (int j = 0; j < 3; j++) {
      int dstX = x;
      for (int i = 0; i < 3; i++) {
        if (i == 1 || j == 1) {
          drawScaledSprite(buffer, dstX, dstY, dstW[i], dstH[j], sheet.sprites[j*3+i]);
        } else {
          drawSprite(buffer, dstX, dstY, sheet.sprites[j*3+i], 255);
        }
        dstX += dstW[i];
      }
      dstY += dstH[j];
    }
  }
  void drawText(int x, int y, const char* text)
  {
    while (*text) {
      if (!isspace(*text))
        drawSprite(buffer, x, y, spriteSheet.sprites[letterIndex(*text)], 255);
      x += 14;
      text++;
    }
  }

  void onDraw(int x1, int y1, int x2, int y2)
  {
    refresh();
    if (splashFrame >= 0) {
      CanvasWindow::onDraw(x1, y1, x2, y2);
      // TODO: turn in to canvas items
      if (splashFrame < 7) {
        drawImage(buffer, 0, 0, bgImage); 
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, titleImage, splashFrame * (255/7));
      } else if (splashFrame < 14) {
        drawImage(buffer, 0, 0, bgImage); 
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, titleImage, 255);
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, splashImage, (splashFrame-7) * (255/7));
      } else if (splashFrame < 60) {
        drawImage(buffer, 0, 0, bgImage); 
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, titleImage, 255);
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, splashImage, 255);
      } else if (splashFrame < 67) {
        drawTestGradient(buffer);
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, bgImage, (67-splashFrame) * (255/7)); 
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, titleImage, (67-splashFrame) * (255/7));
        drawImageWithMaskWithConstantAlpha(buffer, 0, 0, splashImage, (67-splashFrame) * (255/7));
      } else {
        drawTestGradient(buffer);
        drawButton((width()-128)/2, height()/2 - 80, 128, 50, "Play");
      }
      splashFrame++;
      if (splashFrame > 75) {
        //splashFrame = -1;
        //newGameFrame = 0;
      }
      return;
    }
   
    int finishedCount = 0;
    for (int i = 0; i < game.m_events.size(); i++)
    {
      //printf("board events\n");
      BoardEvent& evt = game.m_events[i];
      if (!evt.userInit)
      {
        //printf("initing\n");
        evt.frame = 0;
        evt.frames = 14;
        evt.userInit = true;
        if (evt.type == BoardEvent::Added) {
          //printf("added value: %i,  x: %i  y: %i\n", evt.value, evt.newX, evt.newY);
          canvasAddSprite(canvas, offX + evt.newX*spacing, offY + evt.newY*spacing, spriteSheet.sprites[evt.value]);
          //printf("added index:  before: %i   after: %i\n", evt.itemIndex, canvas.sprites.size() - 1);
          evt.itemIndex = canvas.sprites.size() - 1;
        }
        if (evt.type == BoardEvent::Moved) {
          int dy = evt.delay;
          evt.frames += dy * 2;
        }
        if (evt.type == BoardEvent::NoMoreMoves) {
          noMoreMoves = true;
          evt.frames = 0;
          printf("no more moves\n");
          save();
        }
      } else {
        //printf("not initing\n");
      }
      if (evt.frame >= evt.frames)
      {
        //printf("finished\n");
        finishedCount++;
        evt.type = BoardEvent::None;
      }
      else
      {
        //printf("updating\n");
        switch (evt.type)
        {
          case BoardEvent::Swapped:
          {
            //printf("swapped\n");
            CanvasItem copy1 = canvas.sprites[evt.itemIndex];
            canvas.sprites[evt.itemIndex] = canvas.sprites[evt.newItemIndex];
            canvas.sprites[evt.newItemIndex] = copy1;
            evt.type = BoardEvent::None;
            break;
          }
          case BoardEvent::Added:
          {
            //printf("added\n");
            CanvasItem& item = canvas.sprites[evt.itemIndex];
            item.x = offX + spacing*evt.newX;
            item.y = offY + lerp<int>(spacing*evt.y, spacing*evt.newY, Ratio<int>(evt.frame, evt.frames));
            //printf("cur y: %i \n", item.y );
            item.alpha = lerp<int>(32, 255, Ratio<int>(evt.frame, evt.frames));
            break;
          }
          case BoardEvent::Removed:
          {
            //printf("removed\n");
            CanvasItem& item = canvas.sprites[evt.itemIndex];
            
            item.alpha = lerp<int>(255, 0, Ratio<int>(evt.frame, evt.frames));
            
            //printf("item.y: %i  evt.y: %i\n", item.y, evt.y);

            // animate them moving up to top right of screen as they are collected
            int x = offX + spacing*evt.x, y = offY + spacing*evt.y;
            float per1 = float(std::max(evt.frame - evt.delay*2, 0))/(evt.frames - evt.delay*2);
            float per = easeOutCurve(per1);
            vec2i pt = bezierCurve((vec2i){x, y}, (vec2i){x-70, y-70}, (vec2i){335, -3},  per);
            item.x = pt.x;
            item.y = pt.y;
            item.mode = 4;
            item.xScaling = easeOutElasticCurve(per1);
            item.yScaling = item.xScaling;
            break;
          }
          case BoardEvent::Moved:
          {
            //printf("moved  %i,%i\n", evt.x, evt.y);
            CanvasItem& item = canvas.sprites[evt.itemIndex];
            int dy = evt.delay;
            float per1 = float(std::max(evt.frame - dy*2, 0))/(evt.frames - dy*2);
            float per = easeInOutBackCurve(per1);
            item.y = int(offY +  spacing*evt.y + per * (spacing*evt.newY - spacing*evt.y));
            item.x = int(offX +  spacing*evt.x + per * (spacing*evt.newX - spacing*evt.x));
            if (per1 > 0.7f && per1 < 0.9f) {
              item.mode = 4;
              item.xScaling = std::max(1.0f, per*per*per);//1.0 - 2.8*easeInElasticCurve(per1));
              item.yScaling = std::max(0.2f, 2.0f - item.xScaling);
            } else {
              item.mode = 2;
              item.xScaling = 1.0f;
              item.yScaling = 1.0f;
            }
            break;
          }
          case BoardEvent::None:
          case BoardEvent::NoMoreMoves:
          case BoardEvent::ScoredPoints:
            break;
        }
        /*
        static int ff = 0;
        ff++;
        if (ff > 5) {
          ff = 0;
          evt.frame++;
        }
        */
        evt.frame++;
      }
    }
    //printf("finished iterating board events\n");
    if (finishedCount != 0 && finishedCount == game.m_events.size())
    {
      //printf("anims finished\n");
      game.m_events.clear();

      // re-normalize the board<->sprites
      populateCanvasItems();
      
      if (!noMoreMoves)
        checkForMoreMatches(game);
    }


    int canvasX = 0;
    int canvasY = 0;
    if (newGameFrame >= 0) {
      float curvePos = easeOutBackCurve(float(newGameFrame)/20);
      canvasX = lerp<int>(350, 0, curvePos);
      newGameFrame++;
      if (newGameFrame > 20)
        newGameFrame = -1;
    }

    //printf("calling canvas draw\n");
    drawTestGradient(buffer);
    drawImageWithAlpha(buffer, canvasX + offX-5, canvasY + offY-5, gridImage); 
    CanvasWindow::onDraw(canvasX, canvasY, x2, y2);
    //printf("finished drawing canvas\n");

    if (selectedSprite != -1) {
      CanvasItem& item = canvas.sprites[selectedSprite];
      drawSprite(buffer, item.x, item.y, *item.sprite, 255);
      drawSprite(buffer, item.x, item.y, spriteSheet.sprites[12], 255);
    }

    for (int i = 0; i < game.m_events.size(); i++)
    {
      BoardEvent& evt = game.m_events[i];
      if (evt.type == BoardEvent::Removed)
      {
        int frame = evt.frame - 1;
        //CanvasItem& item = canvas.sprites[evt.itemIndex];
        //item.alpha = lerp<int>(evt.frame, evt.frames, 255, 0);
        int x = offX + spacing*evt.x, y = offY + spacing*evt.y;
        float per = float(std::max(evt.frame - evt.delay*2, 0))/(evt.frames - evt.delay*2);
        per = easeOutCurve(per);
        vec2i pt = bezierCurve((vec2i){x, y}, (vec2i){x-70, y-70}, (vec2i){335, -3},  per);
        //item.x = pt.x;
        //item.y = pt.y;
        drawSparkles(pt.x, pt.y, 3, evt.frame, 15.0 + evt.frame);
      }
    }

    static int f = 0;
    static int sparkleX = 4;
    static int sparkleY = 5;
    static int sparkleDelay = 50;
    f++;
    if (f < 20) {
      int x = offX + spacing * sparkleX, y = offY + spacing * sparkleY;
      drawSparkles(x, y, 1, f, 10.0 + f);
    } else if (f > sparkleDelay) {
      sparkleX = rand() % game.width();
      sparkleY = rand() % game.height();
      sparkleDelay = 10 + (rand() % 70);
      f = 0;
    }

    char buf[256];
    snprintf(buf, 255, "Highest %04i   Score %04i", state.m_highestScore, game.m_score);
    drawText(40, 5, buf);
    if (noMoreMoves) {
      drawText(140, 160, "NO MORE MOVES");
    }
    //drawSprite(buffer, (width()-128)/2, height()-55, spriteSheet.sprites[8], 255);
    drawDialog((width()-200)/2, height()-120, 200, 110);
    drawButton((width()-128)/2, height()-65, 128, 50, "New");

#if CURVE_TEST
    for (int i = 0; i < 1000; i++) {
      vec2f pnt = kochanekBartelsSpline(testCurve, float(i) / 1000.0); // a_tension = 0.0f, a_bias = 0.0f, a_continuity = 0.0f
      buffer.pixels[ int(height() - 1 - pnt.y) * width() + int(pnt.x) ] = 0xFFFFFFFF;
    }
#endif // CURVE_TEST

  }
  int width()
  {
#if USE_RETINA
    return buffer.w / 2;
#else
    return buffer.w;
#endif
  }
  int height()
  {
#if USE_RETINA
    return buffer.h / 2;
#else
    return buffer.h;
#endif
  }
  void populateCanvasItems()
  {
    updateTiles(game);
    canvas.sprites.clear();
    foreach (Tile& t, game.m_tiles) 
      canvasAddSprite(canvas, offX + t.x*spacing, offY + t.y*spacing, spriteSheet.sprites[t.tileTypeId]);
  }
  void onMouseEvent(int x, int y, int buttons)
  {
    if (buttons && splashFrame != -1 && splashFrame > 75) {
      splashFrame = -1;
      newGameFrame = 0;
    }

#if CURVE_TEST
    static int oldButtons = 0;
    if (!oldButtons && buttons) {
      printf("adding point\n");
      testCurve.push_back((vec2f){ x, y });
    }
    oldButtons = buttons;
#endif // CURVE_TEST

    if ( x > ((width()-128)/2) && x < ((width()-128)/2 + 128)
        && y > (height()-50) && y < (height()-50+32) ) {
      if (!buttons) {
        //newGameFrame = 0;
        newGame();
        return;
      }
    }

    //printf("mouse event: %i %i   %i\n", x, y, buttons);
    if (buttons) {
      if (selectedSprite != -1) {
        CanvasItem& item = canvas.sprites[selectedSprite];
        int dx = x - selx;
        int dy = y - sely;
        int boardX = selectedSprite % game.width();
        int boardY = selectedSprite / game.width();
        int curOtherSprite = -1;
        dx = std::min<int>(std::max<int>(dx, -40), 40);
        dy = std::min<int>(std::max<int>(dy, -40), 40);
        if (boardX == 0 && dx < 0) dx = 0;
        if (boardX == (game.width()-1) && dx > 0) dx = 0;
        if (boardY == 0 && dy < 0) dy = 0;
        if (boardY == (game.height()-1) && dy > 0) dy = 0;

        item.x = selitemx;
        item.y = selitemy;

        if (dx*dx > dy*dy) {
          item.x += dx;
          // find the item that will be swapped with this
          if (dx > 0)
            curOtherSprite = selectedSprite + 1;
          if (dx < 0)
            curOtherSprite = selectedSprite - 1;
        } else {
          item.y += dy;
          // find the item that will be swapped with this
          if (dy > 0)
            curOtherSprite = selectedSprite + game.width();
          if (dy < 0)
            curOtherSprite = selectedSprite - game.width();
        }

        if (otherSprite != -1 && curOtherSprite != otherSprite)
        {
          CanvasItem& other = canvas.sprites[otherSprite];
          other.x = otherX;
          other.y = otherY;
        }

        if (curOtherSprite != -1)
        {
          CanvasItem& other = canvas.sprites[curOtherSprite];
          if (curOtherSprite != otherSprite)
          {
            otherX = other.x;
            otherY = other.y;
            otherSprite = curOtherSprite;
          }
          other.x = otherX;
          other.y = otherY;
          if (dx*dx > dy*dy) {
            other.x -= dx;
          } else {
            other.y -= dy;
          }
        }
      } else {
        // HACK: offset -3,-3 to make it line up - perhaps there is a border between the view and the window
        selectedSprite = canvasHitTestWithAlpha(canvas, x-3, y-3);
        if (selectedSprite != -1) {
          size_t n = canvas.sprites.size() - 1;
          CanvasItem itemS = canvas.sprites[selectedSprite];
          /*
          // Reorder so the selected one will be rendered on top
          // if do this, the selected index is used to infer the position in the board, so will need a mapping of index to board position
          memmove(&canvas.sprites[selectedSprite], &canvas.sprites[selectedSprite+1], sizeof(CanvasItem)*(n-selectedSprite+1));
          canvas.sprites[n] = itemS;
          selectedSprite = n;
          */
          selx = x;
          sely = y;
          selitemx = itemS.x;
          selitemy = itemS.y;
        }
      }
    } else {
      bool canSwap = false;
      if (selectedSprite != -1 && otherSprite != -1) {
        int boardX1 = selectedSprite % game.width();
        int boardY1 = selectedSprite / game.width();
        int boardX2 = otherSprite % game.width();
        int boardY2 = otherSprite / game.width();
        canSwap = swapTiles(game, boardX1, boardY1, boardX2, boardY2);
        //printf("can swap: %s\n", canSwap ? "true" : "false");
        if (canSwap)
        {
          int tmp = otherSprite;
          otherSprite = selectedSprite;
          selectedSprite = tmp;
        } else {
          printf("failed swap attempt\n");
          CanvasItem& item0 = canvas.sprites[selectedSprite];
          CanvasItem& item1 = canvas.sprites[otherSprite];
          BoardEvent evt;
          evt.type = BoardEvent::Moved;
          evt.delay = 0;
          evt.userInit = false;
          evt.itemIndex = selectedSprite;
          evt.x = boardX2;
          evt.y = boardY2;
          evt.newX = boardX1;
          evt.newY = boardY1;
          game.m_events.push_back(evt);
          evt.itemIndex = otherSprite;
          evt.x = boardX1;
          evt.y = boardY1;
          evt.newX = boardX2;
          evt.newY = boardY2;
          game.m_events.push_back(evt);
        }
      }
      if (selectedSprite != -1) {
        CanvasItem& item = canvas.sprites[selectedSprite];
        item.x = selitemx;
        item.y = selitemy;
        // snaps back - but could then apply a swap if criteria is met
      }
      if (otherSprite != -1) {
        CanvasItem& item = canvas.sprites[otherSprite];
        item.x = otherX;
        item.y = otherY;
        // snaps back - but could then apply a swap if criteria is met
      }
      selectedSprite = -1;
      otherSprite = -1;
    }
  }
  void newGame()
  {
    if (game.m_score > state.m_highestScore) {
      state.m_highestScore = game.m_score;
    }
    int sprites = tileTypes;// spriteSheet.sprites.size();
    printf("New match3 board [%ix%i] %i\n", bw, bh, sprites);
    destroyGame(game);
    createNewGame(game, bw, bh, sprites);
    populateCanvasItems();
    noMoreMoves = false;
    //canvasAddSprite(canvas, x-32, y-32, spriteSheet.sprites[0]);
    save();
  }
  void setBoardDimensions(int a_width, int a_height)
  {
    bw = a_width;
    bh = a_height;
    newGame();
  }
private:
  Game game;
  SpriteSheet spriteSheet;
  SpriteSheet buttonSheet;
  int selectedSprite;
  int selx, sely, selitemx, selitemy;
  int otherSprite;
  int otherX, otherY;
  int offX, offY;
  bool noMoreMoves;
  int bw, bh;
};

class MyApp : public Application
{
public:
  MyApp(int argc, char* argv[])
    : Application()
  {
    for (int i = 0; i < argc; i++)
      printf("argv[%i]: %s\n", i, argv[i]);
    int boardWidth = (argc > 1) ? atoi(argv[1]) : 10;
    int boardHeight = (argc > 2) ? atoi(argv[2]) : 10;
    window.setBoardDimensions(boardWidth, boardHeight);
  }
  void onStart()
  {
    window.create(480, 650);
  }
private:
  MyWindow window;
};

int main(int argc, char ** argv)
{
  MyApp app(argc, argv);
  return app.run();
}


