/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2005-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */
// script-controlled InGame dialog to show player infos

#include "C4Include.h"
#include "gui/C4Scoreboard.h"

#include "gui/C4GameOverDlg.h"
#include "gui/C4Gui.h"
#include "graphics/C4Draw.h"
#include "graphics/C4GraphicsResource.h"


class C4ScoreboardDlg : public C4GUI::Dialog
{
private:
	int32_t *piColWidths;
	C4Scoreboard *pBrd;

	enum { XIndent = 4, YIndent = 4, XMargin = 3, YMargin = 3 };

public:
	C4ScoreboardDlg(C4Scoreboard *pForScoreboard);
	~C4ScoreboardDlg() override;

protected:
	void InvalidateRows() { delete [] piColWidths; piColWidths = nullptr; }
	void Update(); // update row widths and own size and caption

	bool DoPlacement(C4GUI::Screen *pOnScreen, const C4Rect &rPreferredDlgRect) override;
	void Draw(C4TargetFacet &cgo) override;
	void DrawElement(C4TargetFacet &cgo) override;

	const char *GetID() override { return "Scoreboard"; }

	bool IsMouseControlled() override { return false; }

	friend class C4Scoreboard;
};

// ************************************************
// *** C4Scoreboard

void C4Scoreboard::Entry::SwapWith(Entry *pSwap)
{
	Entry swp; swp.Text.Take(std::move(Text)); swp.iVal = iVal;
	Text.Take(std::move(pSwap->Text)); iVal = pSwap->iVal;
	pSwap->Text.Take(std::move(swp.Text)); pSwap->iVal = swp.iVal;
}

void C4Scoreboard::Clear()
{
	// del all cells
	delete [] pEntries;
	pEntries = nullptr; iRows=iCols=0;
	// del dialog
	iDlgShow = 0;
	if (pDlg) { delete pDlg; pDlg = nullptr; }
}

void C4Scoreboard::AddRow(int32_t iInsertBefore)
{
	// counts
	int32_t iNewEntryCount = (iRows+1) * iCols;
	if (!iNewEntryCount) { ++iRows; return; }
	// realloc and copy array
	Entry *pNewEntries = new Entry[iNewEntryCount];
	for (int32_t iRow = 0, iFromRow = 0; iFromRow < iRows; ++iRow,++iFromRow)
	{
		if (iFromRow == iInsertBefore) ++iRow;
		for (int32_t iCol = 0; iCol < iCols; ++iCol)
			pNewEntries[iRow*iCols+iCol].GrabFrom(GetCell(iCol, iFromRow));
	}
	++iRows;
	delete [] pEntries; pEntries = pNewEntries;
}

void C4Scoreboard::AddCol(int32_t iInsertBefore)
{
	// counts
	int32_t iNewEntryCount = iRows * (iCols + 1);
	if (!iNewEntryCount) { ++iCols; return; }
	// realloc and copy array
	Entry *pNewEntries = new Entry[iNewEntryCount];
	for (int32_t iRow = 0; iRow < iRows; ++iRow)
		for (int32_t iCol = 0, iFromCol = 0; iFromCol < iCols; ++iCol, ++iFromCol)
		{
			if (iFromCol == iInsertBefore) ++iCol;
			pNewEntries[iRow*(iCols+1)+iCol].GrabFrom(GetCell(iFromCol, iRow));
		}
	++iCols; // CAUTION: must inc after add, so GetCell won't point into bogus!
	delete [] pEntries; pEntries = pNewEntries;
}

void C4Scoreboard::DelRow(int32_t iDelIndex)
{
	// counts
	int32_t iNewEntryCount = (iRows-1) * iCols;
	if (!iNewEntryCount) { --iRows; delete [] pEntries; pEntries = nullptr; return; }
	// realloc and copy array
	Entry *pNewEntries = new Entry[iNewEntryCount]; Entry *pCpy = pNewEntries;
	for (int32_t iRow = 0, iFromRow = 0; iRow < (iRows-1); ++iRow,++iFromRow)
	{
		if (iFromRow == iDelIndex) ++iFromRow;
		for (int32_t iCol = 0; iCol < iCols; ++iCol)
			pCpy++->GrabFrom(GetCell(iCol, iFromRow));
	}
	--iRows;
	delete [] pEntries; pEntries = pNewEntries;
}

