//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
//=============================================================================

#include "stdafx.h"

#include "vgui_controls/TreeView.h"
#include "vgui_controls/ListPanel.h"
#include "vgui_controls/SectionedListPanel.h"
#include "vgui_controls/ComboBox.h"
#include "vgui_controls/PropertySheet.h"
#include "vgui_controls/PropertyPage.h"

#include <ctype.h>

using namespace vgui;
extern IP4* p4;

// list of all tree view icons
enum
{
	IMAGE_FOLDER = 1,
	IMAGE_OPENFOLDER,
	IMAGE_FILE,
};


//-----------------------------------------------------------------------------
// Purpose: Handles file view
//-----------------------------------------------------------------------------
class CFileTreeView : public TreeView
{
	DECLARE_CLASS_SIMPLE( CFileTreeView, TreeView );
public:
	CFileTreeView(Panel *parent, const char *name) : BaseClass(parent, name)
	{
	}

	// override to incremental request and show p4 directories
	virtual void GenerateChildrenOfNode(int itemIndex)
	{
		KeyValues *pkv = GetItemData(itemIndex);
		if (!pkv->GetInt("dir"))
			return;

		const char *pFilePath = pkv->GetString("path", "");
		if (!pFilePath[0])
			return;

		surface()->SetCursor(dc_waitarrow);
		// get the list of files
		CUtlVector<P4File_t> &files = p4->GetFileList(pFilePath);

		// generate children
		// add all the items
		for (int i = 0; i < files.Count(); i++)
		{
			if (files[i].m_bDeleted)
				continue;

			KeyValues *kv = new KeyValues("node", "text", p4->String( files[i].m_sName ) );

			if (files[i].m_bDir)
			{
				kv->SetInt("expand", 1);
				kv->SetInt("dir", 1);
				kv->SetInt("image", IMAGE_FOLDER);
			}
			else
			{
				kv->SetInt("image", IMAGE_FILE);
			}

			// get the files path
			char szPath[MAX_PATH];
			if (files[i].m_bDir)
			{
				Q_snprintf(szPath, sizeof(szPath), "%s/%s/%%%%1", p4->String( files[i].m_sPath ), p4->String( files[i].m_sName ) );
			}
			else
			{
				Q_snprintf(szPath, sizeof(szPath), "%s/%s", p4->String( files[i].m_sPath ), p4->String( files[i].m_sName ));
			}

			// translate the files path from a depot path into a local path
			char szLocalPath[MAX_PATH];
			p4->GetLocalFilePath(szLocalPath, szPath, sizeof(szLocalPath));
			kv->SetString("path", szLocalPath);

			// now change the items text to match the local paths file name...
			char *pLocalName = Q_strrchr(szLocalPath, '\\');
			if (pLocalName)
			{
				*pLocalName = 0;
				++pLocalName;
			}
			kv->SetString("text", pLocalName);

			int itemID = AddItem(kv, itemIndex);

			// mark out of date files in red
			if (files[i].m_iHaveRevision < files[i].m_iHeadRevision)
			{
				SetItemFgColor(itemID, Color(255, 0, 0, 255));
			}
		}
	}

	// setup a context menu whenever a directory is clicked on
	virtual void GenerateContextMenu( int itemIndex, int x, int y ) 
	{
		KeyValues *pkv = GetItemData(itemIndex);
		const char *pFilePath = pkv->GetString("path", "");
		if (!pFilePath[0])
			return;

		Menu *pContext = new Menu(this, "FileContext");

		if (pkv->GetInt("dir"))
		{
			pContext->AddMenuItem("Cloak folder", new KeyValues("CloakFolder", "item", itemIndex), GetParent(), NULL);
		}
		else
		{
			pContext->AddMenuItem("Open for edit", new KeyValues("EditFile", "item", itemIndex), GetParent(), NULL);
			pContext->AddMenuItem("Open for delete", new KeyValues("DeleteFile", "item", itemIndex), GetParent(), NULL);
		}

		// show the context menu
		pContext->SetPos(x, y);
		pContext->SetVisible(true);
	}



	// setup a smaller font
	virtual void ApplySchemeSettings(IScheme *pScheme)
	{
		BaseClass::ApplySchemeSettings(pScheme);
		SetFont( pScheme->GetFont("DefaultSmall") );
	}
};

