//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "MainForm.h"
//---------------------------------------------------------------------------

#include <stdio.h>
#include "ConfigForm.h"
#include "AboutForm.h"
#include <fstream>
#include <string>

#pragma package(smart_init)
#pragma resource "*.dfm"

//******************************************************************************
//******************************************************************************
//******************************************************************************
// Global stuff
//******************************************************************************

TfrmMain *frmMain;

// Set up some colors
TColor MapColors[] = {clAqua,    // empty
                      clWhite,   // obstacle
                      clBlue,    // character
                      clRed,     // enemy
                      clYellow   // prize
                     };

ColorID ColorMap[] = {{ctEMPTY, MapColors[ctEMPTY]},
                      {ctOBSTACLE, MapColors[ctOBSTACLE]},
                      {ctCHARACTER, MapColors[ctCHARACTER]},
                      {ctENEMY, MapColors[ctENEMY]},
                      {ctPRIZE, MapColors[ctPRIZE]}
                     };

/*
  Constructor.
*/
__fastcall TfrmMain::TfrmMain(TComponent* Owner) : TForm(Owner)

{
  pnlWorld->DoubleBuffered = true;     // prevent flicker
  gridWorld->DoubleBuffered = true;    // prevent flicker

    // Some defaults
  FShowGrid = true;
  FGridColor = clBlack;
  FCurrColorID = ColorMap[ctOBSTACLE];
  FColumns = 20;
  FRows = 20;
  FCurrFileName = "Exported.txt"; // for compatibility with CS230 game .exe
  IsUndo = false;

    // Reset the internal data
  InitializeApp();
}
//---------------------------------------------------------------------------

/*
  Initializes the internal state of the UI
*/
void TfrmMain::InitializeApp(void)
{
    // Clear undo history
  while (!UndoData.empty())
    UndoData.pop();

    // Set all cells to empty
  for (int i = 0; i < FMaxRows; i++)
    for (int j = 0; j < FMaxColumns; j++)
      FDataMap[i][j] = ctEMPTY;

    // Initialize the size of the grid
  spnRows->Position = FRows;
  spnColumns->Position = FColumns;

    // Assume the mouse is off of the grid
  FCurrRow = -1;
  FCurrColumn = -1;
  FPrevRow = -1;
  FPrevColumn = -1;

  AdjustGrid();
  DisplayCurrentCellInStatusBar();
}

/*
  Add information to the undo stack. The information is the previous
  state so that it can be un-done later.
*/
void TfrmMain::PushUndo(void)
{
  CellData cd;

    // All we need is the previous color and the row/column
  cd.row = FCurrRow;
  cd.col = FCurrColumn;
  cd.ctype = (CELL_TYPE) FDataMap[FCurrRow][FCurrColumn];
  UndoData.push(cd);
}

/*
  Remove information from the undo stack. The removed information
  tells the application "how" to undo the last step.
*/
void TfrmMain::PopUndo(void)
{
    // Nothing to do
  if (UndoData.empty())
    return;

    // Get/remove undo info
  CellData cd = UndoData.top();
  UndoData.pop();

    // Set the state to what was popped off
  FCurrRow = cd.row;
  FCurrColumn = cd.col;
  FDataMap[FCurrRow][FCurrColumn] = cd.ctype;

    // A little tricky. Since we don't want to highlight (a thick border)
    // the cell, we need to communicate this to the HighlightCell method.
    // It won't highlight a cell if it is during an undo operation. Are there
    // other/better ways to handle this? Probably, but this will do here.
  IsUndo = true;
  HighlightCell(true);
  IsUndo = false;

  DisplayCurrentCellInStatusBar();
}

/*
  Displays current row/column in the status bar
*/
void TfrmMain::DisplayCurrentCellInStatusBar(void)
{
    // Invalid location, erase the display
  if (FOffGrid || FCurrColumn == -1 || FCurrRow == -1)
    barStatus->Panels->Items[0]->Text = "";
  else
    barStatus->Panels->Items[0]->Text = Format(" Row: %d, Col: %d", ARRAYOFCONST((FCurrRow + 1, FCurrColumn + 1)));
}

