#include "BoardCanvas.hpp"

wxDEFINE_EVENT(PLAY_MOVE_EVENT, wxCommandEvent);

BoardCanvas::BoardCanvas(wxFrame *parent)
    : wxPanel(parent), black_side(false), is_black_turn(true), frozen(false),
      lock_square_size(false), t(new Theme()), t_captures(new Theme()) {
  board = "rnbqkbnrpppppppp                                PPPPPPPPRNBQKBNR";
  is_dragging = false;
  valid_drag = false;
  adata.reuseBuffer=false;
  adata.buffer=nullptr;
  t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
  SetClockTime(-1, -1, -1, false);
  SetClockTime(-1, -1, -1, true);
  ApplyPreferences();
  // The following should be called when using an EVT_PAINT handler
  SetBackgroundStyle(wxBG_STYLE_PAINT);
  adata.duration=200;
  adata.duration_fast=80;
  adata.fps=30;
  // Let GameTableLeftPanel process keyboard events:
  Bind(wxEVT_KEY_DOWN, [p=this](wxKeyEvent &e){e.ResumePropagation(1);e.Skip();});
  Bind(wxEVT_KEY_UP, [p=this](wxKeyEvent &e){e.ResumePropagation(1);e.Skip();});
}

BoardCanvas::~BoardCanvas() {
  delete t;
  delete t_captures;
  if(adata.buffer!= nullptr)
    free(adata.buffer);
}

BoardCanvas::BoardCanvas(wxFrame *parent, std::uint32_t square_width,
                         bool frozen)
    : BoardCanvas(parent) {
  t->ResizeSquaresAndPieces(square_width);
  t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
  this->frozen = true;
  lock_square_size = true;
}

void BoardCanvas::OnPaint(wxPaintEvent &event) {
  wxBufferedPaintDC dc(this);
  dc.SetBackground(*wxWHITE_BRUSH);
  dc.Clear();

  if(!adata.reuseBuffer){
    // Setting up required attributes
    REFRESH_MOUSE_LOCATION();
    square_width = t->GetSquaresSizes();
    canvas_size = this->GetSize();
    boardX = (canvas_size.x - (8 * square_width)) / 2;
    boardY = (canvas_size.y - (8 * square_width)) / 2;
    if (boardX > canvas_size.x)
      boardX = 0;
    if (boardY > canvas_size.y)
      boardY = 0;

    // Setup buffer (later use for animations)
    if(adata.buffer!=nullptr)
      free(adata.buffer);
    adata.buffer=new wxBitmap(canvas_size.x,canvas_size.y,32);
    wxMemoryDC memDC(*adata.buffer);
    DrawBoard(memDC);
    dc.Blit(0,0,canvas_size.x,canvas_size.y,(wxDC*)&memDC,0,0);
  }
  else {
    // Otherwise reuse buffer and animate
    dc.DrawBitmap(*adata.buffer, 0, 0, true);
    double percent=adata.frame/adata.frames;
    // Draw moving piece
    dc.DrawBitmap(*t->Get(adata.piece_moved), 
    adata.src.x + adata.frame*(adata.transVect.x/adata.frames), 
    adata.src.y + adata.frame*(adata.transVect.y/adata.frames), false);
    // end drawing
    adata.frame++;
  }
}

void BoardCanvas::ApplyPreferences() {
  if (t != nullptr)
    delete t;
  if (t_captures != nullptr)
    delete t_captures;
  t = new Theme();
  t_captures = new Theme();

  CONFIG_OPEN(config);
  black_side = config->Read("board/black_by_default", false);
  if (lock_square_size) {
    t->ResizeSquaresAndPieces(square_width);
  } else {
    t->ResizeSquaresAndPieces(config->Read("board/square_size", 80));
  }
  t->SetSquareRadius(config->Read("board/corner_radius", 10));
  t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
  CONFIG_CLOSE(config);

  Refresh();
}

