#include "stdafx.h"
#include "CListControl.h"
#include "CListControlHeaderImpl.h" // redundant but makes intelisense quit showing false errors
#include "CListControl-Cells.h"
#include "PaintUtils.h"
#include "GDIUtils.h"
#include "win32_utility.h"

enum {
	lineBelowHeaderCY = 1
};

static bool testDrawLineBelowHeader() {
	// Win10
	return GetOSVersionCode() >= 0xA00;
}

void CListControlHeaderImpl::InitializeHeaderCtrl(DWORD flags) {
	PFC_ASSERT(!IsHeaderEnabled());
	WIN32_OP( m_header.Create(*this,NULL,NULL,WS_CHILD | flags) != NULL);
	m_header.SetFont( GetFont() );

	if (testDrawLineBelowHeader()) {
		m_headerLine.Create( *this, NULL, NULL, WS_CHILD );
	}

	UpdateHeaderLayout();
}

void CListControlHeaderImpl::UpdateHeaderLayout() {
	CRect client; WIN32_OP_D( GetClientRect(client) );
	m_clientWidth = client.Width();
	if (IsHeaderEnabled()) {
		auto rc = client; 
		rc.left -= GetViewOffset().x;
		WINDOWPOS wPos = {};
		HDLAYOUT layout = {&rc, &wPos};
		if (m_header.Layout(&layout)) {
			m_header.SetWindowPos(wPos.hwndInsertAfter,wPos.x,wPos.y,wPos.cx,wPos.cy,wPos.flags | SWP_SHOWWINDOW);
			if (m_headerLine != NULL) m_headerLine.SetWindowPos(m_header, wPos.x, wPos.y + wPos.cy, wPos.cx, lineBelowHeaderCY, wPos.flags | SWP_SHOWWINDOW);
		} else {
			m_header.ShowWindow(SW_HIDE);
			if (m_headerLine != NULL) m_headerLine.ShowWindow(SW_HIDE);
		}
	}
}

int CListControlHeaderImpl::GetItemWidth() const {
	if (IsHeaderEnabled()) return m_itemWidth;
	else return m_clientWidth;
}

LRESULT CListControlHeaderImpl::OnSizePassThru(UINT,WPARAM,LPARAM p_lp) {
	UpdateHeaderLayout();

	ProcessAutoWidth();
	
	SetMsgHandled(FALSE);
	return 0;
}

void CListControlHeaderImpl::OnViewOriginChange(CPoint p_delta) {
	TParent::OnViewOriginChange(p_delta);
	if (p_delta.x != 0) UpdateHeaderLayout();
}

void CListControlHeaderImpl::SetHeaderFont(HFONT font) {
	if (IsHeaderEnabled()) {
		m_header.SetFont(font); UpdateHeaderLayout();
	}
}

LRESULT CListControlHeaderImpl::OnDividerDoubleClick(int,LPNMHDR hdr,BOOL&) {
	const NMHEADER * info = (const NMHEADER *) hdr;
	if (info->iButton == 0) {
		AutoColumnWidth((t_size)info->iItem);
	}
	return 0;
}

LRESULT CListControlHeaderImpl::OnHeaderItemClick(int,LPNMHDR p_hdr,BOOL&) {
	const NMHEADER * info = (const NMHEADER *) p_hdr;
	if (info->iButton == 0) {
		OnColumnHeaderClick((t_uint32)info->iItem);
	}
	return 0;
}

LRESULT CListControlHeaderImpl::OnHeaderItemChanged(int,LPNMHDR p_hdr,BOOL&) {
	const NMHEADER * info = (const NMHEADER*) p_hdr;
	if (info->pitem->mask & (HDI_WIDTH | HDI_ORDER)) {
		if(!m_ownColumnsChange) ProcessColumnsChange();
	}
	return 0;
}

LRESULT CListControlHeaderImpl::OnHeaderEndDrag(int,LPNMHDR hdr,BOOL&) {
	NMHEADER * info = (NMHEADER*) hdr;
	return OnColumnHeaderDrag(info->iItem,info->pitem->iOrder) ? TRUE : FALSE;
}

bool CListControlHeaderImpl::OnColumnHeaderDrag(t_size index, t_size newPos) {
	index = GetSubItemOrder(index);
	const t_size count = this->GetColumnCount();
	if ( count == 0 ) return false;
	std::vector<size_t> perm; perm.resize(count); pfc::create_move_items_permutation(&perm[0],count, pfc::bit_array_one(index), (int) newPos - (int) index );
	std::vector<int> order, newOrder; order.resize(count); newOrder.resize(count);
	WIN32_OP_D(m_header.GetOrderArray((int)count, &order[0]));
	for(t_size walk = 0; walk < count; ++walk) newOrder[walk] = order[perm[walk]];
	WIN32_OP_D(m_header.SetOrderArray((int)count, &newOrder[0]));
	OnColumnsChanged();
	return true;
}
t_size CListControlHeaderImpl::SubItemFromPointAbs(CPoint pt) const {
	
	auto order = GetColumnOrderArray();
	const t_size colCount = order.size();

	size_t item;
	if (! ItemFromPointAbs(pt, item ) ) item = pfc_infinite;

	long xWalk = 0;
	for(t_size _walk = 0; _walk < colCount; ) {
		const t_size walk = (t_size) order[_walk];

		size_t span = 1;
		if (item != pfc_infinite) span = this->GetSubItemSpan(item, walk);

		PFC_ASSERT( span == 1 || _walk == walk );

		if ( walk + span > colCount ) span = colCount - walk;

		long width = 0;
		for( size_t sub = 0; sub < span; ++ sub ) {
			width += (long)this->GetSubItemWidth(walk + sub);
		}

		if (xWalk + width > pt.x) return walk;
		xWalk += width;
		_walk += span;
	}
	return pfc_infinite;
}