//-----------------------------------------------------------------------------
// Purpose: Revision list
//-----------------------------------------------------------------------------
class CSmallTextListPanel : public ListPanel
{
	DECLARE_CLASS_SIMPLE( CSmallTextListPanel, ListPanel );
public:
	CSmallTextListPanel(Panel *parent, const char *name) : BaseClass(parent, name)
	{
	}

	virtual void ApplySchemeSettings(IScheme *pScheme)
	{
		BaseClass::ApplySchemeSettings(pScheme);

		SetFont( pScheme->GetFont("DefaultSmall") );
	}
};

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CVP4Dialog::CVP4Dialog() : BaseClass(NULL, "vp4dialog"), m_Images(false)
{
	SetSize(1024, 768);
	SetTitle("VP4", true);

	// clientspec selection
	m_pClientCombo = new ComboBox(this, "ClientCombo", 16, false);

	// file browser tree controls
	m_pFileTree = new CFileTreeView(this, "FileTree");
	// build our list of images
	m_Images.AddImage(scheme()->GetImage("resource/icon_folder", false));
	m_Images.AddImage(scheme()->GetImage("resource/icon_folder_selected", false));
	m_Images.AddImage(scheme()->GetImage("resource/icon_file", false));
	m_pFileTree->SetImageList(&m_Images, false);

	// property sheet - revisions, changes, etc.
	m_pViewsSheet = new PropertySheet(this, "ViewsSheet");

	// changes
	m_pChangesPage = new PropertyPage(m_pViewsSheet, "ChangesPage");
	m_pViewsSheet->AddPage(m_pChangesPage, "Changes");
	m_pChangesList = new SectionedListPanel(m_pChangesPage, "ChangesList");

	// layout changes
	int x, y, wide, tall;
	m_pChangesPage->GetBounds(x, y, wide, tall);
	m_pChangesList->SetAutoResize( Panel::PIN_TOPLEFT, Panel::AUTORESIZE_DOWNANDRIGHT, 6, 6, -12, -12 );

	// revisions
	m_pRevisionsPage = new PropertyPage(m_pViewsSheet, "RevisionsPage");
	m_pViewsSheet->AddPage(m_pRevisionsPage, "History");
	m_pRevisionList = new CSmallTextListPanel(m_pRevisionsPage, "RevisionList");
	m_pRevisionList->SetEmptyListText("No file or directory currently selected.");
	m_pRevisionList->AddColumnHeader(0, "change", "change", 52, ListPanel::COLUMN_HIDDEN);
	m_pRevisionList->AddColumnHeader(1, "date", "date", 52, 0);
	m_pRevisionList->AddColumnHeader(2, "time", "time", 52, ListPanel::COLUMN_HIDDEN);
	m_pRevisionList->AddColumnHeader(3, "user", "user", 64, 0);
	m_pRevisionList->AddColumnHeader(4, "description", "description", 32, ListPanel::COLUMN_RESIZEWITHWINDOW);
	m_pRevisionList->SetAllowUserModificationOfColumns(true);

	// layout revisions
	m_pRevisionsPage->GetBounds(x, y, wide, tall);
	m_pRevisionList->SetAutoResize( Panel::PIN_TOPLEFT, Panel::AUTORESIZE_DOWNANDRIGHT, 6, 6, -12, -12 );

	LoadControlSettingsAndUserConfig("//PLATFORM/resource/vp4dialog.res");
}

//-----------------------------------------------------------------------------
// Purpose: Destructor
//-----------------------------------------------------------------------------
CVP4Dialog::~CVP4Dialog()
{
}

//-----------------------------------------------------------------------------
// Purpose: stops app on close
//-----------------------------------------------------------------------------
void CVP4Dialog::OnClose()
{
	BaseClass::OnClose();
	ivgui()->Stop();
}

//-----------------------------------------------------------------------------
// Purpose: called to open
//-----------------------------------------------------------------------------
void CVP4Dialog::Activate()
{
	BaseClass::Activate();

	char szTitle[256];
	Q_snprintf(szTitle, sizeof(szTitle), "VP4 - %s", p4->String( p4->GetActiveClient().m_sUser ));

	SetTitle(szTitle, true);

	RefreshFileList();
	RefreshClientList();
	RefreshChangesList();

	p4->GetClientList();
}

