/*
 * Copyright (c) 2001, 2002, 2003 Gary R. Van Sickle.
 * Copyright (c) 2005-2006 Tom N Harris.
 *
 *     This program is free software; you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation; either version 2 of the License, or
 *     (at your option) any later version.
 *
 *     A copy of the GNU General Public License can be found at
 *     http://www.gnu.org/
 *
 * Written by Gary R. Van Sickle <g.r.vansickle@worldnet.att.net>
 * Written by Tom N Harris <telliamed@whoopdedo.org>
 *
 */

// This is the implementation of the PropertySheet class.  This class encapsulates
// a Windows property sheet / wizard and interfaces with the PropertyPage class.

#include "PropSheet.h"
#include "PropPage.h"
#include "resource.h"
#include "ControlAdjuster.h"

#include <shlwapi.h>

#define PROPSHEETHEADER_V1_SIZE 40

// Sort of a "hidden" Windows structure.  Used in the PropSheetCallback.
#include <pshpack1.h>
typedef struct DLGTEMPLATEEX
{
	WORD dlgVer;
	WORD signature;
	DWORD helpID;
	DWORD exStyle;
	DWORD style;
	WORD cDlgItems;
	short x;
	short y;
	short cx;
	short cy;
}
DLGTEMPLATEEX, *LPDLGTEMPLATEEX;
#include <poppack.h>

using namespace Setup;

PropertySheet::PropertySheet()
{
}

PropertySheet::~PropertySheet()
{
}

HPROPSHEETPAGE *
PropertySheet::createPages()
{
	HPROPSHEETPAGE *retarray;

	// Create the return array
	retarray = new HPROPSHEETPAGE[m_propertyPages.size()];

	// Create the pages with CreatePropertySheetPage().
	// We do it here rather than in the PropertyPages themselves
	// because, for reasons known only to Microsoft, these handles will be
	// destroyed by the property sheet before the PropertySheet() call returns,
	// at least if it's modal (don't know about modeless).
	PageContainer::iterator i;
	HPROPSHEETPAGE * p = retarray;
	for (i = m_propertyPages.begin(); i != m_propertyPages.end(); ++i)
	{
		*p++ = ::CreatePropertySheetPage((*i)->getPROPSHEETPAGEPtr());

		// Set position info
		if (i == m_propertyPages.begin())
		{
			(*i)->youAreFirst();
		}
		if (i == m_propertyPages.end() - 1)
		{
			(*i)->youAreLast();
		}
	}

	return retarray;
}

// Stuff needed by the PropertySheet wndproc hook
struct PropertySheetData
{
	WNDPROC oldWndProc;
	bool clientRectValid;
	RECTWrapper lastClientRect;
	bool gotPage;
	RECTWrapper pageRect;
	bool hasMinRect;
	RECTWrapper minRect;

	PropertySheetData()
	{
		oldWndProc = 0;
		clientRectValid = false;
		gotPage = false;
		hasMinRect = false;
	}
  
// @@@ Ugly. Really only works because only one PS is used now.
	static PropertySheetData& Instance()
	{
		static PropertySheetData TheInstance;
		return TheInstance;  
	}
};

static ControlAdjuster::ControlInfo PropertySheetControlsInfo[] = {
	{0x3023, CP_RIGHT,   CP_BOTTOM},	// Back
	{0x3024, CP_RIGHT,   CP_BOTTOM},	// Next
	{0x3025, CP_RIGHT,   CP_BOTTOM},	// Finish
	{0x3026, CP_STRETCH, CP_BOTTOM},	// Line above buttons
	{     2, CP_RIGHT,   CP_BOTTOM},	// Cancel
	{0, CP_LEFT, CP_TOP}
};

static bool IsDialog(HWND hwnd)
{
	char className[7];
	::GetClassNameA(hwnd, className, sizeof(className));
	return (::strcmp(className, "#32770") == 0);
}

BOOL CALLBACK ResizePagesEnum(HWND hwnd, LPARAM lParam)
{
	// Is it really a dialog?
	if (IsDialog(hwnd))
	{
		PropertySheetData& psd = PropertySheetData::Instance();
		::SetWindowPos(hwnd, 0, psd.pageRect.left, psd.pageRect.top, 
			       psd.pageRect.width(), psd.pageRect.height(), 
			       SWP_NOACTIVATE | SWP_NOZORDER);
	}

	return TRUE;
}