/*
  If the grid is resized, need to update variables with the new sizes
*/
void TfrmMain::AdjustGrid(void)
{
  gridWorld->ColCount = FColumns;
  gridWorld->RowCount = FRows;

    // The size of the cells, including the borders
  FCellWidth = gridWorld->Width / FColumns;
  FCellHeight = gridWorld->Height / FRows;

    // The size of the cells without the borders
  if (gridWorld->GridLineWidth)
  {
    FCellWidth -= gridWorld->GridLineWidth;
    FCellHeight -= gridWorld->GridLineWidth;
  }

    // Make this the default now
  gridWorld->DefaultColWidth = FCellWidth;
  gridWorld->DefaultRowHeight = FCellHeight;

    // Keep the currently selected cell as selected
  TGridRect rect = {{FCurrColumn, FCurrRow}, {FCurrColumn, FCurrRow}};
  gridWorld->Selection = rect;
}

/*
  Displays text in the bottom of the app. Used for debugging.
*/
void TfrmMain::AddStatus(const char *text)
{
  mmoStatus->Lines->Insert(0, text);
}

/*
  The mouse movement is tracked on the grid by highlighting the border of
  the cell that is under the mouse cursor.
*/
void TfrmMain::HighlightCell(bool Force)
{
    // If we are in the same cell, do nothing
  if ( (!Force) && (FPrevRow == FCurrRow && FPrevColumn == FCurrColumn) )
    return;

    // Otherwise, we've moved from one cell to the next.
    // Un-highlight (lowlight?) the previous cell.
  TRect rect = gridWorld->CellRect(FPrevColumn, FPrevRow);
  TGridDrawState state;
  DrawCell(FPrevColumn, FPrevRow, rect);

    // Off the board, do nothing
  if (FCurrRow < 0 || FCurrRow >= FRows || FCurrColumn < 0 || FCurrColumn >= FColumns)
    return;

    // Highlight the new cell
  rect = gridWorld->CellRect(FCurrColumn, FCurrRow);
  DrawCell(FCurrColumn, FCurrRow, rect);
}

/*
  This does the actual drawing of each cell
*/
void TfrmMain::DrawCell(int ACol, int ARow, TRect &Rect)
{
    // Rectangle of cell to draw
  Rect.Left = Rect.Left - 1;
  Rect.Right = Rect.Right;
  Rect.Top = Rect.Top - 1;
  Rect.Bottom = Rect.Bottom;

    // Default
  gridWorld->Canvas->Pen->Width = 1;
  gridWorld->Canvas->Pen->Color = FGridColor;

  //bool highlight = false;

    // Draw a 3 pixel wide black border on the highlighted cell
  if ((ACol == FCurrColumn && ARow == FCurrRow && !FOffGrid && (ACol != -1 && ARow != -1)) )
  {
    if (!IsUndo)
    {
      gridWorld->Canvas->Pen->Width = 3;
      gridWorld->Canvas->Pen->Color = clBlack;
      Rect.left += gridWorld->Canvas->Pen->Width - 1;
      Rect.Top += gridWorld->Canvas->Pen->Width - 1;
      Rect.right -= gridWorld->Canvas->Pen->Width - 1;
      Rect.Bottom -= gridWorld->Canvas->Pen->Width - 1;
      //highlight = true;
    }
  }

    // BUG FIX (from WarBoats!)
  if ((ACol == -1) || (ARow == -1))
    return;

    // The cell is empty
  if (FDataMap[ARow][ACol] == ctEMPTY)
  {
    gridWorld->Canvas->Brush->Color = MapColors[ctEMPTY];
    gridWorld->Canvas->Rectangle(Rect);
  }
    // The cell is an obstacle (collision)
  else if (FDataMap[ARow][ACol] == ctOBSTACLE)
  {
    gridWorld->Canvas->Brush->Color = MapColors[ctOBSTACLE];
    gridWorld->Canvas->Rectangle(Rect);
  }
    // The cell is the character in the game
  else if (FDataMap[ARow][ACol] == ctCHARACTER)
  {
    gridWorld->Canvas->Brush->Color = MapColors[ctCHARACTER];
    gridWorld->Canvas->Rectangle(Rect);
  }
    // The cell is an enemy
  else if (FDataMap[ARow][ACol] == ctENEMY)
  {
    gridWorld->Canvas->Brush->Color = MapColors[ctENEMY];
    gridWorld->Canvas->Rectangle(Rect);
  }
    // The cell is a prize (coin)
  else if (FDataMap[ARow][ACol] == ctPRIZE)
  {
    #if 1
      gridWorld->Canvas->Brush->Color = MapColors[ctPRIZE];
      gridWorld->Canvas->Rectangle(Rect);
    #else
      /***** EXPERIMENTAL CODE *****/
      gridWorld->Canvas->Brush->Color = MapColors[ctEMPTY];
      TRect r = Rect;
      /*
      r.Top++;
      r.Bottom -= 2;
      r.Left++;
      r.Right -= 2;
      */

      gridWorld->Canvas->StretchDraw(r, imgPrize->Picture->Bitmap);

      TPoint pts[4];
      pts[0].x = Rect.Left;
      pts[0].y = Rect.Top;
      pts[1].x = Rect.Left;
      pts[1].y = Rect.Bottom;
      pts[2].x = Rect.Right;
      pts[2].y = Rect.Bottom;
      pts[3].x = Rect.Right;
      pts[3].y = Rect.Top;
      if (highlight)
        gridWorld->Canvas->Polyline(pts, 4);
    #endif
  }
}