bool CListControlHeaderImpl::OnClickedSpecial(DWORD status, CPoint pt) {
	
	const DWORD maskButtons = MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2;

	if ( (status & maskButtons) != MK_LBUTTON ) return false;

	if (!GetCellTypeSupported()) return false;

	size_t item; size_t subItem;
	if (! this->GetItemAtPointAbsEx( pt + GetViewOffset(), item, subItem ) ) {
		return false;
	}

	bool bCapture = false;
	auto cellType = GetCellType(item, subItem);
	if ( !CellTypeReactsToMouseOver( cellType ) ) return false;
	auto rcHot = CellHotRect( item, subItem, cellType );
	if (!rcHot.PtInRect( pt )) return false;

	SetPressedItem(item, subItem);
	SetCaptureEx([=](UINT, DWORD newStatus, CPoint pt) {

		{
			CRect rc = this->GetClientRectHook();
			if (!rc.PtInRect(pt)) {
				ClearPressedItem(); return false;
			}
		}

		size_t newItem, newSubItem;
		if (!this->GetItemAtPointAbsEx(pt + GetViewOffset(), newItem, newSubItem) || newItem != item || newSubItem != subItem) {
			ClearPressedItem(); return false;
		}

		DWORD buttons = newStatus & maskButtons;
		if (buttons == 0) {
			// button released?
			this->defer( [=] {
				OnSubItemClicked(item, subItem, pt);
			} );
			ClearPressedItem(); return false;
		}
		if (buttons != MK_LBUTTON) {
			// another button pressed?
			ClearPressedItem(); return false;
		}

		return true;
	});
	return true;
}

bool CListControlHeaderImpl::CellTypeUsesSpecialHitTests( cellType_t ct ) {
	if ( ct == nullptr ) return false;
	return ct->SuppressRowSelect();
}

bool CListControlHeaderImpl::OnClickedSpecialHitTest(CPoint pt) {
	if ( ! GetCellTypeSupported() ) return false;
	auto ct = GetCellTypeAtPointAbs( pt + GetViewOffset() );
	return CellTypeUsesSpecialHitTests(ct);
}

void CListControlHeaderImpl::OnItemClicked(t_size item, CPoint pt) {
	t_size subItem = SubItemFromPointAbs(pt + GetViewOffset());
	if (subItem != ~0) {
		if ( this->GetCellTypeSupported() ) {
			auto ct = this->GetCellType(item, subItem );
			// we don't handle hyperlink & button clicks thru here
			if (CellTypeUsesSpecialHitTests(ct)) return;
		}
		OnSubItemClicked(item, subItem, pt);
	}
}

std::vector<int> CListControlHeaderImpl::GetColumnOrderArray() const {
	const size_t cCount = this->GetColumnCount();
	std::vector<int> order; 
	if ( cCount > 0 ) {
		order.resize(cCount);
		if (IsHeaderEnabled()) {
			WIN32_OP_D(m_header.GetOrderArray((int)cCount, &order[0]));
		} else {
			for (size_t c = 0; c < cCount; ++c) order[c] = (int)c;
		}
	}
	return order;
}

void CListControlHeaderImpl::RenderItemText(t_size item,const CRect & itemRect,const CRect & updateRect,CDCHandle dc, bool allowColors) {
	
	t_uint32 xWalk = itemRect.left;
	CRect subItemRect(itemRect);
	auto order = GetColumnOrderArray();
	const size_t cCount = order.size();
	SelectObjectScope fontScope(dc,GetFont());
	for(t_size _walk = 0; _walk < cCount; ) {
		const t_size walk = order[_walk];
		
		size_t span = GetSubItemSpan(item, walk);

		PFC_ASSERT( walk == _walk || span == 1 );

		t_uint32 width = GetSubItemWidth(walk);

		if ( span > 1 ) {
			if ( walk + span > cCount ) span = cCount - walk;
			for( size_t extraWalk = 1; extraWalk < span; ++ extraWalk ) {
				width += GetSubItemWidth(walk + extraWalk);
			}
		}

		subItemRect.left = xWalk; subItemRect.right = xWalk + width;
		CRect subUpdate;
		if (subUpdate.IntersectRect(subItemRect, updateRect)) {
			DCStateScope scope(dc);
			if (dc.IntersectClipRect(subItemRect) != NULLREGION) {
				RenderSubItemText(item,walk,subItemRect,subUpdate,dc, allowColors);
			}
		}
		xWalk += width;

		_walk += span;
	}
}

t_size CListControlHeaderImpl::GetSubItemOrder(t_size subItem) const {
	if ( ! IsHeaderEnabled( ) ) return subItem;
	HDITEM hditem = {};
	hditem.mask = HDI_ORDER;
	WIN32_OP_D( m_header.GetItem( (int) subItem, &hditem ) );
	return (t_size) hditem.iOrder;
}

size_t CListControlHeaderImpl::GetSubItemSpan(size_t row, size_t column) const {
	return 1;
}

