//-----------------------------------------------------------------------------
// File: cdevicecontrol.cpp
//
// Desc: CDeviceControl is a class that encapsulate the functionality of a
//       device control (or a callout).  CDeviceView accesses it to retrieve/
//       save information about the control.
//
// Copyright (C) 1999-2000 Microsoft Corporation. All Rights Reserved.
//-----------------------------------------------------------------------------

#include "common.hpp"


CDeviceControl::CDeviceControl(CDeviceUI &ui, CDeviceView &view) :
	m_ui(ui),
	m_view(view),
	m_bHighlight(FALSE),
	m_ptszCaption(NULL),
	m_dwDrawTextFlags(0),
	m_FontHeight(-1),
	m_bCalledCalcCallout(FALSE),
	m_bPlacedOnlyFirstCorner(FALSE),
	m_bInit(FALSE),
	m_dwCalloutAlign(CAF_TOPLEFT),
	m_nLinePoints(0),
	m_dwDeviceControlOffset((DWORD)-1),
	m_bOffsetAssigned(FALSE),
	m_pbmOverlay(NULL),
	m_pbmHitMask(NULL),
	m_ptszOverlayPath(NULL),
	m_bCaptionClipped(FALSE)
{
}

CDeviceControl::~CDeviceControl()
{
	DEVICEUINOTIFY uin;
	uin.from = DEVUINFROM_CONTROL;
	uin.control.pControl = (CDeviceControl *)this;
	uin.msg = DEVUINM_ONCONTROLDESTROY;
	m_ui.Notify(uin);
	if (m_ptszCaption)
		free(m_ptszCaption);
	delete m_pbmOverlay;
	delete m_ptszOverlayPath;
}

void CDeviceControl::SetCaption(LPCTSTR tszCaption, BOOL bFixed)
{
	LPTSTR tszNewCaption = NULL;

	m_bFixed = bFixed;

	if (tszCaption != NULL)
	{
		tszNewCaption = _tcsdup(tszCaption);

		if (tszNewCaption == NULL)
			return;
	}

	free(m_ptszCaption);
	m_ptszCaption = tszNewCaption;
	tszNewCaption = NULL;

	CalcCallout();
	Invalidate();
}

LPCTSTR CDeviceControl::GetCaption()
{
	return (LPCTSTR)m_ptszCaption;
}

BOOL CDeviceControl::HitControl(POINT point)
{
	return FALSE;
}

DEVCTRLHITRESULT CDeviceControl::HitTest(POINT test)
{
	if (!m_bInit)
		return DCHT_NOHIT;

	if (m_ui.InEditMode() &&
			PtInRect(&m_rectCalloutMax, test))
		return DCHT_MAXRECT;

	PrepCallout();

	if (PtInRect(&m_rectCallout, test))
		return DCHT_CAPTION;

	if (HitControl(test))
		return DCHT_CONTROL;

	return DCHT_NOHIT;
}

void CDeviceControl::Init()
{
	m_uin.from = DEVUINFROM_CONTROL;
	m_uin.control.pControl = this;

	CalcCallout();

	m_bInit = TRUE;
}

// We will have to know the view's scrolling offset to adjust the tooltip's position.
void CDeviceControl::OnMouseOver(POINT point)
{
	// Tooltip only if the callout text is clipped.
	if (m_bCaptionClipped)
	{
		TOOLTIPINITPARAM ttip;
		ttip.hWndParent = m_view.m_hWnd;
		ttip.iSBWidth = 0;
		ttip.dwID = m_dwDeviceControlOffset;
		ttip.hWndNotify = m_view.m_hWnd;
		ttip.tszCaption = GetCaption();
		CFlexToolTip::UpdateToolTipParam(ttip);
	} else
		CFlexWnd::s_ToolTip.SetToolTipParent(NULL);

	m_uin.msg = DEVUINM_MOUSEOVER;
	m_ui.Notify(m_uin);
}

void CDeviceControl::OnClick(POINT point, BOOL bLeft, BOOL bDoubleClick)
{

	m_uin.msg = bDoubleClick ? DEVUINM_DOUBLECLICK : DEVUINM_CLICK;
	m_uin.click.bLeftButton = bLeft;
	m_ui.Notify(m_uin);
}