void BoardCanvas::SetupBoard(std::string board, bool is_black_turn,
                             std::map<char, std::uint8_t> captures,
                             std::string white_player, std::string black_player) {
  this->board = board;
  this->is_black_turn = is_black_turn;
  this->captures = captures;
  this->white_player=white_player;
  this->black_player=black_player;
  Refresh();
}

void BoardCanvas::Animate(const std::string &board, bool is_black_turn, std::map<char, std::uint8_t> captures, std::string src, std::string dst,bool faster){
  std::uint8_t pfile = src[0]-'a';
  std::uint8_t prank = src[1]-'1';
  adata.piece_moved = this->board[pfile + 8 * (7-prank)]; // Piece to move
  
  // Now remove the piece that will be moved
  this->board[pfile + 8 * (7-prank)]=' ';
  wxMemoryDC memDC(*adata.buffer);
  DrawBoard(memDC);

  // Now compute piece start position and translation vector
  std::uint32_t piece_width = t->GetPiecesSizes();
  std::uint32_t centrer_offset = (square_width - piece_width) / 2;
    if (!black_side) {
        prank = 7 - prank;
        pfile = 7 - pfile;
  }
  std::uint32_t x = boardX + (7 - pfile) * square_width;
  std::uint32_t y = boardY + prank * square_width;
  adata.src.x = x + centrer_offset;
  adata.src.y = y + centrer_offset;
  // Now dst:
  pfile = dst[0]-'a';
  prank = dst[1]-'1';
    if (!black_side) {
        prank = 7 - prank;
        pfile = 7 - pfile;
  }
  x = boardX + (7 - pfile) * square_width;
  y = boardY + prank * square_width;
  adata.transVect.x=x-adata.src.x+centrer_offset;
  adata.transVect.y=y-adata.src.y+centrer_offset;

  // Start animation:
  adata.reuseBuffer=true;
  int duration_backup=adata.duration;
  adata.duration=faster ? adata.duration_fast : adata.duration;
  int frame_duration=(1000/adata.fps);
  adata.frame=0;
  adata.frames=adata.duration/frame_duration; // total number of frames
  int time_per_frame=adata.duration/adata.frames;
  wxStopWatch sw;
  for(int i=adata.frames;i>0;i--){
    Refresh();
    Update();
    int delay=sw.Time()-frame_duration;
    if(delay>5){ // 5ms tolerance
      wxMilliSleep(delay);
    }
    sw.Start(0);
  }
  adata.duration=faster ? duration_backup : duration_backup;
  adata.reuseBuffer=false;
  SetupBoard(board, is_black_turn, captures,white_player,black_player);
}