uint32_t CListControlHeaderImpl::GetSubItemWidth(t_size subItem) const {
	if ( ! IsHeaderEnabled( ) ) {
		// Should be overridden for custom columns layout
		PFC_ASSERT( GetColumnCount() == 1 );
		PFC_ASSERT( subItem == 0 );
		return GetItemWidth();
	}

	if ( subItem < m_colRuntime.size() ) return m_colRuntime[subItem].m_widthPixels;
	PFC_ASSERT( !"bad column idx");
	return 0;
}

int CListControlHeaderImpl::GetHeaderItemWidth( int which ) {
	HDITEM hditem = {};
	hditem.mask = HDI_WIDTH;
	WIN32_OP_D( m_header.GetItem( which, &hditem) );
	return hditem.cxy;
}

void CListControlHeaderImpl::OnColumnsChanged() {
	if ( IsHeaderEnabled() ) {
		for( size_t walk = 0; walk < m_colRuntime.size(); ++ walk ) {
			m_colRuntime[walk].m_widthPixels = GetHeaderItemWidth( (int) walk );
		}
		RecalcItemWidth();
	}
	this->OnViewAreaChanged();
}

void CListControlHeaderImpl::ResetColumns(bool update) {
	m_colRuntime.clear();
	m_itemWidth = 0;
	PFC_ASSERT(IsHeaderEnabled());
	for(;;) {
		int count = m_header.GetItemCount();
		if (count <= 0) break;
		m_header.DeleteItem(count - 1);
	}
	if (update) OnColumnsChanged();
}

void CListControlHeaderImpl::SetColumn( size_t which, const char * label, DWORD fmtFlags, bool updateView) {
	PFC_ASSERT( IsHeaderEnabled() );
	pfc::stringcvt::string_os_from_utf8 labelOS(label);

	HDITEM item = {};
	item.mask = HDI_TEXT | HDI_FORMAT;
	item.fmt = fmtFlags | HDF_STRING;
	item.pszText = const_cast<TCHAR*>(labelOS.get_ptr());
	m_header.SetItem( (int) which, &item );

	if (which < m_colRuntime.size()) m_colRuntime[which].m_text = label;

	if (updateView) OnColumnsChanged();
}

void CListControlHeaderImpl::GetColumnText(size_t which, pfc::string_base & out) const {
	if (which < m_colRuntime.size()) {
		out = m_colRuntime[which].m_text.c_str();
	} else {
		out = "";
	}
}

void CListControlHeaderImpl::ResizeColumn(t_size index, t_uint32 widthPixels, bool updateView) {
	PFC_ASSERT( IsHeaderEnabled() );
	PFC_ASSERT( index < m_colRuntime.size() );
	HDITEM item = {};
	item.mask = HDI_WIDTH;
	item.cxy = widthPixels;
	{ pfc::vartoggle_t<bool> scope(m_ownColumnsChange, true); m_header.SetItem( (int) index, &item ); }
	m_colRuntime[index].m_widthPixels = widthPixels;
	RecalcItemWidth();
	if (updateView) OnColumnsChanged();
}

void CListControlHeaderImpl::DeleteColumns( pfc::bit_array const & mask, bool updateView ) {
	int nDeleted = 0;
	const size_t oldCount = GetColumnCount();
	mask.for_each(true, 0, oldCount, [&] (size_t idx) {
		int iDelete = (int) idx - nDeleted;
		bool bDeleted = m_header.DeleteItem( iDelete );
		PFC_ASSERT( bDeleted );
		if ( bDeleted ) ++ nDeleted;
		} );

	pfc::remove_mask_t( m_colRuntime, mask );

	ColumnWidthFix();

	if (updateView) {
		OnColumnsChanged();
	}

}

bool CListControlHeaderImpl::DeleteColumn(size_t index, bool update) {
	PFC_ASSERT( IsHeaderEnabled() );

	if (!m_header.DeleteItem( (int) index )) return false;

	pfc::remove_mask_t( m_colRuntime, pfc::bit_array_one( index ) );

	ColumnWidthFix();

	if (update) {
		OnColumnsChanged();
	}

	return true;
}

void CListControlHeaderImpl::SetSortIndicator( size_t whichColumn, bool isUp ) {
	HeaderControl_SetSortIndicator( GetHeaderCtrl(), (int) whichColumn, isUp );
}

void CListControlHeaderImpl::ClearSortIndicator() {
	HeaderControl_SetSortIndicator(GetHeaderCtrl(), -1, false);
}

bool CListControlHeaderImpl::HaveAutoWidthContentColumns() const {
	for( auto i = m_colRuntime.begin(); i != m_colRuntime.end(); ++ i ) {
		if ( i->autoWidthContent() ) return true;
	}
	return false;
}
bool CListControlHeaderImpl::HaveAutoWidthColumns() const {
	for( auto i = m_colRuntime.begin(); i != m_colRuntime.end(); ++ i ) {
		if ( i->autoWidth() ) return true;
	}
	return false;
}
void CListControlHeaderImpl::AddColumnEx( const char * label, uint32_t widthPixelsAt96DPI, DWORD fmtFlags, bool update ) {
	uint32_t w = widthPixelsAt96DPI;
	if ( w <= columnWidthMax ) {
		w = MulDiv( w, m_dpi.cx, 96 );
	}
	AddColumn( label, w, fmtFlags, update );
}