void CDeviceControl::Unpopulate()
{
}

void CDeviceControl::Highlight(BOOL bHighlight)
{
	if (m_bHighlight == bHighlight)
		return;

	m_bHighlight = bHighlight;

	// If the view has scrolling enabled, we need to adjust the scroll
	// bar position to make this callout visible.
	if (bHighlight)
		m_view.ScrollToMakeControlVisible(m_rectCalloutMax);

	CalcCallout();

	// We do not invalidate rectangle if we are unhighlighting.  Let CDeviceView handle that.
	if (bHighlight) Invalidate();
}

void CDeviceControl::GetInfo(GUID &rGuid, DWORD &rdwOffset)
{
	m_ui.GetDeviceInstanceGuid(rGuid);
	rdwOffset = m_dwDeviceControlOffset;
}

BOOL CDeviceControl::PrepCaption()
{
	if (m_ptszCaption != NULL)
		return TRUE;
	m_ptszCaption = _tcsdup(g_tszUnassignedControlCaption);
	return m_ptszCaption != NULL;
}

void CDeviceControl::PrepLinePoints()
{
	if (m_nLinePoints > 0)
		return;
	m_nLinePoints = 1;
	POINT pt = {0, 0};
	if (m_dwCalloutAlign & CAF_LEFT)
		pt.x = m_rectCalloutMax.left;
	if (m_dwCalloutAlign & CAF_RIGHT)
		pt.x = m_rectCalloutMax.right - 1;
	if (m_dwCalloutAlign & CAF_TOP)
		pt.y = m_rectCalloutMax.top;
	if (m_dwCalloutAlign & CAF_BOTTOM)
		pt.y = m_rectCalloutMax.bottom - 1;
	if (!(m_dwCalloutAlign & (CAF_LEFT | CAF_RIGHT)))
		pt.x = (m_rectCalloutMax.left + m_rectCalloutMax.right - 1) / 2;
	if (!(m_dwCalloutAlign & (CAF_BOTTOM | CAF_TOP)))
		pt.y = (m_rectCalloutMax.top + m_rectCalloutMax.bottom - 1) / 2;
	m_rgptLinePoint[0] = pt;
}

void CDeviceControl::PrepCallout()
{
	if (m_bCalledCalcCallout)
		return;
	CalcCallout();
}

void CDeviceControl::PrepFont()
{
	if (m_FontHeight != -1)
		return;

	HDC hDC = CreateCompatibleDC(NULL);
	if (hDC != NULL)
	{
		RECT rect = {0, 0, 500, 1};
		{
			CPaintHelper ph(m_ui.m_uig, hDC);
			ph.SetFont(UIF_CALLOUT);
			m_FontHeight = DrawText(hDC, _T("Testify"), -1, &rect, m_dwDrawTextFlags);
		}
		DeleteDC(hDC);
	}
}