void BoardCanvas::DrawBoard(wxDC &dc) {
  std::uint32_t piece_width = t->GetPiecesSizes();
  std::uint32_t centrer_offset = (square_width - piece_width) / 2;
  wxSize numbers_size=dc.GetTextExtent("0");

  bool DrawDraggingPiece = false;
  char dp = 'p';
  std::uint32_t dpx = 0, dpy = 0;
  for (std::int8_t file = 7; file >= 0; file--) {
    for (std::uint8_t rank = 0; rank <= 7; rank++) {
      std::uint32_t x = boardX + (7 - file) * square_width;
      std::uint32_t y = boardY + rank * square_width;
      if ((file + rank) % 2 == 0) {
        if (file == 0 && rank == 0) {
          dc.DrawBitmap(*t->Get('1'), x, y, true);
        } else if (file == 7 && rank == 7) {
          dc.DrawBitmap(*t->Get('2'), x, y, true);
        } else {
          dc.DrawBitmap(*t->Get('s'), x, y, true);
        }
      } else {
        if (file == 7 && rank == 0) {
          dc.DrawBitmap(*t->Get('0'), x, y, true);
        } else if (file == 0 && rank == 7) {
          dc.DrawBitmap(*t->Get('3'), x, y, true);
        } else {
          dc.DrawBitmap(*t->Get('S'), x, y, true);
        }
      }

      // Draw numbers
      dc.SetFont(wxFont(*wxNORMAL_FONT).MakeBold());
      if(file==7){ // Right numbers
        dc.DrawText(wxString((char)('1'+7-rank)),
        x-numbers_size.x*1.5,y+square_width/2-numbers_size.y/2);
      }
      if(rank==7){ // Bottom numbers
        dc.DrawText(wxString((char)('a'+7-file)),
        x+square_width/2-numbers_size.x/2,y+square_width);
      }    

      // Draw pieces
      std::uint8_t prank = rank;
      std::uint8_t pfile = file;
      if (black_side) {
        prank = 7 - rank;
        pfile = 7 - file;
      }
      std::uint32_t px = x + centrer_offset;
      std::uint32_t py = y + centrer_offset;
      char piece = board[(7 - pfile) + 8 * prank];
      if (is_dragging && (7 - pfile) == active_square.x &&
          (7 - prank) == active_square.y) {
        dp = piece;
        dpx = px - (lastClickX - mouseX);
        dpy = py - (lastClickY - mouseY);
        DrawDraggingPiece = true;
        continue;
      }
      if (piece != ' ') {
        dc.DrawBitmap(*t->Get(piece), px, py, false);
      }
    }
  }

  // Draw badge
  dc.SetPen(wxPen(*wxBLACK, 3));
  std::uint32_t badgeY = boardY;
  std::uint32_t badgeWidth = square_width / 2;
  if (is_black_turn) {
    dc.SetBrush(*wxBLACK_BRUSH);
    if (black_side) {
      badgeY = boardY + (8 * square_width) - badgeWidth;
    }
  } else {
    dc.SetBrush(*wxWHITE_BRUSH);
    if (!black_side) {
      badgeY = boardY + (8 * square_width) - badgeWidth;
    }
  }
  wxRect badge(boardX + (8 * square_width) + badgeWidth/2, badgeY, badgeWidth,
               badgeWidth);
  dc.DrawRectangle(badge);

  // Draw captures (+player names) first for white then for black
  std::uint32_t captures_size = t_captures->GetPiecesSizes();
  std::uint8_t padding = 10;
  std::uint32_t offsetX = 0;
  std::uint32_t offsetY = -(captures_size + padding);
  std::uint32_t offsetYPlayerName=offsetY-captures_size-padding; // For top player name we
  // add -padding at the end (ortherwise bottom of the letters
  // are to close to captured pieces)
  // White (black's captures):
  if (black_side) {
    offsetY = 8 * square_width + padding + numbers_size.y;
    offsetYPlayerName = offsetY+captures_size;
  }
  for (char p : {'P', 'N', 'B', 'R', 'Q'}) {
    if (captures.find(p) != captures.end()) {
      for (std::uint8_t i = 0; i < captures[p]; i++) {
        dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY);
        offsetX += captures_size / 2;
      }
      offsetX += captures_size / 2;
    }
  }
  dc.DrawText(wxString(black_player),boardX,boardY + offsetYPlayerName);
  // Black (white's captures):
  offsetX = 0;
  if (black_side) {
    offsetY = -(captures_size + padding);
    offsetYPlayerName = offsetY-captures_size-padding; // Same for -padding (cf. for black's captures)
  } else {
    offsetY = 8 * square_width + padding + numbers_size.y;
    offsetYPlayerName = offsetY+captures_size;
  }
  for (char p : {'p', 'n', 'b', 'r', 'q'}) {
    if (captures.find(p) != captures.end()) {
      for (std::uint8_t i = 0; i < captures[p]; i++) {
        dc.DrawBitmap(*t_captures->Get(p), boardX + offsetX, boardY + offsetY);
        offsetX += captures_size / 2;
      }
      offsetX += captures_size / 2;
    }
  }
  dc.DrawText(wxString(white_player),boardX,boardY + offsetYPlayerName);

  // Draw dragging piece
  if (DrawDraggingPiece) {
    dc.DrawBitmap(*t->Get(dp), dpx, dpy, false);
  }

  // Draw numbers
  for (char l = 'a'; l < 'a' + 8; l++) {
    dc.DrawText(wxString(l), wxPoint(boardX + l - 'a' * square_width,
                                     boardY + 8 * square_width + 10));
  }

  // Draw Clocks
  if (std::get<0>(black_time) >= 0) {
    wxFont font = dc.GetFont();
    ClockTime clock = black_side ? white_time : black_time;
    wxString time =
        wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock));
    if (std::get<0>(clock) > 0) {
      time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock));
    } else if (std::get<1>(clock) > 0) {
      time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock));
    }
    dc.SetFont(font.Scale(1.5).MakeBold());
    wxCoord width, height;
    dc.GetTextExtent(time, &width, &height);
    dc.DrawText(time,
                wxPoint(boardX + square_width * 8 - width, boardY - height - numbers_size.y*2));
    clock = black_side ? black_time : white_time;
    time = wxString::Format("%ds", std::get<1>(clock), std::get<2>(clock));
    if (std::get<0>(clock) > 0) {
      time = wxString::Format("%d:%d", std::get<0>(clock), std::get<1>(clock));
    } else if (std::get<1>(clock) > 0) {
      time = wxString::Format("%d:%ds", std::get<1>(clock), std::get<2>(clock));
    }
    dc.GetTextExtent(time, &width, &height);
    dc.DrawText(time, wxPoint(boardX + square_width * 8 - width,
                              boardY + square_width * 8 + numbers_size.y*2));
  }
}