void CListControlHeaderImpl::AddColumnDLU( const char * label, uint32_t widthDLU, DWORD fmtFlags, bool update ) {
	uint32_t w = widthDLU;
	if ( w <= columnWidthMax ) {
		w = ::MapDialogWidth( GetParent(), w );
	}
	AddColumn( label, w, fmtFlags, update );
}
void CListControlHeaderImpl::AddColumnF( const char * label, float widthF, DWORD fmtFlags, bool update) {
	uint32_t w = columnWidthMax;
	if ( widthF >= 0 ) {
		w = pfc::rint32( widthF * m_dpi.cx / 96.0f );
	}
	AddColumn( label, w, fmtFlags, update );
}
void CListControlHeaderImpl::AddColumn(const char * label, uint32_t width, DWORD fmtFlags,bool update) {
	if (! IsHeaderEnabled( ) ) InitializeHeaderCtrl();

	pfc::stringcvt::string_os_from_utf8 labelOS(label);
	HDITEM item = {};
	item.mask = HDI_TEXT | HDI_FORMAT;
	if ( width != UINT32_MAX ) {
		item.cxy = width;
		item.mask |= HDI_WIDTH;
	}
	
	item.pszText = const_cast<TCHAR*>(labelOS.get_ptr());
	item.fmt = HDF_STRING | fmtFlags;
	int iColumn;
	WIN32_OP_D( (iColumn = m_header.InsertItem(m_header.GetItemCount(),&item) ) >= 0 );
	colRuntime_t rt;
	rt.m_text = label;
	rt.m_userWidth = width;
	if ( width <= columnWidthMax ) {
		m_itemWidth += width;
		rt.m_widthPixels = width;
	}
	m_colRuntime.push_back( std::move(rt) );
	
	if (update) OnColumnsChanged();

	ProcessAutoWidth();
}

float CListControlHeaderImpl::GetColumnWidthF(size_t subItem) const {
	auto w = GetSubItemWidth(subItem);
	return (float) w * 96.0f / (float)m_dpi.cx;
}

void CListControlHeaderImpl::RenderBackground(CDCHandle dc, CRect const& rc) {
	__super::RenderBackground(dc,rc);
#if 0
	if ( m_drawLineBelowHeader && IsHeaderEnabled()) {
		CRect rcHeader;
		if (m_header.GetWindowRect(rcHeader)) {
			// Draw a grid line below header
			int y = rcHeader.Height();
			if ( y >= rc.top && y < rc.bottom ) {
				CDCPen pen(dc, GridColor());
				SelectObjectScope scope(dc, pen);
				dc.MoveTo(rc.left, y);
				dc.LineTo(rc.right, y);
			}
		}
	}
#endif
}

CRect CListControlHeaderImpl::GetClientRectHook() const {
	CRect rcClient = __super::GetClientRectHook();
	if (m_header != NULL) {
		PFC_ASSERT( m_header.IsWindow() );
		CRect rcHeader;
		if (m_header.GetWindowRect(rcHeader)) {
			int h = rcHeader.Height();
			if ( m_headerLine != NULL ) h += lineBelowHeaderCY;
			rcClient.top = pfc::min_t(rcClient.bottom,rcClient.top + h);
		}
	}
	return rcClient;
}

CRect CListControlHeaderImpl::GetItemTextRectHook(size_t, size_t, CRect const & rc) const {
	return GetItemTextRect( rc );
}

CRect CListControlHeaderImpl::GetItemTextRect(const CRect & itemRect) const {
	CRect rc ( itemRect );
	rc.DeflateRect(GetColumnSpacing(),0);
	return rc;
}

void CListControlHeaderImpl::SetColumnSort(t_size which, bool isUp) {
	HeaderControl_SetSortIndicator(m_header,(int)which,isUp);
}

void CListControlHeaderImpl::SetColumnFormat(t_size which, DWORD format) {
	HDITEM item = {};
	item.mask = HDI_FORMAT;
	item.fmt = HDF_STRING | format;
	WIN32_OP_D( m_header.SetItem((int)which,&item) );
}

DWORD CListControlHeaderImpl::GetColumnFormat(t_size which) const {
	if (!IsHeaderEnabled()) return HDF_LEFT;
	HDITEM hditem = {};
	hditem.mask = HDI_FORMAT;
	WIN32_OP_D( m_header.GetItem( (int) which, &hditem) );
	return hditem.fmt;
}

BOOL CListControlHeaderImpl::OnSetCursor(CWindow wnd, UINT nHitTest, UINT message) {
#if 0
	// no longer meaningful since SetCapture on mouse over was added to track hot status
	if ( message != 0 && GetCellTypeSupported() ) {
		CPoint pt( (LPARAM) GetMessagePos() );
		WIN32_OP_D( ScreenToClient( &pt ) );
		if ( GetCellTypeAtPointAbs( pt + GetViewOffset() ) == cell_hyperlink ) {
			SetCursor(LoadCursor(NULL, IDC_HAND)); return TRUE;
		}
	}
#endif
	SetMsgHandled(FALSE); return FALSE;
}

bool CListControlHeaderImpl::GetItemAtPointAbsEx(CPoint pt, size_t & outItem, size_t & outSubItem) const {
	size_t item, subItem;
	if (ItemFromPointAbs(pt, item)) {
		subItem = SubItemFromPointAbs(pt);
		if (subItem != pfc_infinite) {
			outItem = item; outSubItem = subItem; return true;
		}
	}
	return false;
}