//-----------------------------------------------------------------------------
// Purpose: Refreshes the active file list
//-----------------------------------------------------------------------------
void CVP4Dialog::RefreshFileList()
{
	m_pFileTree->RemoveAll();
	m_pFileTree->SetFgColor(Color(216, 222, 211, 255));

	// add the base node
	KeyValues *pkv = new KeyValues("root");
	pkv->SetString("text", p4->String( p4->GetActiveClient().m_sLocalRoot ));
	pkv->SetString("path", p4->String( p4->GetActiveClient().m_sLocalRoot ));
	pkv->SetInt("dir", 1);
	int iRoot = m_pFileTree->AddItem(pkv, m_pFileTree->GetRootItemIndex());
	m_pFileTree->ExpandItem(iRoot, true);
}


//-----------------------------------------------------------------------------
// Purpose: utility function used in qsort
//-----------------------------------------------------------------------------
int __cdecl IntSortFunc(const void *elem1, const void *elem2)
{
	if ( *((int *)elem1) < *((int *)elem2) )
		return -1;
	else if ( *((int *)elem1) > *((int *)elem2) )
		return 1;

	return 0;
}

//-----------------------------------------------------------------------------
// Purpose: rebuilds the list of changes
//-----------------------------------------------------------------------------
void CVP4Dialog::RefreshChangesList()
{
	m_pChangesList->RemoveAll();
	m_pChangesList->RemoveAllSections();

	CUtlVector<P4File_t> files;
	p4->GetOpenedFileList( files );
	CUtlVector<int> sections;

	// find out all the changelists
	for (int i = 0; i < files.Count(); i++)
	{
		if (!sections.IsValidIndex(sections.Find(files[i].m_iChangelist)))
		{
			// add a new section
			sections.AddToTail(files[i].m_iChangelist);
		}
	}

	// sort the changelists
	qsort(sections.Base(), sections.Count(), sizeof(int), &IntSortFunc);

	// add the changeslists
	{for (int i = 0; i < sections.Count(); i++)
	{
		m_pChangesList->AddSection(sections[i], "");

		char szChangelistName[256];
		if (sections[i] > 0)
		{
			Q_snprintf(szChangelistName, sizeof(szChangelistName), "CHANGE: %d", sections[i]);
		}
		else
		{
			Q_snprintf(szChangelistName, sizeof(szChangelistName), "CHANGE: DEFAULT");
		}

		m_pChangesList->AddColumnToSection(sections[i], "file", szChangelistName, SectionedListPanel::COLUMN_BRIGHT, 512);
	}}

	// add the files to the changelists
	{for (int i = 0; i < files.Count(); i++)
	{
		char szFile[_MAX_PATH];
		Q_snprintf(szFile, sizeof(szFile), "%s/%s", p4->String( files[i].m_sPath ) + p4->GetDepotRootLength(), p4->String( files[i].m_sName ));
		KeyValues *pkv = new KeyValues("node", "file", szFile);
		m_pChangesList->AddItem(files[i].m_iChangelist, pkv);
	}}
}

//-----------------------------------------------------------------------------
// Purpose: Refreshes the client selection combo
//-----------------------------------------------------------------------------
void CVP4Dialog::RefreshClientList()
{
	m_pClientCombo->RemoveAll();
	
	CUtlVector<P4Client_t> &clients = p4->GetClientList();
	P4Client_t &activeClient = p4->GetActiveClient();

	// go through all clients
	for (int i = 0; i < clients.Count(); i++)
	{
		// only add clients that are defined for this machine (host)
		if (clients[i].m_sHost != activeClient.m_sHost)
			continue;

		// add in the new item
		char szText[256];
		Q_snprintf(szText, sizeof(szText), "%s    %s", p4->String( clients[i].m_sName ), p4->String( clients[i].m_sLocalRoot ));
		m_pClientCombo->AddItem(szText, new KeyValues("client", "client", p4->String( clients[i].m_sName )));
	}

	m_pClientCombo->SetText( p4->String( activeClient.m_sName ));

	if (m_pClientCombo->GetItemCount() > 1)
	{
		m_pClientCombo->SetEnabled(true);
	}
	else
	{
		m_pClientCombo->SetEnabled(false);
	}
}