void C4Scoreboard::DelCol(int32_t iDelIndex)
{
	// counts
	int32_t iNewEntryCount = iRows * (iCols-1);
	if (!iNewEntryCount) { --iCols; delete [] pEntries; pEntries = nullptr; return; }
	// realloc and copy array
	Entry *pNewEntries = new Entry[iNewEntryCount]; Entry *pCpy = pNewEntries;
	for (int32_t iRow = 0; iRow < iRows; ++iRow)
		for (int32_t iCol = 0, iFromCol = 0; iCol < (iCols-1); ++iCol,++iFromCol)
		{
			if (iFromCol == iDelIndex) ++iFromCol;
			pCpy++->GrabFrom(GetCell(iFromCol, iRow));
		}
	--iCols; // CAUTION: must dec after add, so GetCell won't point into bogus!
	delete [] pEntries; pEntries = pNewEntries;
}

void C4Scoreboard::SwapRows(int32_t iRow1, int32_t iRow2)
{
	Entry *pXChg1 = pEntries+iRow1*iCols;
	Entry *pXChg2 = pEntries+iRow2*iCols;
	int32_t i = iCols; while (i--) pXChg1++->SwapWith(pXChg2++);
}

int32_t C4Scoreboard::GetColByKey(int32_t iKey) const
{
	// safety
	if (!iRows) return -1;
	// check all col headers
	Entry *pCheck = pEntries;
	for (int32_t i = 0; i < iCols; ++i)
		if (pCheck++->iVal == iKey) return i;
	return -1;
}

int32_t C4Scoreboard::GetRowByKey(int32_t iKey) const
{
	// safety
	if (!iCols) return -1;
	// check all row headers
	Entry *pCheck = pEntries;
	for (int32_t i = 0; i < iRows; ++i,(pCheck+=iCols))
		if (pCheck->iVal == iKey) return i;
	return -1;
}

void C4Scoreboard::SetCell(int32_t iColKey, int32_t iRowKey, const char *szValue, int32_t iValue)
{
	// ensure primary row/col exists
	if (!iCols || !iRows)
	{
		if (!iCols) AddCol(0);
		if (!iRows) AddRow(0);
		GetCell(0, 0)->iVal = TitleKey;
	}
	// get row/col; create new if not yet existing
	int32_t iCol = GetColByKey(iColKey);
	if (iCol<0) { AddCol(iCol=iCols); GetCell(iCol, 0)->iVal = iColKey; }
	int32_t iRow = GetRowByKey(iRowKey);
	if (iRow<0) { AddRow(iRow=iRows); GetCell(0, iRow)->iVal = iRowKey; }
	// now set values
	Entry *pCell = GetCell(iCol, iRow);
	pCell->Text.Copy(szValue);
	if (iCol && iRow) pCell->iVal = iValue; // do NOT overwrite index keys!
	// if an empty value was set, prune empty
	if (!szValue || !*szValue)
	{
		// prune empty row (not label row)
		int32_t i;
		if (iRow)
		{
			for (i=1; i<iCols; ++i) if (GetCell(i, iRow)->Text) break;
			if (i == iCols) DelRow(iRow);
		}
		// prune empty col (not label col)
		if (iCol)
		{
			for (i=1; i<iRows; ++i) if (GetCell(iCol, i)->Text) break;
			if (i == iRows) DelCol(iCol);
		}
	}
	/*  // prune empty board? but this would prevent boards that just sort
	  if (iRows == 1 && iCols == 1)
	    Clear(); // must not do this, because it will del pDlg
	  else*/
	// recalc row widths in display (else done by clear)
	InvalidateRows();
}

const char *C4Scoreboard::GetCellString(int32_t iColKey, int32_t iRowKey)
{
	// get row/col
	int32_t iCol = GetColByKey(iColKey);
	int32_t iRow = GetRowByKey(iRowKey);
	if (iCol<0 || iRow<0) return nullptr;
	// now get value
	Entry *pCell = GetCell(iCol, iRow);
	return pCell->Text.getData();
}