CListControlHeaderImpl::cellType_t CListControlHeaderImpl::GetCellTypeAtPointAbs(CPoint pt) const {
	size_t item, subItem;
	if ( GetItemAtPointAbsEx( pt, item, subItem) ) {
		return GetCellType( item, subItem );
	}
	return nullptr;
}

void CListControlHeaderImpl::RenderSubItemTextInternal(t_size subItem, const CRect & subItemRect, CDCHandle dc, const char * text, bool allowColors) {
	pfc::stringcvt::string_os_from_utf8 cvt(text);
	CRect clip = GetItemTextRect(subItemRect);
	const t_uint32 format = PaintUtils::DrawText_TranslateHeaderAlignment(GetColumnFormat(subItem));
	if (true) {
		const t_uint32 bk = dc.GetBkColor();
		const t_uint32 fg = dc.GetTextColor();
		const t_uint32 hl = (allowColors ? GetSysColorHook(colorHighlight) : fg);
		const t_uint32 colors[3] = {PaintUtils::BlendColor(bk, fg, 33), fg, hl};

		PaintUtils::TextOutColorsEx(dc, cvt, clip, format, colors);
		
		dc.SetTextColor(fg);
	} else {
		dc.DrawText(cvt,(int)cvt.length(),clip,DT_NOPREFIX | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER | format );
	}
}
CListCell * CListControlHeaderImpl::GetCellType( size_t item, size_t subItem ) const {
	return &PFC_SINGLETON(CListCell_Text);
}
void CListControlHeaderImpl::RenderSubItemText(t_size item, t_size subItem,const CRect & subItemRect,const CRect & updateRect,CDCHandle dc, bool allowColors) {
	const auto cellType = GetCellType( item, subItem );
	if ( cellType == nullptr ) {
		PFC_ASSERT( !"Should not get here" );
		return;
	}
	pfc::string_formatter label;
	const bool bHaveText = GetSubItemText(item,subItem,label);
	if (! bHaveText ) {
		label = ""; //sanity
	}

	pfc::stringcvt::string_os_from_utf8_fast labelOS ( label );
	CListCell::DrawContentArg_t arg;
	arg.hdrFormat = GetColumnFormat( subItem );
	arg.subItemRect = subItemRect;
	arg.dc = dc;
	arg.text = labelOS.get_ptr();
	arg.allowColors = allowColors;
	bool bPressed;
	if ( cellType->IsToggle() ) bPressed = this->GetCellCheckState(item, subItem);
	else bPressed = (item == m_pressedItem) && (subItem == m_pressedSubItem);
	bool bHot = (item == m_hotItem) && ( subItem == m_hotSubItem );
	if ( bPressed ) arg.cellState |= CListCell::cellState_pressed;
	if ( bHot ) arg.cellState|= CListCell::cellState_hot;
	arg.rcText = GetItemTextRectHook(item, subItem, subItemRect);
	arg.rcHot = CellHotRect(item, subItem, cellType, subItemRect);

	auto strTheme = cellType->Theme();
	if ( strTheme != nullptr ) {
		arg.theme = themeFor( strTheme ).m_theme;
	}
	arg.colorHighlight = GetSysColorHook(colorHighlight);

	arg.thisWnd = m_hWnd;

	CFontHandle fontRestore;
	CFont fontOverride;

	LOGFONT data;
	if (dc.GetCurrentFont().GetLogFont(&data)) {
		if ( cellType->ApplyTextStyle( data, CellTextScale(item, subItem ), arg.cellState ) ) {
			if (fontOverride.CreateFontIndirect( & data )) {
				fontRestore = dc.SelectFont( fontOverride );
			}
		}
	}
	cellType->DrawContent( arg );

	if ( fontRestore != NULL ) dc.SelectFont( fontRestore );
}

void CListControlHeaderImpl::RenderGroupHeaderText(int id,const CRect & headerRect,const CRect & updateRect,CDCHandle dc) {
	pfc::string_formatter label;
	if (GetGroupHeaderText(id,label)) {
		SelectObjectScope fontScope(dc,GetGroupHeaderFont());
		pfc::stringcvt::string_os_from_utf8 cvt(label);
		CRect contentRect(GetItemTextRect(headerRect));
		dc.DrawText(cvt,(int)cvt.length(),contentRect,DT_NOPREFIX | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER | DT_LEFT );
		SIZE txSize;
		const int lineSpacing = contentRect.Height() / 2;
		if (dc.GetTextExtent(cvt,(int)cvt.length(),&txSize)) {
			if (txSize.cx + lineSpacing < contentRect.Width()) {
				const CPoint center = contentRect.CenterPoint();
				const CPoint pt1(contentRect.left + txSize.cx + lineSpacing, center.y), pt2(contentRect.right, center.y);
				const COLORREF lineColor = PaintUtils::BlendColor(dc.GetTextColor(),dc.GetBkColor(),25);

#ifndef CListControl_ScrollWindowFix
#error FIXME CMemoryDC needed
#endif
				PaintUtils::DrawSmoothedLine(dc, pt1, pt2, lineColor, 1.0 * (double)m_dpi.cy / 96.0);
			}
		}
	}
}