//-----------------------------------------------------------------------------
// Purpose: refreshes dialog on text changing
//-----------------------------------------------------------------------------
void CVP4Dialog::OnTextChanged()
{
	KeyValues *pkv = m_pClientCombo->GetActiveItemUserData();
	if (!pkv)
		return;

	// set the new client
	p4->SetActiveClient(pkv->GetString("client"));

	// refresh the UI
	Activate();
	m_pRevisionList->RemoveAll();
}

//-----------------------------------------------------------------------------
// Purpose: Cloaks a folder
//-----------------------------------------------------------------------------
void CVP4Dialog::CloakFolder(int iItem)
{
	KeyValues *pkv = m_pFileTree->GetItemData(iItem);
	if (!pkv)
		return;

	// change the clientspec to remove the folder
	p4->RemovePathFromActiveClientspec(pkv->GetString("path"));

	// remove the folder from the tree view
	m_pFileTree->RemoveItem(0-iItem, false);
	m_pFileTree->InvalidateLayout();
	m_pFileTree->Repaint();
}

//-----------------------------------------------------------------------------
// Purpose: open for edit
//-----------------------------------------------------------------------------
void CVP4Dialog::OpenFileForEdit(int iItem)
{
	KeyValues *pkv = m_pFileTree->GetItemData(iItem);
	if (!pkv)
		return;

	p4->OpenFileForEdit(pkv->GetString("path"));

	RefreshChangesList();
}

//-----------------------------------------------------------------------------
// Purpose: open for delete
//-----------------------------------------------------------------------------
void CVP4Dialog::OpenFileForDelete(int iItem)
{
	KeyValues *pkv = m_pFileTree->GetItemData(iItem);
	if (!pkv)
		return;

	p4->OpenFileForDelete(pkv->GetString("path"));

	RefreshChangesList();
}

//-----------------------------------------------------------------------------
// Purpose: updates revision view on a file being selected
//-----------------------------------------------------------------------------
void CVP4Dialog::OnFileSelected()
{
	m_pRevisionList->RemoveAll();

	// only update if reviews page is visible
	if (!m_pRevisionsPage->IsVisible())
		return;

	// update list
	int iItem = m_pFileTree->GetFirstSelectedItem();
	if (iItem < 0)
	{
		m_pRevisionList->SetEmptyListText("No file or directory currently selected.");
		return;
	}

	surface()->SetCursor(dc_waitarrow);

	m_pRevisionList->SetEmptyListText("There is no revision history for the selected file.");

	KeyValues *pkv = m_pFileTree->GetItemData(iItem);

	CUtlVector<P4Revision_t> &revisions = p4->GetRevisionList(pkv->GetString("path"), pkv->GetInt("dir"));

	// add all the revisions
	for (int i = 0; i < revisions.Count(); i++)
	{
		KeyValues *pkv = new KeyValues("node");
		pkv->SetString("user", p4->String( revisions[i].m_sUser ));
		pkv->SetInt("change", revisions[i].m_iChange);

		char szDate[256];
		Q_snprintf(szDate, sizeof(szDate), "%d/%d/%d", revisions[i].m_nYear, revisions[i].m_nMonth, revisions[i].m_nDay);
		pkv->SetString("date", szDate);

		 char szTime[256];
		 Q_snprintf(szTime, sizeof(szTime), "%d:%d:%d", revisions[i].m_nHour, revisions[i].m_nMinute, revisions[i].m_nSecond);
		 pkv->SetString("time", szTime);

		// take just the first line of the description
		char *pDesc = revisions[i].m_Description.GetForModify();
		// move to the first non-whitespace
		while (*pDesc && (isspace(*pDesc) || iscntrl(*pDesc)))
		{
			++pDesc;
		}

		char szShortDescription[256];
		Q_strncpy(szShortDescription, pDesc, sizeof(szShortDescription));
		
		// truncate to last terminator
		char *pTerm = szShortDescription;
		while (*pTerm && !iscntrl(*pTerm))
		{
			++pTerm;
		}
		*pTerm = 0;

		pkv->SetString("description", szShortDescription);

		m_pRevisionList->AddItem(pkv, 0, false, false);
	}
}