int32_t C4Scoreboard::GetCellData(int32_t iColKey, int32_t iRowKey)
{
	// get row/col
	int32_t iCol = GetColByKey(iColKey);
	int32_t iRow = GetRowByKey(iRowKey);
	if (iCol<0 || iRow<0) return 0;
	// now get value
	Entry *pCell = GetCell(iCol, iRow);
	return pCell->iVal;
}

void C4Scoreboard::RemoveCol(int32_t iColKey)
{
	int32_t iCol = GetColByKey(iColKey);
	if (iCol>=0) DelCol(iCol);
	InvalidateRows(); // recalc row widths in display
}

void C4Scoreboard::RemoveRow(int32_t iRowKey)
{
	int32_t iRow = GetRowByKey(iRowKey);
	if (iRow>=0) DelRow(iRow);
	InvalidateRows(); // recalc row widths in display
}

bool C4Scoreboard::SortBy(int32_t iColKey, bool fReverse)
{
	// get sort col
	int32_t iCol = GetColByKey(iColKey);
	if (iCol<0) return false;
	// sort
	int32_t iSortDir = fReverse ? -1 : +1;
	int32_t iSortBegin=1, iSortEnd=iRows-1;
	while (iSortBegin < iSortEnd)
	{
		int32_t iNewBorder = iSortBegin; int32_t i;
		for (i = iSortBegin; i < iSortEnd; ++i)
			if (GetCell(iCol, i)->iVal * iSortDir > GetCell(iCol, i+1)->iVal * iSortDir)
			{
				SwapRows(i, i+1);
				iNewBorder = i;
			}
		iSortEnd = iNewBorder;
		for (i = iSortEnd; i > iSortBegin; --i)
			if (GetCell(iCol, i-1)->iVal * iSortDir > GetCell(iCol, i)->iVal * iSortDir)
			{
				SwapRows(i-1, i);
				iNewBorder = i;
			}
		iSortBegin = iNewBorder;
	}
	return true;
}

void C4Scoreboard::InvalidateRows()
{
	// recalculate row sizes
	if (pDlg) pDlg->InvalidateRows();
}

void C4Scoreboard::DoDlgShow(int32_t iChange, bool fUserToggle)
{
	if (::pGUI->IsExclusive()) return;
	// update dlg show
	iDlgShow += iChange;
	if (!fUserToggle)
		// script update: Dlg on off if iDlgShow variable passed zero
		fUserToggle = (ShouldBeShown() == !pDlg);
	else
		// user pressed Tab: Always toggle except if the scoreboard cannot be shown at all
		if (!CanBeShown() && !pDlg) fUserToggle = false;
	if (fUserToggle)
	{
		if (!pDlg)
		{
			if (!C4GameOverDlg::IsShown()) // never show during game over dlg
				::pGUI->ShowRemoveDlg(pDlg = new C4ScoreboardDlg(this));
		}
		else
			pDlg->Close(false);
	}
}

void C4Scoreboard::HideDlg()
{
	if (::pGUI->IsExclusive()) return;
	// hide scoreboard if it was active
	if (pDlg) pDlg->Close(false);
}

void C4Scoreboard::CompileFunc(StdCompiler *pComp)
{
	bool deserializing = pComp->isDeserializer();
	if (deserializing) Clear();
	pComp->Value(mkNamingAdapt(iRows,     "Rows",     0));
	pComp->Value(mkNamingAdapt(iCols,     "Cols",     0));
	pComp->Value(mkNamingAdapt(iDlgShow,  "DlgShow",  0));
	if (iRows * iCols)
	{
		if (deserializing) pEntries = new Entry[iRows * iCols];
		for (int32_t iRow = 0; iRow < iRows; ++iRow)
			for (int32_t iCol = 0; iCol < iCols; ++iCol)
			{
				Entry *pEnt = GetCell(iCol, iRow);
				pComp->Value(mkNamingAdapt(pEnt->Text, FormatString("Cell%i_%iString", iCol, iRow).getData()));
				pComp->Value(mkNamingAdapt(pEnt->iVal, FormatString("Cell%i_%iValue", iCol, iRow).getData()));
			}
		// recheck dlg show in read mode
		// will usually not do anything, because reading is done before enetering shared mode
		if (pComp->isDeserializer()) DoDlgShow(0, false);
	}
}


// ************************************************
// *** C4ScoreboardDlg