uint32_t CListControlHeaderImpl::GetOptimalColumnWidthFixed(const char * fixedText) const {
	CWindowDC dc(*this);
	SelectObjectScope fontScope(dc, GetFont());
	GetOptimalWidth_Cache cache;
	cache.m_dc = dc;
	cache.m_stringTemp = fixedText;
	return cache.GetStringTempWidth() + this->GetColumnSpacing() * 2;
}

t_uint32 CListControlHeaderImpl::GetOptimalSubItemWidth(t_size item, t_size subItem, GetOptimalWidth_Cache & cache) const {
	const t_uint32 base = this->GetColumnSpacing() * 2;
	if (GetSubItemText(item,subItem,cache.m_stringTemp)) {
		return base + cache.GetStringTempWidth();
	} else {
		return base;
	}
}

t_uint32 CListControlHeaderImpl::GetOptimalWidth_Cache::GetStringTempWidth() {
	if (m_stringTemp.replace_string_ex(m_stringTempUnfuckAmpersands, "&", "&&") > 0) {
		m_convertTemp.convert(m_stringTempUnfuckAmpersands);
	} else {
		m_convertTemp.convert(m_stringTemp);
	}
	return PaintUtils::TextOutColors_CalcWidth(m_dc, m_convertTemp);
}

t_uint32 CListControlHeaderImpl::GetOptimalColumnWidth(t_size which, GetOptimalWidth_Cache & cache) const {
	const t_size totalItems = GetItemCount();
	t_uint32 val = 0;
	for(t_size item = 0; item < totalItems; ++item) {
		pfc::max_acc( val, GetOptimalSubItemWidth( item, which, cache ) );
	}
	return val;
}

t_uint32 CListControlHeaderImpl::GetOptimalSubItemWidthSimple(t_size item, t_size subItem) const {
	CWindowDC dc(*this);
	SelectObjectScope fontScope(dc, GetFont() );
	GetOptimalWidth_Cache cache;
	cache.m_dc = dc;
	return GetOptimalSubItemWidth(item, subItem, cache);
}

LRESULT CListControlHeaderImpl::OnKeyDown(UINT,WPARAM wp,LPARAM,BOOL& bHandled) {
	switch(wp) {
	case VK_ADD:
		if (IsKeyPressed(VK_CONTROL)) {
			AutoColumnWidths();
			return 0;
		}
		break;
	}
	bHandled = FALSE;
	return 0;
}

uint32_t CListControlHeaderImpl::GetOptimalColumnWidth(size_t colIndex) const {
	CWindowDC dc(*this);
	SelectObjectScope fontScope(dc, GetFont());
	GetOptimalWidth_Cache cache;
	cache.m_dc = dc;
	uint32_t ret = 0;
	const auto itemCount = GetItemCount();
	for (t_size walk = 0; walk < itemCount; ++walk) {
		pfc::max_acc(ret, GetOptimalSubItemWidth(walk, colIndex, cache));
	}
	return ret;
}

void CListControlHeaderImpl::AutoColumnWidths(const pfc::bit_array & mask, bool expandLast) {
	PFC_ASSERT( IsHeaderEnabled() );
	if (!IsHeaderEnabled()) return;
	const t_size itemCount = GetItemCount();
	if (itemCount == 0) return;
	const t_size columnCount = (t_size) m_header.GetItemCount();
	if (columnCount == 0) return;
	pfc::array_t<t_uint32> widths; widths.set_size(columnCount); widths.fill_null();
	{
		CWindowDC dc(*this);
		SelectObjectScope fontScope(dc,GetFont());
		GetOptimalWidth_Cache cache;
		cache.m_dc = dc;
		for(t_size walk = 0; walk < itemCount; ++walk) {
			for(t_size colWalk = mask.find_first(true,0,columnCount); colWalk < columnCount; colWalk = mask.find_next(true,colWalk,columnCount)) {
				pfc::max_acc(widths[colWalk], GetOptimalSubItemWidth(walk,colWalk,cache));
			}
		}
	}

	if (expandLast) {
		uint32_t usedWidth = 0; size_t lastCol = SIZE_MAX;
		pfc::array_t<int> order; order.set_size(columnCount);
		WIN32_OP_D( m_header.GetOrderArray((int)columnCount,order.get_ptr()) );
		for(size_t _walk = 0; _walk < columnCount; ++_walk) {
			const size_t colWalk = (size_t) order[_walk];
			PFC_ASSERT( colWalk < columnCount );
			if (mask[colWalk]) {
				lastCol = colWalk;
				usedWidth += widths[colWalk];
			} else {
				usedWidth += GetSubItemWidth(colWalk);
			}
		}
		if (lastCol != SIZE_MAX) {
			t_uint32 clientWidth = this->GetClientRectHook().Width(); 
			if (clientWidth > 0) --clientWidth; // $!@# scrollbar hack
			if (usedWidth < clientWidth) {
				widths[lastCol] += clientWidth - usedWidth;
			}
		}
	}
	for(t_size colWalk = mask.find_first(true,0,columnCount); colWalk < columnCount; colWalk = mask.find_next(true,colWalk,columnCount)) {
		ResizeColumn(colWalk,widths[colWalk],false);
	}
	ProcessColumnsChange();
}