void CDeviceControl::CalcCallout()
{
	m_bCalledCalcCallout = TRUE;

	RECT max = m_rectCalloutMax;
	InflateRect(&max, -1, -1);
	RECT rect = max;
	rect.bottom = rect.top + 1;

	m_dwDrawTextFlags = DT_WORDBREAK | DT_CALCRECT | DT_NOPREFIX | DT_END_ELLIPSIS | DT_EDITCONTROL;

	PrepFont();

	HDC hDC = CreateCompatibleDC(NULL);

	{
		CPaintHelper ph(m_ui.m_uig, hDC);
		ph.SetFont(UIF_CALLOUT);

		// first, drawtext/calcrect into the temporary rect
		if (!PrepCaption())
		{
			return;
		}

		int th = DrawText(hDC, m_ptszCaption, -1, &rect, m_dwDrawTextFlags);

		m_bCaptionClipped = rect.bottom > max.bottom || rect.right > max.right;  // Set clipped flag.
		
		BOOL bSingleTextLine = th <= m_FontHeight;

		if (rect.right > max.right)
		{
			bSingleTextLine = TRUE;
			rect.right = max.right;
		}

		if (bSingleTextLine)
			m_dwDrawTextFlags &= ~DT_WORDBREAK;

		m_dwDrawTextFlags &= ~DT_CALCRECT;

		RECT rect2 = rect;
		if (rect2.bottom > max.bottom)
			rect2.bottom = max.bottom;
		th = DrawText(hDC, m_ptszCaption, -1, &rect2, m_dwDrawTextFlags);
		int ith = (th / m_FontHeight) * m_FontHeight;
		rect.bottom = rect.top + ith + 1;
	}

	DeleteDC(hDC);
	hDC = NULL;

	if (rect.bottom > max.bottom)
		rect.bottom = max.bottom;

	assert(rect.right <= max.right);
	assert(rect.bottom <= max.bottom);

	PrepLinePoints();
	POINT adj = {0, 0};

	assert(rect.left == max.left);
	assert(rect.top == max.top);

	int w = rect.right - rect.left;
	int h = rect.bottom - rect.top;
	int mw = max.right - max.left;
	int mh = max.bottom - max.top;
	int dw = mw - w, dh = mh - h;
	int cx = mw / 2 + max.left, cy = mh / 2 + max.top;
	int cl = cx - w / 2, ct = cy - h / 2;

	assert(dw >= 0);
	assert(dh >= 0);

	if (m_dwCalloutAlign & CAF_RIGHT && rect.right < max.right)
		adj.x = max.right - rect.right;
	if (m_dwCalloutAlign & CAF_BOTTOM && rect.bottom < max.bottom)
		adj.y = max.bottom - rect.bottom;
	if (!(m_dwCalloutAlign & (CAF_RIGHT | CAF_LEFT)) && w < mw && rect.left != cl)
		adj.x = cl - rect.left;
	if (!(m_dwCalloutAlign & (CAF_BOTTOM | CAF_TOP)) && h < mh && rect.top != ct)
		adj.y = ct - rect.top;

	OffsetRect(&rect, adj.x, adj.y);

	InflateRect(&rect, 1, 1);

	m_rectCallout = rect;
}

BOOL CDeviceControl::DrawOverlay(HDC hDC)
{
	if (m_pbmOverlay == NULL)
		return FALSE;

	return m_pbmOverlay->Blend(hDC, m_ptOverlay);
}

void CDeviceControl::OnPaint(HDC hDC)
{
	if (!m_bInit)
		return;

	// If we are in view mode and the callout is not assigned, don't draw anything.
	if (!m_ui.m_uig.InEditMode() && !lstrcmp(m_ptszCaption, g_tszUnassignedControlCaption))
		return;

	PrepCallout();

	CPaintHelper ph(m_ui.m_uig, hDC);
	UIELEMENT eCallout = m_bHighlight ? UIE_CALLOUTHIGH : UIE_CALLOUT;

	// draw lines...
	if (m_nLinePoints > 1)
	{
		ph.SetElement(UIE_CALLOUTSHADOW);
		PolyLineArrowShadow(hDC, m_rgptLinePoint, m_nLinePoints);
		ph.SetElement(eCallout);
		PolyLineArrow(hDC, m_rgptLinePoint, m_nLinePoints);
	}

	// draw text
	ph.SetElement(eCallout);
	RECT rect = m_rectCallout;
	InflateRect(&rect, -1, -1);

	// If this control is assigned an action with DIA_FIXED (m_bFixed), use gray color for text.
	COLORREF OldColor;
	if (m_bFixed)
	{
		OldColor = ::SetTextColor(hDC, 0);  // Set an arbitrary color to find out what we are currently using.
		::SetTextColor(hDC, RGB(GetRValue(OldColor) >> 1, GetGValue(OldColor) >> 1, GetBValue(OldColor) >> 1));
	}
	DrawText(hDC, m_ptszCaption, -1, &rect, m_dwDrawTextFlags);

	if (m_bFixed)
		::SetTextColor(hDC, OldColor);
}

void CDeviceControl::Invalidate()
{
	m_view.Invalidate();
}

void MakeRect(RECT &rect, POINT a, POINT b)
{
	rect.left = min(a.x, b.x);
	rect.right = max(a.x, b.x);
	rect.top = min(a.y, b.y);
	rect.bottom = max(a.y, b.y);
}