static LRESULT CALLBACK PropertySheetWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	PropertySheetData& psd = PropertySheetData::Instance();
	switch (uMsg)
	{
	  case WM_SIZE:
	  {
		RECTWrapper clientRect;
		::GetClientRect(hwnd, &clientRect);
	
		/*
		  When the window is minimized, the client rect is reduced to 
		  (0,0-0,0), which causes child adjusting to screw slightly up. Work 
		  around by not adjusting child upon minimization - it isn't really
		  needed then, anyway.
		 */
		if (wParam != SIZE_MINIMIZED)
		{
			/*
			  The first time we get a WM_SIZE, the client rect will be all zeros.
			*/
			if (psd.clientRectValid)
			{
				const int dX = clientRect.width() - psd.lastClientRect.width();
				const int dY = clientRect.height() - psd.lastClientRect.height();
			  
				ControlAdjuster::AdjustControls(hwnd, PropertySheetControlsInfo, dX, dY);
			
				psd.pageRect.right += dX;
				psd.pageRect.bottom += dY;
			  
				/*
				  The pages are child windows, but don't have IDs.
				  So change them by enumerating all childs and adjust all 
				  dialogs among them.
				 */
				if (psd.gotPage)
					::EnumChildWindows(hwnd, &ResizePagesEnum, 0);	
			}
			else
			{
				psd.clientRectValid = true;
			}
			/*
			  Store away the current size and use it as the minmal window size.
			*/
			if (!psd.hasMinRect)
			{
				::GetWindowRect(hwnd, &psd.minRect);
				psd.hasMinRect = true;
			}

			psd.lastClientRect = clientRect;
		}
		break;
	  }
	  case WM_GETMINMAXINFO:
	  {
		if (psd.hasMinRect)
		{
			LPMINMAXINFO mmi = reinterpret_cast<LPMINMAXINFO>(lParam);
			mmi->ptMinTrackSize.x = psd.minRect.width();
			mmi->ptMinTrackSize.y = psd.minRect.height();
		}
		break;
	  }
	}
  
	return ::CallWindowProc(psd.oldWndProc, hwnd, uMsg, wParam, lParam);
}

static int CALLBACK
PropertySheetProc(HWND hwndDlg, UINT uMsg, LPARAM lParam)
{
	switch (uMsg)
	{
	  case PSCB_PRECREATE:
	  {
		const LONG additionalStyle = (WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME);
		// Add a minimize box to the sheet/wizard.
		if (reinterpret_cast<LPDLGTEMPLATEEX>(lParam)->signature == 0xFFFF)
		{
			reinterpret_cast<LPDLGTEMPLATEEX>(lParam)->style |= additionalStyle;
		}
		else
		{
			reinterpret_cast<LPDLGTEMPLATE>(lParam)->style |= additionalStyle;
		}
		return TRUE;
	  }
	  case PSCB_INITIALIZED:
	  {
		/*
		  Hook into the window proc.
		  We need to catch some messages for resizing.
		 */
		PropertySheetData::Instance().oldWndProc = 
			reinterpret_cast<WNDPROC>(::GetWindowLongPtr(hwndDlg, GWLP_WNDPROC));
		::SetWindowLongPtr(hwndDlg, GWLP_WNDPROC, 
			reinterpret_cast<LONG_PTR>(&PropertySheetWndProc));
		return TRUE;
	  }
	}
	return TRUE;
}

static DWORD
GetPROPSHEETHEADERSize()
{
	// For compatibility with all versions of comctl32.dll, we have to do this.

	DLLVERSIONINFO vi;
	HMODULE mod;
	DLLGETVERSIONPROC DllGetVersion;
	DWORD retval = 0;

	// This 'isn't safe' in a DLL, according to MSDN
	mod = ::LoadLibrary("comctl32.dll");

	DllGetVersion = reinterpret_cast<DLLGETVERSIONPROC>(::GetProcAddress(mod, "DllGetVersion"));
	if (DllGetVersion == NULL)
	{
		// Something's wildly broken, punt.
		retval = PROPSHEETHEADER_V1_SIZE;
	}
	else
	{
		vi.cbSize = sizeof(DLLVERSIONINFO);
		DllGetVersion(&vi);

		if ((vi.dwMajorVersion < 4)
		 || ((vi.dwMajorVersion == 4) && (vi.dwMinorVersion < 71)))
		{
			// Recent.
			retval = sizeof(PROPSHEETHEADER);
		}
		else
		{
			// Old (== Win95/NT4 w/o IE 4 or better)
			retval = PROPSHEETHEADER_V1_SIZE;
		}
	}

	::FreeLibrary(mod);

	return retval;
}