t_uint32 CListControlHeaderImpl::GetOptimalGroupHeaderWidth(int which) const {
	CWindowDC dc(*this);
	SelectObjectScope fontScope(dc,GetGroupHeaderFont());
	GetOptimalWidth_Cache cache; cache.m_dc = dc;
	const t_uint32 base = this->GetColumnSpacing() * 2;
	if (GetGroupHeaderText(which,cache.m_stringTemp)) {
		return base + cache.GetStringTempWidth();
	} else {
		return base;
	}
}

size_t CListControlHeaderImpl::GetColumnCount() const {
	if ( ! IsHeaderEnabled() ) return 1;
#if PFC_DEBUG
	int iHeaderCount = m_header.GetItemCount();
	PFC_ASSERT( m_colRuntime.size() == (size_t) iHeaderCount );
#endif
	return m_colRuntime.size();
}

void CListControlHeaderImpl::ColumnWidthFix() {
	if ( this->HaveAutoWidthColumns() ) {
		ProcessAutoWidth();
	} else {
		RecalcItemWidth();
	}
}

void CListControlHeaderImpl::ProcessAutoWidth() {
	if ( HaveAutoWidthColumns() ) {
		const int clientWidth = this->GetClientRectHook().Width();

		if ( ! this->HaveAutoWidthContentColumns( ) && clientWidth == m_itemWidth) return;

		const size_t count = GetColumnCount();
		uint32_t totalNonAuto = 0;
		size_t numAutoWidth = 0;
		for(size_t walk = 0; walk < count; ++ walk ) {
			if ( m_colRuntime[walk].autoWidth() ) {
				++ numAutoWidth;
			} else {
				totalNonAuto += GetSubItemWidth(walk);
			}
		}
		int toDivide = clientWidth - totalNonAuto;
		if ( toDivide < 0 ) toDivide = 0;

		size_t numLeft = numAutoWidth;
		auto worker = [&] ( size_t iCol ) {
			auto & rt = m_colRuntime[iCol];
			int lo = this->GetOptimalColumnWidthFixed( rt.m_text.c_str() );
			if ( rt.autoWidthContent() ) {
				int lo2 = this->GetOptimalColumnWidth( iCol );
				if ( lo < lo2 ) lo = lo2;
			}
			int width = (int)(toDivide / numLeft);
			if ( width < lo ) width = lo;
			
			HDITEM item = {};
			item.mask = HDI_WIDTH;
			item.cxy = width;
			WIN32_OP_D( m_header.SetItem( iCol, &item ) );
			rt.m_widthPixels = width;

			if ( toDivide > width ) {
				toDivide -= width;
			} else {
				toDivide = 0;
			}
			-- numLeft;

		};
		for( size_t iCol = 0; iCol < count; ++ iCol ) {
			if (m_colRuntime[iCol].autoWidthContent() ) worker(iCol);
		}
		for( size_t iCol = 0; iCol < count; ++ iCol ) {
			if ( m_colRuntime[iCol].autoWidthPlain() ) worker(iCol);
		}

		RecalcItemWidth();
		OnColumnsChanged();
		m_header.Invalidate();
	}
}

void CListControlHeaderImpl::RecalcItemWidth() {
	int total = 0;
	const t_size count = GetColumnCount();
	for(t_size walk = 0; walk < count; ++walk) total += GetSubItemWidth(walk);
	m_itemWidth = total;
}

CRect CListControlHeaderImpl::GetSubItemRectAbs(t_size item,t_size subItem) const {
	CRect rc = GetItemRectAbs(item);
	auto order = GetColumnOrderArray();
	const t_size colCount = order.size();
	for(t_size _walk = 0; _walk < colCount; ++_walk) {
		const t_size walk = (t_size) order[_walk];

		t_size width = this->GetSubItemWidth(walk);
		if (subItem == walk) {
			
			size_t span = GetSubItemSpan(item, walk);
			if ( walk + span > colCount ) span = colCount - walk;
			for( size_t extra = 1; extra < span; ++ extra ) {
				width += GetSubItemWidth( walk + extra);
			}
			
			rc.right = rc.left + (long)width;

			return rc;
		} else {
			rc.left += (long)width;
		}
	}
	throw pfc::exception_invalid_params();
}

CRect CListControlHeaderImpl::GetSubItemRect(t_size item,t_size subItem) const {
	CRect rc = GetSubItemRectAbs(item,subItem); rc.OffsetRect(-GetViewOffset()); return rc;
}

void CListControlHeaderImpl::SetHotItem(size_t row, size_t column) {
	if ( m_hotItem != row  || m_hotSubItem != column ) {
		if (m_hotItem != pfc_infinite) InvalidateRect(GetSubItemRect(m_hotItem, m_hotSubItem));
		m_hotItem = row; m_hotSubItem = column;
		if (m_hotItem != pfc_infinite) InvalidateRect(GetSubItemRect(m_hotItem, m_hotSubItem));
	}
}

void CListControlHeaderImpl::SetPressedItem(size_t row, size_t column) {
	if (m_pressedItem != row || m_pressedSubItem != column) {
		if (m_pressedItem != pfc_infinite) InvalidateRect(GetSubItemRect(m_pressedItem, m_pressedSubItem));
		m_pressedItem = row; m_pressedSubItem = column;
		if (m_pressedItem != pfc_infinite) InvalidateRect(GetSubItemRect(m_pressedItem, m_pressedSubItem));
	}
}

void CListControlHeaderImpl::SetCellCheckState(size_t item, size_t subItem, bool value) {
	ReloadItem(item); (void)subItem; (void)value;
	// Subclass deals with keeping track of state
}