/*
  Enables/disables the grid lines
*/
void TfrmMain::ChangeGrid(bool Show)
{
  if (Show)
    FGridColor = clBlack;
  else
    FGridColor = MapColors[ctEMPTY];

  gridWorld->Refresh(); // repaint grid
}

//******************************************************************************
//******************************************************************************
//******************************************************************************
// Event Handlers
//******************************************************************************

//******************************************************************************
// Form Event Handlers
//******************************************************************************
void __fastcall TfrmMain::FormCreate(TObject *Sender)
{
    // The 'usual' visual modifications at runtime
  pgeWorld->Align = alClient;
  pnlWorld->Align = alClient;
  gridWorld->Align = alClient;

    // Seamless grid
  //pnlBottom->Color = MapColors[ctEMPTY];
  //gridWorld->Color = MapColors[ctEMPTY];

    // Size of the main window (default)
  Width = 700;
  Height = 700;

    // Default size
  spnColumns->Position = FColumns;
  spnRows->Position = FRows;

    // Hide the memo for now
  mmoStatus->Height = 0;

    // Register this application with the OS as a drop target.
  DragAcceptFiles(this->Handle, true);

    // We need to guarantee that this has been created before
    // calling any of its methods (e.g. LoadSettings). We take
    // ownership, so it will be deleted when this class is deleted.
  frmConfig = new TfrmConfig(this);
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::FormClose(TObject *Sender, TCloseAction &Action)
{
  frmConfig->SaveSettings();
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::FormActivate(TObject *Sender)
{
  frmConfig->LoadSettings();
  FShowGrid = frmConfig->GridEnabled;
  btnGrid->Down = FShowGrid;
  ChangeGrid(FShowGrid);
  spnRows->Position = frmConfig->DefaultRowCount;
  spnColumns->Position = frmConfig->DefaultColCount;
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::FormDestroy(TObject *Sender)
{
    // Unregister this application with the OS as a drop target.
  DragAcceptFiles(this->Handle, false);
}
//---------------------------------------------------------------------------

//******************************************************************************
// TDrawGrid Event Handlers
//******************************************************************************

/*
  Called each time a cell needs to be repainted.
*/
void __fastcall TfrmMain::gridWorldDrawCell(TObject *, int ACol, int ARow, TRect &Rect, TGridDrawState)
{
  DrawCell(ACol, ARow, Rect);
}
//---------------------------------------------------------------------------

/*
  Called when a cell is selected (with the mouse or keyboard).
*/
void __fastcall TfrmMain::gridWorldSelectCell(TObject *Sender, int ACol,  int ARow, bool &CanSelect)
{
  FCurrRow = ARow;
  FCurrColumn = ACol;

  PushUndo();

  FDataMap[ARow][ACol] = FCurrColorID.type;
  DisplayCurrentCellInStatusBar();
}
//---------------------------------------------------------------------------

/*
  Handles the OnMouseDown event. If the user holds the right mouse button
  down, we will erase (paint in the background color) the cell.
*/
void __fastcall TfrmMain::gridWorldMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y)
{
  if (Button == mbRight)
  {
      // Map mouse position (pixels) to row/col
    TGridCoord coord = gridWorld->MouseCoord(X, Y);
    FCurrRow = coord.Y;
    FCurrColumn = coord.X;

    PushUndo();

    FDataMap[FCurrRow][FCurrColumn] = ctEMPTY;
    DisplayCurrentCellInStatusBar();
    HighlightCell(true);
  }
}
//---------------------------------------------------------------------------

/*
  Tracks the mouse movement across the grid so the cell under the mouse
  can be highlighted.
*/
void __fastcall TfrmMain::gridWorldMouseMove(TObject *Sender, TShiftState Shift, int X, int Y)
{
  TGridCoord coord = gridWorld->MouseCoord(X, Y);

  FPrevRow = FCurrRow;
  FPrevColumn = FCurrColumn;
  FCurrRow = coord.Y;
  FCurrColumn = coord.X;
  if (Shift.Contains(ssRight))
  {
    if (FPrevRow == FCurrRow && FPrevColumn == FCurrColumn)
      return;

    FDataMap[FCurrRow][FCurrColumn] = ctEMPTY;
  }
  DisplayCurrentCellInStatusBar();
  HighlightCell();
}
//---------------------------------------------------------------------------

/*
  When the mouse leaves the grid, we need to update the display.
*/
void __fastcall TfrmMain::gridWorldMouseLeave(TObject *Sender)
{
  FOffGrid = true;
  TRect rect = gridWorld->CellRect(FCurrColumn, FCurrRow);
  DrawCell(FCurrColumn, FCurrRow, rect);
  DisplayCurrentCellInStatusBar();
}
//---------------------------------------------------------------------------

/*
  When the mouse moves onto the grid, we need to start tracking it and update
  the display.
*/
void __fastcall TfrmMain::gridWorldMouseEnter(TObject *Sender)
{
  FOffGrid = false;
  TRect rect = gridWorld->CellRect(FCurrColumn, FCurrRow);
  DrawCell(FCurrColumn, FCurrRow, rect);
}
//---------------------------------------------------------------------------

//******************************************************************************
// Action Event Handlers
//******************************************************************************

/*
  Handler for File Open
*/
void __fastcall TfrmMain::actFileOpenExecute(TObject *Sender)
{
  if (dlgFileOpen->Execute())
  {
    InitializeApp();
    if (ImportMapData(dlgFileOpen->FileName.c_str()))
    {
      spnRows->Position = FRows;
      spnColumns->Position = FColumns;
    }
  }
}
//---------------------------------------------------------------------------

/*
  Handler for File save
*/
void __fastcall TfrmMain::actFileSaveExecute(TObject *Sender)
{
  if (dlgFileSaveText->Execute())
    ExportMapData(dlgFileSaveText->FileName.c_str());
}
//---------------------------------------------------------------------------

/*
  Handler for testing the level in the game
*/
void __fastcall TfrmMain::actTestExecute(TObject *Sender)
{
    // Export the current level
  ExportMapData(FCurrFileName.c_str());

    // Launch the game. The name of the game executable is now stored
    // in the .ini file (config file).
  int result =  WinExec(frmConfig->GameExecutable.c_str(), SW_NORMAL);

    // If it failed to launch for some reason, show the error code.
    // TODO: Add a better message regarding the reason it failed.
  if (result < 32)
    ShowMessage(IntToStr(result));
}
//---------------------------------------------------------------------------

/*
  Handler for File New
*/
void __fastcall TfrmMain::actFileNewExecute(TObject *Sender)
{
  InitializeApp();
}
//---------------------------------------------------------------------------

/*
  Handler for toggling the grid on/off
*/
void __fastcall TfrmMain::actShowGridExecute(TObject *Sender)
{
  FShowGrid = !FShowGrid; // toggle
  ChangeGrid(FShowGrid);
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::actHelpAboutExecute(TObject *Sender)
{
  frmAbout->ShowModal();
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::actEditConfigurationExecute(TObject *Sender)
{
  frmConfig->ShowModal();
}
//---------------------------------------------------------------------------


void __fastcall TfrmMain::actEditUndoExecute(TObject *Sender)
{
  PopUndo();
}
//---------------------------------------------------------------------------

void __fastcall TfrmMain::actEditUndoUpdate(TObject *Sender)
{
  actEditUndo->Enabled = !UndoData.empty();
}
//---------------------------------------------------------------------------


//******************************************************************************
// Other event handlers
//******************************************************************************
/*
  When the user changes the number of rows/columns, need to update the
  display.
*/
void __fastcall TfrmMain::OnCellCountChange(TObject *Sender)
{
  FRows = spnRows->Position;
  FColumns = spnColumns->Position;
  AdjustGrid();
}
//---------------------------------------------------------------------------

/*
  Handles the event when the sheet (in the PageControl) is resized.
*/
void __fastcall TfrmMain::shtPierResize(TObject *Sender)
{
  AdjustGrid();
}
//---------------------------------------------------------------------------

/*
  If the splitter bar is moved, the board will be resized and needs to be
  updated. (The cell width/height has changed.)
*/
void __fastcall TfrmMain::splitHorizontalMoved(TObject *Sender)
{
  AdjustGrid();
}
//---------------------------------------------------------------------------

/*
  Handles the event when the user clicks on one of the color buttons on the
  left toolbar.
*/
void __fastcall TfrmMain::ColorChange(TObject *Sender)
{
  TButton *b = (TButton *)Sender;
  FCurrColorID = ColorMap[b->Tag];
}
//---------------------------------------------------------------------------

/*
  If the user drops files from Windows Explorer onto the window, this
  message (WM_DROPFILES) will be sent and handled here.
*/
void __fastcall TfrmMain::WMDropFiles(TWMDropFiles &Message)
{
  int num_files, filename_length;
  char file_name[MAX_PATH + 1];

    // Refer to the Win SDK documentation about the details
    // of DragQueryFile and the HDROP structure

    // Get the number of files that are being dropped on the app
  num_files = DragQueryFile((HDROP)Message.Drop, -1, NULL, 0);

    // Unlikely to happen
  if (num_files < 1)
    return;

    // Retrieve each filename that was dropped
  for(int i = 0; i < num_files; i++)
  {
      // Get the length of the i'th filename
    filename_length = DragQueryFile((HDROP)Message.Drop, i, NULL, 0);

      // Retrieve the filename from the HDROP structure
    DragQueryFile((HDROP)Message.Drop, i, file_name, filename_length + 1);

      // We  can only deal with one at this point
    InitializeApp();
    ImportMapData(file_name);
    short r = FRows;
    short c = FColumns;
    spnRows->Position = r;
    spnColumns->Position = c;
    
    return;
  }
}

/*
Sample data file:

Width 10
Height 10
1 1 1 1 1 1 1 1 1 1
1 0 3 0 0 4 0 4 0 1
1 0 0 0 1 1 1 0 0 1
1 0 0 1 0 4 0 0 0 1
1 0 1 0 0 0 0 0 0 1
1 0 4 0 1 0 0 0 0 1
1 0 0 0 0 1 1 1 1 1
1 0 0 2 0 0 0 4 0 1
1 0 0 0 0 0 0 0 0 1
1 1 1 1 1 1 1 1 1 1
*/
bool TfrmMain::ImportMapData(const AnsiString &FileName)
{
  std::ifstream infile(FileName.c_str());
  if (!infile.is_open())
    return false;

  std::string str;

    // e.g.  Width 20
  infile >> str;      // skip "Width"
  infile >> FColumns; // read width

    // e.g. Height 20
  infile >> str;   // skip "Height"
  infile >> FRows; // read height

  for(int r = 0; r < FRows; r++)
    for(int c = 0; c < FColumns; c++)
      infile >> FDataMap[FRows - r - 1][c];

  return true;
}

bool TfrmMain::ExportMapData(const AnsiString &FileName)
{
  std::ofstream outfile(FileName.c_str());
  if (!outfile.is_open())
    return false;

    // e.g. 'Width 20'
  outfile << "Width ";
  outfile << FColumns;
  outfile << std::endl;

    // e.g. 'Height 20'
  outfile << "Height ";
  outfile << FRows;
  outfile << std::endl;

  for(int r = 0; r < FRows; r++)
  {
    for(int c = 0; c < FColumns; c++)
      outfile << FDataMap[FRows - r - 1][c] << " ";
    outfile << std::endl;
  }
  return true;
}