void CDeviceControl::PlaceCalloutMaxCorner(int nCorner, POINT point)
{
	switch (nCorner)
	{
		case 0:
			m_ptFirstCorner = point;
			m_bPlacedOnlyFirstCorner = TRUE;
			Invalidate();
			break;

		case 1:
			MakeRect(m_rectCalloutMax, m_ptFirstCorner, point);
			m_bPlacedOnlyFirstCorner = FALSE;
			if (!m_bInit)
				Init();
			else
				CalcCallout();
			Invalidate();
			break;

		default:
			assert(0);
			break;
	}
}

void CDeviceControl::SetLastLinePoint(int nPoint, POINT point, BOOL bShiftDown)
{
	if (!(nPoint >= 0 && nPoint < MAX_DEVICECONTROL_LINEPOINTS))
		return;

	// Check for SHIFT key state
	if (nPoint && bShiftDown)  // SHIFT key only makes a difference if we are setting 2nd and subsequent points.
	{
		// SHIFT key down.  Need to draw controlled line.
		if (labs(m_rgptLinePoint[nPoint-1].x - point.x) > labs(m_rgptLinePoint[nPoint-1].y - point.y))
		{
			// Wider. Draw horizontal.
			m_rgptLinePoint[nPoint].x = point.x;
			m_rgptLinePoint[nPoint].y = m_rgptLinePoint[nPoint-1].y;
		} else
		{
			// Taller. Draw vertical
			m_rgptLinePoint[nPoint].x = m_rgptLinePoint[nPoint-1].x;
			m_rgptLinePoint[nPoint].y = point.y;
		}
	} else
		m_rgptLinePoint[nPoint] = point; // SHIFT key not down.  Draw line as usual.
	m_nLinePoints = nPoint + 1;
	Invalidate();

	if (m_nLinePoints < 2)
		return;

	POINT prev = m_rgptLinePoint[m_nLinePoints - 2];

	// remove identical points
	if (point.x == prev.x && point.y == prev.y)
	{
		m_nLinePoints--;
		return;
	}
}

void PlaceRectCenter(RECT &rect, POINT point)
{
	POINT center = {
		(rect.left + rect.right) / 2,
		(rect.top + rect.bottom) / 2};

	OffsetRect(&rect, point.x - center.x, point.y - center.y);
}

void OffsetRectToWithin(RECT &rect, const RECT &bounds)
{
	POINT adj = {0, 0};

	if (rect.left < bounds.left)
		adj.x = bounds.left - rect.left;
	if (rect.right > bounds.right)
		adj.x = bounds.right - rect.right;
	if (rect.top < bounds.top)
		adj.y = bounds.top - rect.top;
	if (rect.bottom > bounds.bottom)
		adj.y = bounds.bottom - rect.bottom;

	OffsetRect(&rect, adj.x, adj.y);
}

void CDeviceControl::Position(POINT point)
{
	PlaceRectCenter(m_rectCalloutMax, point);
	RECT client;
	m_view.GetClientRect(&client);
	OffsetRectToWithin(m_rectCalloutMax, client);
	CalcCallout();
	Invalidate();
}

void CDeviceControl::ConsiderAlignment(POINT point)
{
	POINT center = {
		(m_rectCalloutMax.right + m_rectCalloutMax.left) / 2,
		(m_rectCalloutMax.bottom + m_rectCalloutMax.top) / 2};
	SIZE dim = {
		m_rectCalloutMax.right - m_rectCalloutMax.left,
		m_rectCalloutMax.bottom - m_rectCalloutMax.top};
	SIZE delta = {point.x - center.x, point.y - center.y};
	int MININ = m_FontHeight;
	SIZE in = {max(dim.cx / 4, MININ), max(dim.cy / 4, MININ)};
	DWORD align = 0;
	if (delta.cx < -in.cx)
		align |= CAF_LEFT;
	if (delta.cx > in.cx)
		align |= CAF_RIGHT;
	if (delta.cy < -in.cy)
		align |= CAF_TOP;
	if (delta.cy > in.cy)
		align |= CAF_BOTTOM;
	m_dwCalloutAlign = align;
	CalcCallout();
	Invalidate();
}