bool
PropertySheet::create(const Window * Parent, DWORD Style)
{
	PROPSHEETHEADER p;

	m_pageHandles = createPages();

	p.dwSize = GetPROPSHEETHEADERSize ();
	p.dwFlags = PSH_NOAPPLYNOW | PSH_WIZARD | PSH_USECALLBACK
		    /*| PSH_MODELESS */ | PSH_USEICONID;
	p.hwndParent = (Parent) ? Parent->getHWND() : NULL;
	p.hInstance = getInstance();
	p.nPages = m_propertyPages.size();
	p.pszIcon = MAKEINTRESOURCE(IDI_SETUP);
	p.nStartPage = 0;
	p.phpage = m_pageHandles;
	p.pfnCallback = PropertySheetProc;


	// The winmain event loop actually resides in here.
	::DoPropertySheet(&p);

	// Do a modeless property sheet...
	//setHWND((HWND)PropertySheet(&p));
	/*
	show(SW_SHOWNORMAL);

	// ...but pretend it's modal
	messageLoop();
	::MessageBox(NULL, "DONE", NULL, MB_OK);

	// FIXME: Enable the parent before destroying this window to prevent another window
	// from becoming the foreground window
	// ala: EnableWindow(<parent_hwnd>, TRUE);
	//::DestroyWindow(WindowHandle);
	*/
	setHWND(NULL);

	return true;
}

void
PropertySheet::setHWNDFromPage(HWND h)
{
	// If we're a modal dialog, there's no way for us to know our window handle unless
	// one of our pages tells us through this function.
	setHWND(h);
}

/*
  Adjust the size of a page so that it fits nicely into the window.
 */
void
PropertySheet::adjustPageSize(HWND page)
{
	PropertySheetData& psd = PropertySheetData::Instance();
	if (!psd.clientRectValid)
		return;

	/*
	  It's probably not obvious what's done here:
	  When this method is called the first time, the first page is already
	  created and sized, but at the coordinates (0,0). The sheet, however,
	  isn't in it's final size. My guess is that the sheet first creates the
	  page, and then resizes itself to have the right metrics to contain the 
	  page and moves it to it's position. For our purposes, however, we need
	  the final metrics of the page. So, the first time this method is called,
	  we basically grab the size of the page, but calculate the top/left coords
	  ourselves.
	 */
  
	if (!psd.gotPage)
	{
		psd.gotPage = true;

		RECTWrapper& pageRect = psd.pageRect;
		::GetWindowRect(page, &pageRect);
		// We want client coords.
		::ScreenToClient(page, (LPPOINT)&pageRect.left);
		::ScreenToClient(page, (LPPOINT)&pageRect.right);

		LONG dialogBaseUnits = ::GetDialogBaseUnits();
		// The margins in DUs are a result of "educated guesses" and T&E.
		int marginX = ::MulDiv(5, LOWORD(dialogBaseUnits), 4);
		int marginY = ::MulDiv(5, HIWORD(dialogBaseUnits), 8);

		pageRect.move(marginX, marginY);
	}

	::SetWindowPos(page, 0, psd.pageRect.left, psd.pageRect.top, 
		      psd.pageRect.width(), psd.pageRect.height(), 
		      SWP_NOACTIVATE | SWP_NOZORDER);
}

void
PropertySheet::addPage(PropertyPage * p)
{
	// Add a page to the property sheet.
	p->youAreBeingAddedToASheet(this);
	m_propertyPages.push_back(p);
}

bool
PropertySheet::setActivePage(int i)
{
	// Posts a message to the message queue, so this won't block
	return static_cast<bool>(::PropSheet_SetCurSel(getHWND(), NULL, i));
}

bool
PropertySheet::setActivePageByID(int resource_id)
{
	// Posts a message to the message queue, so this won't block
	return static_cast<bool>(::PropSheet_SetCurSelByID(getHWND(), resource_id));
}

void
PropertySheet::setButtons(DWORD flags)
{
	// Posts a message to the message queue, so this won't block
	::PropSheet_SetWizButtons(getHWND(), flags);
}

void
PropertySheet::pressButton(int button)
{
	::PropSheet_PressButton(getHWND(), button);
}

void
PropertySheet::setFinish()
{
	::PropSheet_SetWizButtons(getHWND(), PSWIZB_FINISH);
	::PropSheet_CancelToClose(getHWND());
}