C4ScoreboardDlg::C4ScoreboardDlg(C4Scoreboard *pForScoreboard)
		: C4GUI::Dialog(100, 100, "nops", false), piColWidths(nullptr), pBrd(pForScoreboard)
{
	Update();
}

C4ScoreboardDlg::~C4ScoreboardDlg()
{
	delete [] piColWidths;
	pBrd->pDlg = nullptr;
}

void C4ScoreboardDlg::Update()
{
	// counts
	int32_t iRowCount = pBrd->iRows; int32_t iColCount = pBrd->iCols;
	delete [] piColWidths; piColWidths = nullptr;
	// invalid board - scipters can create those, but there's no reason why the engine
	// should display something pretty then; just keep dialog defaults
	if (!iRowCount || !iColCount) return;
	// calc sizes as max col widths plus some indent pixels
	piColWidths = new int32_t[iColCount];
	int32_t iWdt=XMargin*2, iHgt=YMargin*2;
	for (int32_t iCol = 0; iCol < iColCount; ++iCol)
	{
		piColWidths[iCol] = XIndent;
		for (int32_t iRow = 0; iRow < iRowCount; ++iRow)
		{
			C4Scoreboard::Entry *pCell = pBrd->GetCell(iCol, iRow);
			if ((iRow || iCol) && !pCell->Text.isNull()) piColWidths[iCol] = std::max<int32_t>(piColWidths[iCol], ::GraphicsResource.FontRegular.GetTextWidth(pCell->Text.getData()) + XIndent);
		}
		iWdt += piColWidths[iCol];
	}
	iHgt += iRowCount * (::GraphicsResource.FontRegular.GetLineHeight() + YIndent);
	const char *szTitle = pBrd->GetCell(0,0)->Text.getData();
	if (szTitle) iWdt = std::max<int32_t>(iWdt, ::GraphicsResource.FontRegular.GetTextWidth(szTitle) + 40);
	if (!pTitle != !szTitle) SetTitle(szTitle); // needed for title margin...
	iWdt += GetMarginLeft() + GetMarginRight();
	iHgt += GetMarginTop() + GetMarginBottom();
	// update dialog
	SetBounds(C4Rect(rcBounds.x, rcBounds.y, iWdt, iHgt));
	SetTitle(szTitle);
	if (szTitle && pTitle) pTitle->SetIcon(C4GUI::Icon::GetIconFacet(C4GUI::Ico_Player));
	// realign
	C4GUI::Screen *pScr = GetScreen();
	if (pScr) DoPlacement(pScr, pScr->GetPreferredDlgRect());
}

bool C4ScoreboardDlg::DoPlacement(C4GUI::Screen *pOnScreen, const C4Rect &rPreferredDlgRect)
{
	// align topright
	SetPos(rPreferredDlgRect.x + rPreferredDlgRect.Wdt - rcBounds.Wdt - 20, rPreferredDlgRect.y + 38);
	return true;
}

void C4ScoreboardDlg::Draw(C4TargetFacet &cgo)
{
	if (!piColWidths) Update();
	typedef C4GUI::Dialog ParentClass;
	ParentClass::Draw(cgo);
}

void C4ScoreboardDlg::DrawElement(C4TargetFacet &cgo)
{
	typedef C4GUI::Dialog ParentClass;
	ParentClass::DrawElement(cgo);
	// draw spreadsheet
	int32_t iRowCount = pBrd->iRows; int32_t iColCount = pBrd->iCols;
	int32_t iY = YMargin + int32_t(cgo.TargetY) + rcClientRect.y;
	for (int32_t iRow = 0; iRow < iRowCount; ++iRow)
	{
		int32_t iX = XMargin + int32_t(cgo.TargetX) + rcClientRect.x;
		for (int32_t iCol = 0; iCol < iColCount; ++iCol)
		{
			const char *szText = pBrd->GetCell(iCol, iRow)->Text.getData();
			if (szText && *szText && (iRow || iCol))
				pDraw->TextOut(szText, ::GraphicsResource.FontRegular, 1.0, cgo.Surface, iCol ? iX + piColWidths[iCol]/2 : iX, iY, 0xffffffff, iCol ? ACenter : ALeft);
			iX += piColWidths[iCol];
		}
		iY += ::GraphicsResource.FontRegular.GetLineHeight() + YIndent;
	}
}