bool CListControlHeaderImpl::ToggleSelectedItemsHook(const pfc::bit_array & mask) {
	if (this->GetCellTypeSupported() ) {
		bool handled = false;
		bool setTo = true;

		mask.walk(GetItemCount(), [&](size_t idx) {
			auto ct = this->GetCellType(idx, 0);
			if ( ct != nullptr && ct->IsToggle() ) {
				if ( ct->IsRadioToggle() ) {
					if (!handled) {
						handled = true;
						setTo = !this->GetCellCheckState(idx, 0);
						this->SetCellCheckState(idx, 0, setTo);
					}
				} else {
					if (!handled) {
						handled = true;
						setTo = ! this->GetCellCheckState(idx,0);
					}
					this->SetCellCheckState(idx,0,setTo);
				}
			}
		});

		if (handled) return true;
	}
	return __super::ToggleSelectedItemsHook(mask);
}

void CListControlHeaderImpl::OnSubItemClicked(t_size item, t_size subItem, CPoint pt) {
	auto ct = GetCellType(item, subItem);
	if ( ct != nullptr && ct->IsToggle() ) {
		if ( ct->HotRect(GetSubItemRect(item, subItem)).PtInRect(pt) ) {
			this->SetCellCheckState( item, subItem, ! GetCellCheckState( item, subItem ) );
		}
	}
}


bool CListControlHeaderImpl::AllowTypeFindInCell(size_t item, size_t subItem) const {
	auto cell = GetCellType( item, subItem );
	if ( cell == nullptr ) return false;
	return cell->AllowTypeFind();
}

bool CListControlHeaderImpl::CellTypeReactsToMouseOver(cellType_t ct) {
	return ct != nullptr && ct->IsInteractive();
}

CRect CListControlHeaderImpl::CellHotRect( size_t, size_t, cellType_t ct, CRect rcCell) {
	if ( ct != nullptr ) {
		return ct->HotRect(rcCell);
	}
	return rcCell;
}
CRect CListControlHeaderImpl::CellHotRect(size_t item, size_t subItem, cellType_t ct) {
	return CellHotRect( item, subItem, ct, GetSubItemRect( item, subItem ) );
}
void CListControlHeaderImpl::OnMouseMove(UINT nFlags, CPoint pt) {
	const DWORD maskButtons = MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2;
	if (GetCellTypeSupported() && (nFlags & maskButtons) == 0 ) {
		size_t item; size_t subItem;
		if (this->GetItemAtPointAbsEx(pt + GetViewOffset(), item, subItem)) {
			auto ct = this->GetCellType( item, subItem );
			if (CellTypeReactsToMouseOver(ct) ) {
				auto rc = CellHotRect( item, subItem, ct );
				if ( PtInRect( rc, pt ) ) {
					{
						auto c = ct->HotCursor();
						if ( c != NULL ) SetCursor(c);
					}
					SetHotItem(item, subItem);
					SetCaptureEx([=](UINT msg, DWORD newStatus, CPoint pt) {
						if ((newStatus & maskButtons) != 0 || msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL ) {
							// A button has been pressed or wheel has been moved
							this->ClearHotItem();
							SetCaptureMsgHandled(FALSE);
							return false;
						}

						if ( ! PtInRect( rc, pt ) ) {
							// Left the rect
							this->ClearHotItem();
							SetCaptureMsgHandled(FALSE);
							return false;
						}

						return true;
					});
				}
			}
		}
	}
	SetMsgHandled(FALSE);
}

bool CListControlHeaderImpl::AllowScrollbar(bool vertical) const {
	if ( vertical ) {
		// vertical
		return true;
	} else {
		// horizontal
		if (! IsHeaderEnabled( ) ) return false; // no header?
		return true;
	}
}

void CListControlHeaderImpl::OnDestroy() {
	m_colRuntime.clear();
	m_header = NULL;
	m_headerLine = NULL;
	SetMsgHandled(FALSE);
}


uint32_t CListControlHeaderImpl::GetColumnsBlankWidth( size_t colExclude ) const {
	auto client = this->GetClientRectHook().Width();
	int item = GetItemWidth();
	if (colExclude) item -= GetSubItemWidth(colExclude);
	if ( item  < 0 ) item = 0;
	if ( item < client ) return (uint32_t)( client - item );
	else return 0;
}

void CListControlHeaderImpl::SizeColumnToContent( size_t which, uint32_t minWidth ) {
	auto width = this->GetOptimalColumnWidth( which );
	if ( width < minWidth ) width = minWidth;
	this->ResizeColumn( which, width );
}

void CListControlHeaderImpl::SizeColumnToContentFillBlank( size_t which ) {
	this->SizeColumnToContent( which, this->GetColumnsBlankWidth(which) );
}

HBRUSH CListControlHeaderImpl::OnCtlColorStatic(CDCHandle dc, CStatic wndStatic) {
	if ( wndStatic == m_headerLine ) {
		COLORREF col = GridColor();
		dc.SetDCBrushColor( col );
		return (HBRUSH) GetStockObject(DC_BRUSH);
	}
	SetMsgHandled(FALSE);
	return NULL;
}

void CListControlHeaderImpl::ReloadData() {
	__super::ReloadData();
	if ( this->HaveAutoWidthContentColumns( ) ) {
		this->ColumnWidthFix();
	}
}