void BoardCanvas::MouseEvent(wxMouseEvent &event) {
  if (!frozen) {
    if (event.Dragging()) {
      if (valid_drag) {
        is_dragging = true;
        Refresh();
      }
    } else {
      if (is_dragging) {
        is_dragging = false;
        valid_drag = false;
        // Handle drop
        REFRESH_MOUSE_LOCATION();
        INIT_CURRENT_SQUARE();

        if (IsCurrentSquareValid) {
          wxLogDebug("Drag end on square (%d,%d)", file, rank);
          /// Play the move
          wxCommandEvent event(PLAY_MOVE_EVENT, GetId());
          event.SetEventObject(this);
          std::string move = ((char)('a' + active_square.x)) +
                             std::to_string(+active_square.y + 1) +
                             ((char)('a' + file)) + std::to_string(rank + 1);
          event.SetString(move);
          ProcessEvent(event);
        }
      }
      if (event.LeftDown()) {
        SetFocus();
        REFRESH_MOUSE_LOCATION();
        lastClickX = mouseX;
        lastClickY = mouseY;
        INIT_CURRENT_SQUARE();
        if (IsCurrentSquareValid) {
          active_square.x = file;
          active_square.y = rank;
          if (board[(7 - rank) * 8 + file] != ' ') {
            wxLogDebug("Drag start on square (%d,%d)", file, rank);
            valid_drag = true;
          }
        }
      }
    }
  }
  // Let GameTableLeftPanel process mouse wheel events:
  if (event.GetWheelRotation() != 0) {
    event.ResumePropagation(1);event.Skip();
  }
}

void BoardCanvas::Zoom(std::int32_t zoom) {
  t->Zoom(zoom);
  t_captures->ResizePieces(t->GetPiecesSizes() * CAPTURE_FACTOR);
  Refresh();
}

void BoardCanvas::Swap() {
  black_side = !black_side;
  Refresh();
}

void BoardCanvas::SetClockTime(short hours, short min, short sec,
                               bool IsBlack) {
  if (IsBlack) {
    black_time = std::make_tuple(hours, min, sec);
  } else {
    white_time = std::make_tuple(hours, min, sec);
  }
}

wxBEGIN_EVENT_TABLE(BoardCanvas, wxPanel) EVT_PAINT(BoardCanvas::OnPaint)
    EVT_MOUSE_EVENTS(BoardCanvas::MouseEvent)
        wxEND_EVENT_TABLE()