void CDeviceControl::ReselectControl()
{
	SelectControl(TRUE);
}

void CDeviceControl::SelectControl(BOOL bReselect)
{
	CSelControlDlg dlg(m_view, *this, bReselect, m_dwDeviceControlOffset, m_ui.m_didi);

	switch (dlg.DoModal(m_view.m_hWnd))
	{
		case SCDR_OK:
			m_dwDeviceControlOffset = dlg.GetOffset();
			m_bOffsetAssigned = TRUE;
			Invalidate();
			break;

		case SCDR_CANCEL:
			break;

		case SCDR_NOFREE:
			MessageBox(m_view.m_hWnd, _T("All device controls have been assigned for this view."),
			           _T("Can't reselect control."), MB_OK);
			break;

		case -1:
			MessageBox(m_view.m_hWnd, _T("CSelControlDlg.DoModal() failed."), _T("oops"), MB_OK);
			break;

		default:
			assert(0);
			break;
	}
}

DWORD CDeviceControl::GetOffset()
{
	if (m_bOffsetAssigned)
		return m_dwDeviceControlOffset;

	return (DWORD)-1;
}

BOOL CDeviceControl::IsOffsetAssigned()
{
	return m_bOffsetAssigned;
}

void CDeviceControl::FillImageInfo(DIDEVICEIMAGEINFOW *pImgInfo)
{
	if (!pImgInfo) return;

	if (m_ptszOverlayPath != NULL)
		CopyStr(pImgInfo->tszImagePath, m_ptszOverlayPath, MAX_PATH);
	else
		wcscpy(pImgInfo->tszImagePath, L"");  // Overlay Image not yet supported

	SIZE size = {0, 0};
	if (m_pbmOverlay != NULL)
		m_pbmOverlay->GetSize(&size);
	RECT rect = {m_ptOverlay.x, m_ptOverlay.y,
		m_ptOverlay.x + size.cx, m_ptOverlay.y + size.cy};

	pImgInfo->dwFlags = DIDIFT_OVERLAY;  // This is an overlay
	pImgInfo->rcOverlay = rect;
	pImgInfo->dwObjID = GetOffset();
	pImgInfo->dwcValidPts = m_nLinePoints;
	DWORD dwPtsToCopy = m_nLinePoints > 5 ? 5 : m_nLinePoints;
	for (DWORD i = 0; i < dwPtsToCopy; ++i)
		pImgInfo->rgptCalloutLine[i] = m_rgptLinePoint[i];
	pImgInfo->rcCalloutRect = m_rectCalloutMax;
	pImgInfo->dwTextAlign = m_dwCalloutAlign;
}


BOOL CDeviceControl::IsMapped()
{
	return m_ui.IsControlMapped(this);
}

int CDeviceControl::GetControlIndex()
{
	for (int i = 0; i < m_view.GetNumControls(); i++)
		if (m_view.GetControl(i) == this)
			return i;

	return -1;
}

void CDeviceControl::SetLinePoints(int n, POINT *rgpt)
{
	assert(n >= 0 && n <= MAX_DEVICECONTROL_LINEPOINTS && rgpt);

	if (n < 0)
		n = 0;
	if (n > MAX_DEVICECONTROL_LINEPOINTS)
		n = MAX_DEVICECONTROL_LINEPOINTS;

	if (!rgpt)
		n = 0;

	m_nLinePoints = n;

	for (int i = 0; i < n; i++)
		m_rgptLinePoint[i] = rgpt[i];
}

void CDeviceControl::SetOverlayPath(LPCTSTR tszPath)
{
	if (m_ptszOverlayPath)
		free(m_ptszOverlayPath);
	m_ptszOverlayPath = NULL;

	if (tszPath)
		m_ptszOverlayPath = _tcsdup(tszPath);

	delete m_pbmOverlay;
	m_pbmOverlay = NULL;

	if (m_ptszOverlayPath)
		m_pbmOverlay = CBitmap::CreateViaD3DX(m_ptszOverlayPath, m_ui.m_uig.GetSurface3D());
}

void CDeviceControl::SetOverlayRect(const RECT &r)
{
	m_ptOverlay.x = r.left;
	m_ptOverlay.y = r.top;
}
