// Copyright 2011 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package walk

import (
	"encoding/json"
	"fmt"
	"math/big"
	"reflect"
	"syscall"
	"time"
	"unsafe"

	"github.com/lxn/win"
)

const tableViewWindowClass = `\o/ Walk_TableView_Class \o/`

var (
	white                       = win.COLORREF(RGB(255, 255, 255))
	checkmark                   = string([]byte{0xE2, 0x9C, 0x94})
	tableViewFrozenLVWndProcPtr uintptr
	tableViewNormalLVWndProcPtr uintptr
	tableViewHdrWndProcPtr      uintptr
)

func init() {
	AppendToWalkInit(func() {
		MustRegisterWindowClass(tableViewWindowClass)
		tableViewFrozenLVWndProcPtr = syscall.NewCallback(tableViewFrozenLVWndProc)
		tableViewNormalLVWndProcPtr = syscall.NewCallback(tableViewNormalLVWndProc)
		tableViewHdrWndProcPtr = syscall.NewCallback(tableViewHdrWndProc)
	})
}

const (
	tableViewCurrentIndexChangedTimerId = 1 + iota
	tableViewSelectedIndexesChangedTimerId
)

type TableViewCfg struct {
	Style              uint32
	CustomHeaderHeight int // in native pixels?
	CustomRowHeight    int // in native pixels?
}

// TableView is a model based widget for record centric, tabular data.
//
// TableView is implemented as a virtual mode list view to support quite large
// amounts of data.
type TableView struct {
	WidgetBase
	hwndFrozenLV                       win.HWND
	hwndFrozenHdr                      win.HWND
	frozenLVOrigWndProcPtr             uintptr
	frozenHdrOrigWndProcPtr            uintptr
	hwndNormalLV                       win.HWND
	hwndNormalHdr                      win.HWND
	normalLVOrigWndProcPtr             uintptr
	normalHdrOrigWndProcPtr            uintptr
	state                              *tableViewState
	columns                            *TableViewColumnList
	model                              TableModel
	providedModel                      interface{}
	itemChecker                        ItemChecker
	imageProvider                      ImageProvider
	styler                             CellStyler
	style                              CellStyle
	itemFont                           *Font
	hIml                               win.HIMAGELIST
	usingSysIml                        bool
	imageUintptr2Index                 map[uintptr]int32
	filePath2IconIndex                 map[string]int32
	rowsResetHandlerHandle             int
	rowChangedHandlerHandle            int
	rowsChangedHandlerHandle           int
	rowsInsertedHandlerHandle          int
	rowsRemovedHandlerHandle           int
	sortChangedHandlerHandle           int
	selectedIndexes                    []int
	prevIndex                          int
	currentIndex                       int
	itemIndexOfLastMouseButtonDown     int
	hwndItemChanged                    win.HWND
	currentIndexChangedPublisher       EventPublisher
	selectedIndexesChangedPublisher    EventPublisher
	itemActivatedPublisher             EventPublisher
	columnClickedPublisher             IntEventPublisher
	columnsOrderableChangedPublisher   EventPublisher
	columnsSizableChangedPublisher     EventPublisher
	itemCountChangedPublisher          EventPublisher
	publishNextSelClear                bool
	inSetSelectedIndexes               bool
	lastColumnStretched                bool
	persistent                         bool
	itemStateChangedEventDelay         int
	themeNormalBGColor                 Color
	themeNormalTextColor               Color
	themeSelectedBGColor               Color
	themeSelectedTextColor             Color
	themeSelectedNotFocusedBGColor     Color
	itemBGColor                        Color
	itemTextColor                      Color
	alternatingRowBGColor              Color
	alternatingRowTextColor            Color
	alternatingRowBG                   bool
	delayedCurrentIndexChangedCanceled bool
	sortedColumnIndex                  int
	sortOrder                          SortOrder
	formActivatingHandle               int
	customHeaderHeight                 int // in native pixels?
	customRowHeight                    int // in native pixels?
	dpiOfPrevStretchLastColumn         int
	scrolling                          bool
	inSetCurrentIndex                  bool
	inMouseEvent                       bool
	hasFrozenColumn                    bool
	busyStretchingLastColumn           bool
	focused                            bool
	ignoreNowhere                      bool
	updateLVSizesNeedsSpecialCare      bool
	scrollbarOrientation               Orientation
}

// NewTableView creates and returns a *TableView as child of the specified
// Container.
func NewTableView(parent Container) (*TableView, error) {
	return NewTableViewWithStyle(parent, win.LVS_SHOWSELALWAYS)
}

// NewTableViewWithStyle creates and returns a *TableView as child of the specified
// Container and with the provided additional style bits set.
func NewTableViewWithStyle(parent Container, style uint32) (*TableView, error) {
	return NewTableViewWithCfg(parent, &TableViewCfg{Style: style})
}

// NewTableViewWithCfg creates and returns a *TableView as child of the specified
// Container and with the provided additional configuration.
func NewTableViewWithCfg(parent Container, cfg *TableViewCfg) (*TableView, error) {
	tv := &TableView{
		imageUintptr2Index:   make(map[uintptr]int32),
		filePath2IconIndex:   make(map[string]int32),
		formActivatingHandle: -1,
		customHeaderHeight:   cfg.CustomHeaderHeight,
		customRowHeight:      cfg.CustomRowHeight,
		scrollbarOrientation: Horizontal | Vertical,
	}

	tv.columns = newTableViewColumnList(tv)

	if err := InitWidget(
		tv,
		parent,
		tableViewWindowClass,
		win.WS_BORDER|win.WS_VISIBLE,
		win.WS_EX_CONTROLPARENT); err != nil {
		return nil, err
	}

	succeeded := false
	defer func() {
		if !succeeded {
			tv.Dispose()
		}
	}()

	var rowHeightStyle uint32
	if cfg.CustomRowHeight > 0 {
		rowHeightStyle = win.LVS_OWNERDRAWFIXED
	}

	if tv.hwndFrozenLV = win.CreateWindowEx(
		0,
		syscall.StringToUTF16Ptr("SysListView32"),
		nil,
		win.WS_CHILD|win.WS_CLIPSIBLINGS|win.WS_TABSTOP|win.WS_VISIBLE|win.LVS_OWNERDATA|win.LVS_REPORT|cfg.Style|rowHeightStyle,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		tv.hWnd,
		0,
		0,
		nil,
	); tv.hwndFrozenLV == 0 {
		return nil, newError("creating frozen lv failed")
	}

	tv.frozenLVOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndFrozenLV, win.GWLP_WNDPROC, tableViewFrozenLVWndProcPtr)
	if tv.frozenLVOrigWndProcPtr == 0 {
		return nil, lastError("SetWindowLongPtr")
	}

	tv.hwndFrozenHdr = win.HWND(win.SendMessage(tv.hwndFrozenLV, win.LVM_GETHEADER, 0, 0))
	tv.frozenHdrOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndFrozenHdr, win.GWLP_WNDPROC, tableViewHdrWndProcPtr)
	if tv.frozenHdrOrigWndProcPtr == 0 {
		return nil, lastError("SetWindowLongPtr")
	}

	if tv.hwndNormalLV = win.CreateWindowEx(
		0,
		syscall.StringToUTF16Ptr("SysListView32"),
		nil,
		win.WS_CHILD|win.WS_CLIPSIBLINGS|win.WS_TABSTOP|win.WS_VISIBLE|win.LVS_OWNERDATA|win.LVS_REPORT|cfg.Style|rowHeightStyle,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		win.CW_USEDEFAULT,
		tv.hWnd,
		0,
		0,
		nil,
	); tv.hwndNormalLV == 0 {
		return nil, newError("creating normal lv failed")
	}

	tv.normalLVOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndNormalLV, win.GWLP_WNDPROC, tableViewNormalLVWndProcPtr)
	if tv.normalLVOrigWndProcPtr == 0 {
		return nil, lastError("SetWindowLongPtr")
	}

	tv.hwndNormalHdr = win.HWND(win.SendMessage(tv.hwndNormalLV, win.LVM_GETHEADER, 0, 0))
	tv.normalHdrOrigWndProcPtr = win.SetWindowLongPtr(tv.hwndNormalHdr, win.GWLP_WNDPROC, tableViewHdrWndProcPtr)
	if tv.normalHdrOrigWndProcPtr == 0 {
		return nil, lastError("SetWindowLongPtr")
	}

	tv.SetPersistent(true)

	exStyle := win.SendMessage(tv.hwndFrozenLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	exStyle |= win.LVS_EX_DOUBLEBUFFER | win.LVS_EX_FULLROWSELECT | win.LVS_EX_HEADERDRAGDROP | win.LVS_EX_LABELTIP | win.LVS_EX_SUBITEMIMAGES
	win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
	win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)

	if hr := win.SetWindowTheme(tv.hwndFrozenLV, syscall.StringToUTF16Ptr("Explorer"), nil); win.FAILED(hr) {
		return nil, errorFromHRESULT("SetWindowTheme", hr)
	}
	if hr := win.SetWindowTheme(tv.hwndNormalLV, syscall.StringToUTF16Ptr("Explorer"), nil); win.FAILED(hr) {
		return nil, errorFromHRESULT("SetWindowTheme", hr)
	}

	win.SendMessage(tv.hwndFrozenLV, win.WM_CHANGEUISTATE, uintptr(win.MAKELONG(win.UIS_SET, win.UISF_HIDEFOCUS)), 0)
	win.SendMessage(tv.hwndNormalLV, win.WM_CHANGEUISTATE, uintptr(win.MAKELONG(win.UIS_SET, win.UISF_HIDEFOCUS)), 0)

	tv.group.toolTip.addTool(tv.hwndFrozenHdr, false)
	tv.group.toolTip.addTool(tv.hwndNormalHdr, false)

	tv.applyFont(parent.Font())

	tv.style.dpi = tv.DPI()
	tv.ApplySysColors()

	tv.currentIndex = -1

	tv.GraphicsEffects().Add(InteractionEffect)
	tv.GraphicsEffects().Add(FocusEffect)

	tv.MustRegisterProperty("ColumnsOrderable", NewBoolProperty(
		func() bool {
			return tv.ColumnsOrderable()
		},
		func(b bool) error {
			tv.SetColumnsOrderable(b)
			return nil
		},
		tv.columnsOrderableChangedPublisher.Event()))

	tv.MustRegisterProperty("ColumnsSizable", NewBoolProperty(
		func() bool {
			return tv.ColumnsSizable()
		},
		func(b bool) error {
			return tv.SetColumnsSizable(b)
		},
		tv.columnsSizableChangedPublisher.Event()))

	tv.MustRegisterProperty("CurrentIndex", NewProperty(
		func() interface{} {
			return tv.CurrentIndex()
		},
		func(v interface{}) error {
			return tv.SetCurrentIndex(assertIntOr(v, -1))
		},
		tv.CurrentIndexChanged()))

	tv.MustRegisterProperty("CurrentItem", NewReadOnlyProperty(
		func() interface{} {
			if i := tv.CurrentIndex(); i > -1 {
				if rm, ok := tv.providedModel.(reflectModel); ok {
					return reflect.ValueOf(rm.Items()).Index(i).Interface()
				}
			}

			return nil
		},
		tv.CurrentIndexChanged()))

	tv.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty(
		func() bool {
			return tv.CurrentIndex() != -1
		},
		tv.CurrentIndexChanged()))

	tv.MustRegisterProperty("ItemCount", NewReadOnlyProperty(
		func() interface{} {
			if tv.model == nil {
				return 0
			}
			return tv.model.RowCount()
		},
		tv.itemCountChangedPublisher.Event()))

	tv.MustRegisterProperty("SelectedCount", NewReadOnlyProperty(
		func() interface{} {
			return len(tv.selectedIndexes)
		},
		tv.SelectedIndexesChanged()))

	succeeded = true

	return tv, nil
}

func (tv *TableView) asTableView() *TableView {
	return tv
}

// Dispose releases the operating system resources, associated with the
// *TableView.
func (tv *TableView) Dispose() {
	tv.columns.unsetColumnsTV()

	tv.disposeImageListAndCaches()

	if tv.hWnd != 0 {
		if !win.KillTimer(tv.hWnd, tableViewCurrentIndexChangedTimerId) {
			lastError("KillTimer")
		}
		if !win.KillTimer(tv.hWnd, tableViewSelectedIndexesChangedTimerId) {
			lastError("KillTimer")
		}
	}

	if tv.hwndFrozenLV != 0 {
		tv.group.toolTip.removeTool(tv.hwndFrozenHdr)
		win.DestroyWindow(tv.hwndFrozenLV)
		tv.hwndFrozenLV = 0
	}

	if tv.hwndNormalLV != 0 {
		tv.group.toolTip.removeTool(tv.hwndNormalHdr)
		win.DestroyWindow(tv.hwndNormalLV)
		tv.hwndNormalLV = 0
	}

	if tv.formActivatingHandle > -1 {
		if form := tv.Form(); form != nil {
			form.Activating().Detach(tv.formActivatingHandle)
		}
		tv.formActivatingHandle = -1
	}

	tv.WidgetBase.Dispose()
}

func (tv *TableView) applyEnabled(enabled bool) {
	tv.WidgetBase.applyEnabled(enabled)

	win.EnableWindow(tv.hwndFrozenLV, enabled)
	win.EnableWindow(tv.hwndNormalLV, enabled)
}

func (tv *TableView) applyFont(font *Font) {
	if tv.customHeaderHeight > 0 || tv.customRowHeight > 0 {
		return
	}

	tv.WidgetBase.applyFont(font)

	hFont := uintptr(font.handleForDPI(tv.DPI()))

	win.SendMessage(tv.hwndFrozenLV, win.WM_SETFONT, hFont, 0)
	win.SendMessage(tv.hwndNormalLV, win.WM_SETFONT, hFont, 0)
}

func (tv *TableView) ApplyDPI(dpi int) {
	tv.style.dpi = dpi
	if tv.style.canvas != nil {
		tv.style.canvas.dpi = dpi
	}

	tv.WidgetBase.ApplyDPI(dpi)

	for _, column := range tv.columns.items {
		column.update()
	}

	if tv.hIml != 0 {
		tv.disposeImageListAndCaches()

		if bmp, err := NewBitmapForDPI(SizeFrom96DPI(Size{16, 16}, dpi), dpi); err == nil {
			tv.applyImageListForImage(bmp)
			bmp.Dispose()
		}
	}
}

func (tv *TableView) ApplySysColors() {
	tv.WidgetBase.ApplySysColors()

	// As some combinations of property and state may be invalid for any theme,
	// we set some defaults here.
	tv.themeNormalBGColor = Color(win.GetSysColor(win.COLOR_WINDOW))
	tv.themeNormalTextColor = Color(win.GetSysColor(win.COLOR_WINDOWTEXT))
	tv.themeSelectedBGColor = tv.themeNormalBGColor
	tv.themeSelectedTextColor = tv.themeNormalTextColor
	tv.themeSelectedNotFocusedBGColor = tv.themeNormalBGColor
	tv.alternatingRowBGColor = Color(win.GetSysColor(win.COLOR_BTNFACE))
	tv.alternatingRowTextColor = Color(win.GetSysColor(win.COLOR_BTNTEXT))

	type item struct {
		stateID    int32
		propertyID int32
		color      *Color
	}

	getThemeColor := func(theme win.HTHEME, partId int32, items []item) {
		for _, item := range items {
			var c win.COLORREF
			if result := win.GetThemeColor(theme, partId, item.stateID, item.propertyID, &c); !win.FAILED(result) {
				(*item.color) = Color(c)
			}
		}
	}

	if hThemeListView := win.OpenThemeData(tv.hwndNormalLV, syscall.StringToUTF16Ptr("Listview")); hThemeListView != 0 {
		defer win.CloseThemeData(hThemeListView)

		getThemeColor(hThemeListView, win.LVP_LISTITEM, []item{
			{win.LISS_NORMAL, win.TMT_FILLCOLOR, &tv.themeNormalBGColor},
			{win.LISS_NORMAL, win.TMT_TEXTCOLOR, &tv.themeNormalTextColor},
			{win.LISS_SELECTED, win.TMT_FILLCOLOR, &tv.themeSelectedBGColor},
			{win.LISS_SELECTED, win.TMT_TEXTCOLOR, &tv.themeSelectedTextColor},
			{win.LISS_SELECTEDNOTFOCUS, win.TMT_FILLCOLOR, &tv.themeSelectedNotFocusedBGColor},
		})
	} else {
		// The others already have been retrieved above.
		tv.themeSelectedBGColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHT))
		tv.themeSelectedTextColor = Color(win.GetSysColor(win.COLOR_HIGHLIGHTTEXT))
		tv.themeSelectedNotFocusedBGColor = Color(win.GetSysColor(win.COLOR_BTNFACE))
	}

	if hThemeButton := win.OpenThemeData(tv.hwndNormalLV, syscall.StringToUTF16Ptr("BUTTON")); hThemeButton != 0 {
		defer win.CloseThemeData(hThemeButton)

		getThemeColor(hThemeButton, win.BP_PUSHBUTTON, []item{
			{win.PBS_NORMAL, win.TMT_FILLCOLOR, &tv.alternatingRowBGColor},
			{win.PBS_NORMAL, win.TMT_TEXTCOLOR, &tv.alternatingRowTextColor},
		})
	}

	win.SendMessage(tv.hwndNormalLV, win.LVM_SETBKCOLOR, 0, uintptr(tv.themeNormalBGColor))
	win.SendMessage(tv.hwndFrozenLV, win.LVM_SETBKCOLOR, 0, uintptr(tv.themeNormalBGColor))
}

// ColumnsOrderable returns if the user can reorder columns by dragging and
// dropping column headers.
func (tv *TableView) ColumnsOrderable() bool {
	exStyle := win.SendMessage(tv.hwndNormalLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	return exStyle&win.LVS_EX_HEADERDRAGDROP > 0
}

// SetColumnsOrderable sets if the user can reorder columns by dragging and
// dropping column headers.
func (tv *TableView) SetColumnsOrderable(enabled bool) {
	var hwnd win.HWND
	if tv.hasFrozenColumn {
		hwnd = tv.hwndFrozenLV
	} else {
		hwnd = tv.hwndNormalLV
	}

	exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	if enabled {
		exStyle |= win.LVS_EX_HEADERDRAGDROP
	} else {
		exStyle &^= win.LVS_EX_HEADERDRAGDROP
	}
	win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
	win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)

	tv.columnsOrderableChangedPublisher.Publish()
}

// ColumnsSizable returns if the user can change column widths by dragging
// dividers in the header.
func (tv *TableView) ColumnsSizable() bool {
	style := win.GetWindowLong(tv.hwndNormalHdr, win.GWL_STYLE)

	return style&win.HDS_NOSIZING == 0
}

// SetColumnsSizable sets if the user can change column widths by dragging
// dividers in the header.
func (tv *TableView) SetColumnsSizable(b bool) error {
	updateStyle := func(headerHWnd win.HWND) error {
		style := win.GetWindowLong(headerHWnd, win.GWL_STYLE)

		if b {
			style &^= win.HDS_NOSIZING
		} else {
			style |= win.HDS_NOSIZING
		}

		if 0 == win.SetWindowLong(headerHWnd, win.GWL_STYLE, style) {
			return lastError("SetWindowLong(GWL_STYLE)")
		}

		return nil
	}

	if err := updateStyle(tv.hwndFrozenHdr); err != nil {
		return err
	}
	if err := updateStyle(tv.hwndNormalHdr); err != nil {
		return err
	}

	tv.columnsSizableChangedPublisher.Publish()

	return nil
}

// ContextMenuLocation returns selected item position in screen coordinates in native pixels.
func (tv *TableView) ContextMenuLocation() Point {
	idx := win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTIONMARK, 0, 0)
	rc := win.RECT{Left: win.LVIR_BOUNDS}
	if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_GETITEMRECT, idx, uintptr(unsafe.Pointer(&rc))) {
		return tv.WidgetBase.ContextMenuLocation()
	}
	var pt win.POINT
	if tv.RightToLeftReading() {
		pt.X = rc.Right
	} else {
		pt.X = rc.Left
	}
	pt.X = rc.Bottom
	windowTrimToClientBounds(tv.hwndNormalLV, &pt)
	win.ClientToScreen(tv.hwndNormalLV, &pt)
	return pointPixelsFromPOINT(pt)
}

// SortableByHeaderClick returns if the user can change sorting by clicking the header.
func (tv *TableView) SortableByHeaderClick() bool {
	return !hasWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_NOSORTHEADER) ||
		!hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_NOSORTHEADER)
}

// HeaderHidden returns whether the column header is hidden.
func (tv *TableView) HeaderHidden() bool {
	style := win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE)

	return style&win.LVS_NOCOLUMNHEADER != 0
}

// SetHeaderHidden sets whether the column header is hidden.
func (tv *TableView) SetHeaderHidden(hidden bool) error {
	if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_NOCOLUMNHEADER, hidden); err != nil {
		return err
	}

	return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_NOCOLUMNHEADER, hidden)
}

// AlternatingRowBG returns the alternating row background.
func (tv *TableView) AlternatingRowBG() bool {
	return tv.alternatingRowBG
}

// SetAlternatingRowBG sets the alternating row background.
func (tv *TableView) SetAlternatingRowBG(enabled bool) {
	tv.alternatingRowBG = enabled

	tv.Invalidate()
}

// Gridlines returns if the rows are separated by grid lines.
func (tv *TableView) Gridlines() bool {
	exStyle := win.SendMessage(tv.hwndNormalLV, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	return exStyle&win.LVS_EX_GRIDLINES > 0
}

// SetGridlines sets if the rows are separated by grid lines.
func (tv *TableView) SetGridlines(enabled bool) {
	var hwnd win.HWND
	if tv.hasFrozenColumn {
		hwnd = tv.hwndFrozenLV
	} else {
		hwnd = tv.hwndNormalLV
	}

	exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	if enabled {
		exStyle |= win.LVS_EX_GRIDLINES
	} else {
		exStyle &^= win.LVS_EX_GRIDLINES
	}
	win.SendMessage(tv.hwndFrozenLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
	win.SendMessage(tv.hwndNormalLV, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
}

// Columns returns the list of columns.
func (tv *TableView) Columns() *TableViewColumnList {
	return tv.columns
}

// VisibleColumnsInDisplayOrder returns a slice of visible columns in display
// order.
func (tv *TableView) VisibleColumnsInDisplayOrder() []*TableViewColumn {
	visibleCols := tv.visibleColumns()
	indices := make([]int32, len(visibleCols))

	frozenCount := tv.visibleFrozenColumnCount()
	normalCount := len(visibleCols) - frozenCount

	if frozenCount > 0 {
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(frozenCount), uintptr(unsafe.Pointer(&indices[0]))) {
			newError("LVM_GETCOLUMNORDERARRAY")
			return nil
		}
	}
	if normalCount > 0 {
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(normalCount), uintptr(unsafe.Pointer(&indices[frozenCount]))) {
			newError("LVM_GETCOLUMNORDERARRAY")
			return nil
		}
	}

	orderedCols := make([]*TableViewColumn, len(visibleCols))

	for i, j := range indices {
		if i >= frozenCount {
			j += int32(frozenCount)
		}
		orderedCols[i] = visibleCols[j]
	}

	return orderedCols
}

// RowsPerPage returns the number of fully visible rows.
func (tv *TableView) RowsPerPage() int {
	return int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOUNTPERPAGE, 0, 0))
}

func (tv *TableView) Invalidate() error {
	win.InvalidateRect(tv.hwndFrozenLV, nil, true)
	win.InvalidateRect(tv.hwndNormalLV, nil, true)

	return tv.WidgetBase.Invalidate()
}

func (tv *TableView) redrawItems() {
	first := win.SendMessage(tv.hwndNormalLV, win.LVM_GETTOPINDEX, 0, 0)
	last := first + win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOUNTPERPAGE, 0, 0) + 1
	win.SendMessage(tv.hwndFrozenLV, win.LVM_REDRAWITEMS, first, last)
	win.SendMessage(tv.hwndNormalLV, win.LVM_REDRAWITEMS, first, last)
}

// UpdateItem ensures the item at index will be redrawn.
//
// If the model supports sorting, it will be resorted.
func (tv *TableView) UpdateItem(index int) error {
	if s, ok := tv.model.(Sorter); ok {
		if err := s.Sort(s.SortedColumn(), s.SortOrder()); err != nil {
			return err
		}
	} else {
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_UPDATE, uintptr(index), 0) {
			return newError("LVM_UPDATE")
		}
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_UPDATE, uintptr(index), 0) {
			return newError("LVM_UPDATE")
		}
	}

	return nil
}

func (tv *TableView) attachModel() {
	tv.rowsResetHandlerHandle = tv.model.RowsReset().Attach(func() {
		tv.setItemCount()

		tv.SetCurrentIndex(-1)

		tv.itemCountChangedPublisher.Publish()
	})

	tv.rowChangedHandlerHandle = tv.model.RowChanged().Attach(func(row int) {
		tv.UpdateItem(row)
	})

	tv.rowsChangedHandlerHandle = tv.model.RowsChanged().Attach(func(from, to int) {
		if s, ok := tv.model.(Sorter); ok {
			s.Sort(s.SortedColumn(), s.SortOrder())
		} else {
			first, last := uintptr(from), uintptr(to)
			win.SendMessage(tv.hwndFrozenLV, win.LVM_REDRAWITEMS, first, last)
			win.SendMessage(tv.hwndNormalLV, win.LVM_REDRAWITEMS, first, last)
		}
	})

	tv.rowsInsertedHandlerHandle = tv.model.RowsInserted().Attach(func(from, to int) {
		i := tv.currentIndex

		tv.setItemCount()

		if from <= i {
			i += 1 + to - from

			tv.SetCurrentIndex(i)
		}

		tv.itemCountChangedPublisher.Publish()
	})

	tv.rowsRemovedHandlerHandle = tv.model.RowsRemoved().Attach(func(from, to int) {
		i := tv.currentIndex

		tv.setItemCount()

		index := i

		if from <= i && i <= to {
			index = -1
		} else if from < i {
			index -= 1 + to - from
		}

		if index != i {
			tv.SetCurrentIndex(index)
		}

		tv.itemCountChangedPublisher.Publish()
	})

	if sorter, ok := tv.model.(Sorter); ok {
		tv.sortChangedHandlerHandle = sorter.SortChanged().Attach(func() {
			col := sorter.SortedColumn()
			tv.setSortIcon(col, sorter.SortOrder())

			tv.redrawItems()
		})
	}
}

func (tv *TableView) detachModel() {
	tv.model.RowsReset().Detach(tv.rowsResetHandlerHandle)
	tv.model.RowChanged().Detach(tv.rowChangedHandlerHandle)
	tv.model.RowsInserted().Detach(tv.rowsInsertedHandlerHandle)
	tv.model.RowsRemoved().Detach(tv.rowsRemovedHandlerHandle)
	if sorter, ok := tv.model.(Sorter); ok {
		sorter.SortChanged().Detach(tv.sortChangedHandlerHandle)
	}
}

// ItemCountChanged returns the event that is published when the number of items
// in the model of the TableView changed.
func (tv *TableView) ItemCountChanged() *Event {
	return tv.itemCountChangedPublisher.Event()
}

// Model returns the model of the TableView.
func (tv *TableView) Model() interface{} {
	return tv.providedModel
}

// SetModel sets the model of the TableView.
//
// It is required that mdl either implements walk.TableModel,
// walk.ReflectTableModel or be a slice of pointers to struct or a
// []map[string]interface{}. A walk.TableModel implementation must also
// implement walk.Sorter to support sorting, all other options get sorting for
// free. To support item check boxes and icons, mdl must implement
// walk.ItemChecker and walk.ImageProvider, respectively. On-demand model
// population for a walk.ReflectTableModel or slice requires mdl to implement
// walk.Populator.
func (tv *TableView) SetModel(mdl interface{}) error {
	model, ok := mdl.(TableModel)
	if !ok && mdl != nil {
		var err error
		if model, err = newReflectTableModel(mdl); err != nil {
			if model, err = newMapTableModel(mdl); err != nil {
				return err
			}
		}
	}

	tv.SetSuspended(true)
	defer tv.SetSuspended(false)

	if tv.model != nil {
		tv.detachModel()

		tv.disposeImageListAndCaches()
	}

	oldProvidedModelStyler, _ := tv.providedModel.(CellStyler)
	if styler, ok := mdl.(CellStyler); ok || tv.styler == oldProvidedModelStyler {
		tv.styler = styler
	}

	tv.providedModel = mdl
	tv.model = model

	tv.itemChecker, _ = model.(ItemChecker)
	tv.imageProvider, _ = model.(ImageProvider)

	if model != nil {
		tv.attachModel()

		if dms, ok := model.(dataMembersSetter); ok {
			// FIXME: This depends on columns to be initialized before
			// calling this method.
			dataMembers := make([]string, len(tv.columns.items))

			for i, col := range tv.columns.items {
				dataMembers[i] = col.DataMemberEffective()
			}

			dms.setDataMembers(dataMembers)
		}

		if lfs, ok := model.(lessFuncsSetter); ok {
			lessFuncs := make([]func(i, j int) bool, tv.columns.Len())
			for i, c := range tv.columns.items {
				lessFuncs[i] = c.lessFunc
			}
			lfs.setLessFuncs(lessFuncs)
		}

		if sorter, ok := tv.model.(Sorter); ok {
			if tv.sortedColumnIndex >= tv.visibleColumnCount() {
				tv.sortedColumnIndex = maxi(-1, mini(0, tv.visibleColumnCount()-1))
				tv.sortOrder = SortAscending
			}

			sorter.Sort(tv.sortedColumnIndex, tv.sortOrder)
		}
	}

	tv.SetCurrentIndex(-1)

	tv.setItemCount()

	tv.itemCountChangedPublisher.Publish()

	return nil
}

// TableModel returns the TableModel of the TableView.
func (tv *TableView) TableModel() TableModel {
	return tv.model
}

// ItemChecker returns the ItemChecker of the TableView.
func (tv *TableView) ItemChecker() ItemChecker {
	return tv.itemChecker
}

// SetItemChecker sets the ItemChecker of the TableView.
func (tv *TableView) SetItemChecker(itemChecker ItemChecker) {
	tv.itemChecker = itemChecker
}

// CellStyler returns the CellStyler of the TableView.
func (tv *TableView) CellStyler() CellStyler {
	return tv.styler
}

// SetCellStyler sets the CellStyler of the TableView.
func (tv *TableView) SetCellStyler(styler CellStyler) {
	tv.styler = styler
}

func (tv *TableView) setItemCount() error {
	var count int

	if tv.model != nil {
		count = tv.model.RowCount()
	}

	if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMCOUNT, uintptr(count), win.LVSICF_NOSCROLL) {
		return newError("SendMessage(LVM_SETITEMCOUNT)")
	}
	if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMCOUNT, uintptr(count), win.LVSICF_NOSCROLL) {
		return newError("SendMessage(LVM_SETITEMCOUNT)")
	}

	return nil
}

// CheckBoxes returns if the *TableView has check boxes.
func (tv *TableView) CheckBoxes() bool {
	var hwnd win.HWND
	if tv.hasFrozenColumn {
		hwnd = tv.hwndFrozenLV
	} else {
		hwnd = tv.hwndNormalLV
	}

	return win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)&win.LVS_EX_CHECKBOXES > 0
}

// SetCheckBoxes sets if the *TableView has check boxes.
func (tv *TableView) SetCheckBoxes(checkBoxes bool) {
	var hwnd, hwndOther win.HWND
	if tv.hasFrozenColumn {
		hwnd, hwndOther = tv.hwndFrozenLV, tv.hwndNormalLV
	} else {
		hwnd, hwndOther = tv.hwndNormalLV, tv.hwndFrozenLV
	}

	exStyle := win.SendMessage(hwnd, win.LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0)
	oldStyle := exStyle
	if checkBoxes {
		exStyle |= win.LVS_EX_CHECKBOXES
	} else {
		exStyle &^= win.LVS_EX_CHECKBOXES
	}
	if exStyle != oldStyle {
		win.SendMessage(hwnd, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle)
	}

	win.SendMessage(hwndOther, win.LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle&^win.LVS_EX_CHECKBOXES)

	mask := win.SendMessage(hwnd, win.LVM_GETCALLBACKMASK, 0, 0)

	if checkBoxes {
		mask |= win.LVIS_STATEIMAGEMASK
	} else {
		mask &^= win.LVIS_STATEIMAGEMASK
	}

	if win.FALSE == win.SendMessage(hwnd, win.LVM_SETCALLBACKMASK, mask, 0) {
		newError("SendMessage(LVM_SETCALLBACKMASK)")
	}
}

func (tv *TableView) fromLVColIdx(frozen bool, index int32) int {
	var idx int32

	for i, tvc := range tv.columns.items {
		if frozen == tvc.frozen && tvc.visible {
			if idx == index {
				return i
			}

			idx++
		}
	}

	return -1
}

func (tv *TableView) toLVColIdx(index int) int32 {
	var idx int32

	for i, tvc := range tv.columns.items {
		if tvc.visible {
			if i == index {
				return idx
			}

			idx++
		}
	}

	return -1
}

func (tv *TableView) visibleFrozenColumnCount() int {
	var count int

	for _, tvc := range tv.columns.items {
		if tvc.frozen && tvc.visible {
			count++
		}
	}

	return count
}

func (tv *TableView) visibleColumnCount() int {
	var count int

	for _, tvc := range tv.columns.items {
		if tvc.visible {
			count++
		}
	}

	return count
}

func (tv *TableView) visibleColumns() []*TableViewColumn {
	var cols []*TableViewColumn

	for _, tvc := range tv.columns.items {
		if tvc.visible {
			cols = append(cols, tvc)
		}
	}

	return cols
}

/*func (tv *TableView) selectedColumnIndex() int {
	return tv.fromLVColIdx(tv.SendMessage(LVM_GETSELECTEDCOLUMN, 0, 0))
}*/

// func (tv *TableView) setSelectedColumnIndex(index int) {
// 	tv.SendMessage(win.LVM_SETSELECTEDCOLUMN, uintptr(tv.toLVColIdx(index)), 0)
// }

func (tv *TableView) setSortIcon(index int, order SortOrder) error {
	idx := int(tv.toLVColIdx(index))

	frozenCount := tv.visibleFrozenColumnCount()

	for i, col := range tv.visibleColumns() {
		item := win.HDITEM{
			Mask: win.HDI_FORMAT,
		}

		var headerHwnd win.HWND
		var offset int
		if col.frozen {
			headerHwnd = tv.hwndFrozenHdr
		} else {
			headerHwnd = tv.hwndNormalHdr
			offset = -frozenCount
		}

		iPtr := uintptr(offset + i)
		itemPtr := uintptr(unsafe.Pointer(&item))

		if win.SendMessage(headerHwnd, win.HDM_GETITEM, iPtr, itemPtr) == 0 {
			return newError("SendMessage(HDM_GETITEM)")
		}

		if i == idx {
			switch order {
			case SortAscending:
				item.Fmt &^= win.HDF_SORTDOWN
				item.Fmt |= win.HDF_SORTUP

			case SortDescending:
				item.Fmt &^= win.HDF_SORTUP
				item.Fmt |= win.HDF_SORTDOWN
			}
		} else {
			item.Fmt &^= win.HDF_SORTDOWN | win.HDF_SORTUP
		}

		if win.SendMessage(headerHwnd, win.HDM_SETITEM, iPtr, itemPtr) == 0 {
			return newError("SendMessage(HDM_SETITEM)")
		}
	}

	return nil
}

// ColumnClicked returns the event that is published after a column header was
// clicked.
func (tv *TableView) ColumnClicked() *IntEvent {
	return tv.columnClickedPublisher.Event()
}

// ItemActivated returns the event that is published after an item was
// activated.
//
// An item is activated when it is double clicked or the enter key is pressed
// when the item is selected.
func (tv *TableView) ItemActivated() *Event {
	return tv.itemActivatedPublisher.Event()
}

// CurrentIndex returns the index of the current item, or -1 if there is no
// current item.
func (tv *TableView) CurrentIndex() int {
	return tv.currentIndex
}

// SetCurrentIndex sets the index of the current item.
//
// Call this with a value of -1 to have no current item.
func (tv *TableView) SetCurrentIndex(index int) error {
	if tv.inSetCurrentIndex {
		return nil
	}
	tv.inSetCurrentIndex = true
	defer func() {
		tv.inSetCurrentIndex = false
	}()

	var lvi win.LVITEM

	lvi.StateMask = win.LVIS_FOCUSED | win.LVIS_SELECTED

	if tv.MultiSelection() {
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, ^uintptr(0), uintptr(unsafe.Pointer(&lvi))) {
			return newError("SendMessage(LVM_SETITEMSTATE)")
		}
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, ^uintptr(0), uintptr(unsafe.Pointer(&lvi))) {
			return newError("SendMessage(LVM_SETITEMSTATE)")
		}
	}

	if index > -1 {
		lvi.State = win.LVIS_FOCUSED | win.LVIS_SELECTED
	}

	if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, uintptr(index), uintptr(unsafe.Pointer(&lvi))) {
		return newError("SendMessage(LVM_SETITEMSTATE)")
	}
	if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, uintptr(index), uintptr(unsafe.Pointer(&lvi))) {
		return newError("SendMessage(LVM_SETITEMSTATE)")
	}

	if index != -1 {
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) {
			return newError("SendMessage(LVM_ENSUREVISIBLE)")
		}
		// Windows bug? Sometimes a second LVM_ENSUREVISIBLE is required.
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) {
			return newError("SendMessage(LVM_ENSUREVISIBLE)")
		}
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) {
			return newError("SendMessage(LVM_ENSUREVISIBLE)")
		}
		// Windows bug? Sometimes a second LVM_ENSUREVISIBLE is required.
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), uintptr(0)) {
			return newError("SendMessage(LVM_ENSUREVISIBLE)")
		}
	}

	tv.currentIndex = index

	if index == -1 || tv.itemStateChangedEventDelay == 0 {
		tv.currentIndexChangedPublisher.Publish()
	}

	if tv.MultiSelection() {
		tv.updateSelectedIndexes()
	}

	return nil
}

// CurrentIndexChanged is the event that is published after CurrentIndex has
// changed.
func (tv *TableView) CurrentIndexChanged() *Event {
	return tv.currentIndexChangedPublisher.Event()
}

// ItemVisible returns whether the item at position index is visible.
func (tv *TableView) ItemVisible(index int) bool {
	return 0 != win.SendMessage(tv.hwndNormalLV, win.LVM_ISITEMVISIBLE, uintptr(index), 0)
}

// EnsureItemVisible ensures the item at position index is visible, scrolling if necessary.
func (tv *TableView) EnsureItemVisible(index int) {
	win.SendMessage(tv.hwndNormalLV, win.LVM_ENSUREVISIBLE, uintptr(index), 0)
}

// SelectionHiddenWithoutFocus returns whether selection indicators are visible when the TableView does not have the keyboard input focus.
func (tv *TableView) SelectionHiddenWithoutFocus() bool {
	style := uint(win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE))
	if style == 0 {
		lastError("GetWindowLong")
		return false
	}

	return style&win.LVS_SHOWSELALWAYS == 0
}

// SetSelectionHiddenWithoutFocus sets whether selection indicators are visible when the TableView does not have the keyboard input focus.
func (tv *TableView) SetSelectionHiddenWithoutFocus(hidden bool) error {
	if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_SHOWSELALWAYS, !hidden); err != nil {
		return err
	}

	return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_SHOWSELALWAYS, !hidden)
}

// MultiSelection returns whether multiple items can be selected at once.
//
// By default only a single item can be selected at once.
func (tv *TableView) MultiSelection() bool {
	style := uint(win.GetWindowLong(tv.hwndNormalLV, win.GWL_STYLE))
	if style == 0 {
		lastError("GetWindowLong")
		return false
	}

	return style&win.LVS_SINGLESEL == 0
}

// SetMultiSelection sets whether multiple items can be selected at once.
func (tv *TableView) SetMultiSelection(multiSel bool) error {
	if err := ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_SINGLESEL, !multiSel); err != nil {
		return err
	}

	return ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_SINGLESEL, !multiSel)
}

// SelectedIndexes returns the indexes of the currently selected items.
func (tv *TableView) SelectedIndexes() []int {
	indexes := make([]int, len(tv.selectedIndexes))

	for i, j := range tv.selectedIndexes {
		indexes[i] = j
	}

	return indexes
}

// SetSelectedIndexes sets the indexes of the currently selected items.
func (tv *TableView) SetSelectedIndexes(indexes []int) error {
	tv.inSetSelectedIndexes = true
	defer func() {
		tv.inSetSelectedIndexes = false
		tv.publishSelectedIndexesChanged()
	}()

	lvi := &win.LVITEM{StateMask: win.LVIS_FOCUSED | win.LVIS_SELECTED}
	lp := uintptr(unsafe.Pointer(lvi))

	if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, ^uintptr(0), lp) {
		return newError("SendMessage(LVM_SETITEMSTATE)")
	}
	if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, ^uintptr(0), lp) {
		return newError("SendMessage(LVM_SETITEMSTATE)")
	}

	selectAll := false
	lvi.State = win.LVIS_FOCUSED | win.LVIS_SELECTED
	for _, i := range indexes {
		val := uintptr(i)
		if i == -1 {
			selectAll = true
			val = ^uintptr(0)
		}
		if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETITEMSTATE, val, lp) && i != -1 {
			return newError("SendMessage(LVM_SETITEMSTATE)")
		}
		if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_SETITEMSTATE, val, lp) && i != -1 {
			return newError("SendMessage(LVM_SETITEMSTATE)")
		}
	}

	if !selectAll {
		idxs := make([]int, len(indexes))

		for i, j := range indexes {
			idxs[i] = j
		}

		tv.selectedIndexes = idxs
	} else {
		count := int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTEDCOUNT, 0, 0))
		idxs := make([]int, count)
		for i := range idxs {
			idxs[i] = i
		}
		tv.selectedIndexes = idxs
	}

	return nil
}

func (tv *TableView) updateSelectedIndexes() {
	count := int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETSELECTEDCOUNT, 0, 0))
	indexes := make([]int, count)

	j := -1
	for i := 0; i < count; i++ {
		j = int(win.SendMessage(tv.hwndNormalLV, win.LVM_GETNEXTITEM, uintptr(j), win.LVNI_SELECTED))
		indexes[i] = j
	}

	changed := len(indexes) != len(tv.selectedIndexes)
	if !changed {
		for i := 0; i < len(indexes); i++ {
			if indexes[i] != tv.selectedIndexes[i] {
				changed = true
				break
			}
		}
	}

	if changed {
		tv.selectedIndexes = indexes
		tv.publishSelectedIndexesChanged()
	}
}

func (tv *TableView) copySelectedIndexes(hwndTo, hwndFrom win.HWND) error {
	count := int(win.SendMessage(hwndFrom, win.LVM_GETSELECTEDCOUNT, 0, 0))

	lvi := &win.LVITEM{StateMask: win.LVIS_FOCUSED | win.LVIS_SELECTED}
	lp := uintptr(unsafe.Pointer(lvi))

	if win.FALSE == win.SendMessage(hwndTo, win.LVM_SETITEMSTATE, ^uintptr(0), lp) {
		return newError("SendMessage(LVM_SETITEMSTATE)")
	}

	lvi.StateMask = win.LVIS_SELECTED
	lvi.State = win.LVIS_SELECTED

	j := -1
	for i := 0; i < count; i++ {
		j = int(win.SendMessage(hwndFrom, win.LVM_GETNEXTITEM, uintptr(j), win.LVNI_SELECTED))

		if win.FALSE == win.SendMessage(hwndTo, win.LVM_SETITEMSTATE, uintptr(j), lp) {
			return newError("SendMessage(LVM_SETITEMSTATE)")
		}
	}

	return nil
}

// ItemStateChangedEventDelay returns the delay in milliseconds, between the
// moment the state of an item in the *TableView changes and the moment the
// associated event is published.
//
// By default there is no delay.
func (tv *TableView) ItemStateChangedEventDelay() int {
	return tv.itemStateChangedEventDelay
}

// SetItemStateChangedEventDelay sets the delay in milliseconds, between the
// moment the state of an item in the *TableView changes and the moment the
// associated event is published.
//
// An example where this may be useful is a master-details scenario. If the
// master TableView is configured to delay the event, you can avoid pointless
// updates of the details TableView, if the user uses arrow keys to rapidly
// navigate the master view.
func (tv *TableView) SetItemStateChangedEventDelay(delay int) {
	tv.itemStateChangedEventDelay = delay
}

// SelectedIndexesChanged returns the event that is published when the list of
// selected item indexes changed.
func (tv *TableView) SelectedIndexesChanged() *Event {
	return tv.selectedIndexesChangedPublisher.Event()
}

func (tv *TableView) publishSelectedIndexesChanged() {
	if tv.itemStateChangedEventDelay > 0 {
		if 0 == win.SetTimer(
			tv.hWnd,
			tableViewSelectedIndexesChangedTimerId,
			uint32(tv.itemStateChangedEventDelay),
			0) {

			lastError("SetTimer")
		}
	} else {
		tv.selectedIndexesChangedPublisher.Publish()
	}
}

// LastColumnStretched returns if the last column should take up all remaining
// horizontal space of the *TableView.
func (tv *TableView) LastColumnStretched() bool {
	return tv.lastColumnStretched
}

// SetLastColumnStretched sets if the last column should take up all remaining
// horizontal space of the *TableView.
//
// The effect of setting this is persistent.
func (tv *TableView) SetLastColumnStretched(value bool) error {
	if value {
		if err := tv.StretchLastColumn(); err != nil {
			return err
		}
	}

	tv.lastColumnStretched = value

	return nil
}

// StretchLastColumn makes the last column take up all remaining horizontal
// space of the *TableView.
//
// The effect of this is not persistent.
func (tv *TableView) StretchLastColumn() error {
	colCount := tv.visibleColumnCount()
	if colCount == 0 {
		return nil
	}

	var hwnd win.HWND
	frozenColCount := tv.visibleFrozenColumnCount()
	if colCount-frozenColCount == 0 {
		hwnd = tv.hwndFrozenLV
		colCount = frozenColCount
	} else {
		hwnd = tv.hwndNormalLV
		colCount -= frozenColCount
	}

	var lp uintptr
	if tv.scrollbarOrientation&Horizontal != 0 {
		lp = win.LVSCW_AUTOSIZE_USEHEADER
	} else {
		width := tv.ClientBoundsPixels().Width

		lastIndexInLV := -1
		var lastIndexInLVWidth int

		for _, tvc := range tv.columns.items {
			var offset int
			if !tvc.Frozen() {
				offset = frozenColCount
			}

			colWidth := tv.IntFrom96DPI(tvc.Width())
			width -= colWidth

			if index := int32(offset) + tvc.indexInListView(); int(index) > lastIndexInLV {
				lastIndexInLV = int(index)
				lastIndexInLVWidth = colWidth
			}
		}

		width += lastIndexInLVWidth

		if hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.WS_VSCROLL) {
			width -= int(win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, uint32(tv.DPI())))
		}

		lp = uintptr(maxi(0, width))
	}

	if lp > 0 {
		if 0 == win.SendMessage(hwnd, win.LVM_SETCOLUMNWIDTH, uintptr(colCount-1), lp) {
			return newError("LVM_SETCOLUMNWIDTH failed")
		}

		if dpi := tv.DPI(); dpi != tv.dpiOfPrevStretchLastColumn {
			tv.dpiOfPrevStretchLastColumn = dpi

			tv.Invalidate()
		}
	}

	return nil
}

// Persistent returns if the *TableView should persist its UI state, like column
// widths. See *App.Settings for details.
func (tv *TableView) Persistent() bool {
	return tv.persistent
}

// SetPersistent sets if the *TableView should persist its UI state, like column
// widths. See *App.Settings for details.
func (tv *TableView) SetPersistent(value bool) {
	tv.persistent = value
}

// IgnoreNowhere returns if the *TableView should ignore left mouse clicks in the
// empty space. It forbids the user from unselecting the current index, or when
// multi selection is enabled, disables click drag selection.
func (tv *TableView) IgnoreNowhere() bool {
	return tv.ignoreNowhere
}

// IgnoreNowhere sets if the *TableView should ignore left mouse clicks in the
// empty space. It forbids the user from unselecting the current index, or when
// multi selection is enabled, disables click drag selection.
func (tv *TableView) SetIgnoreNowhere(value bool) {
	tv.ignoreNowhere = value
}

type tableViewState struct {
	SortColumnName     string
	SortOrder          SortOrder
	ColumnDisplayOrder []string
	Columns            []*tableViewColumnState
}

type tableViewColumnState struct {
	Name         string
	Title        string
	Width        int
	Visible      bool
	Frozen       bool
	LastSeenDate string
}

// SaveState writes the UI state of the *TableView to the settings.
func (tv *TableView) SaveState() error {
	if tv.columns.Len() == 0 {
		return nil
	}

	if tv.state == nil {
		tv.state = new(tableViewState)
	}

	tvs := tv.state

	tvs.SortColumnName = tv.columns.items[tv.sortedColumnIndex].name
	tvs.SortOrder = tv.sortOrder

	// tvs.Columns = make([]tableViewColumnState, tv.columns.Len())

	for _, tvc := range tv.columns.items {
		var tvcs *tableViewColumnState
		for _, cur := range tvs.Columns {
			if cur.Name == tvc.name {
				tvcs = cur
				break
			}
		}

		// tvcs := &tvs.Columns[i]

		if tvcs == nil {
			tvs.Columns = append(tvs.Columns, new(tableViewColumnState))
			tvcs = tvs.Columns[len(tvs.Columns)-1]
		}

		tvcs.Name = tvc.name
		tvcs.Title = tvc.titleOverride
		tvcs.Width = tvc.Width()
		tvcs.Visible = tvc.Visible()
		tvcs.Frozen = tvc.Frozen()
		tvcs.LastSeenDate = time.Now().Format("2006-01-02")
	}

	visibleCols := tv.visibleColumns()
	frozenCount := tv.visibleFrozenColumnCount()
	normalCount := len(visibleCols) - frozenCount
	indices := make([]int32, len(visibleCols))
	var lp uintptr
	if frozenCount > 0 {
		lp = uintptr(unsafe.Pointer(&indices[0]))

		if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(frozenCount), lp) {
			return newError("LVM_GETCOLUMNORDERARRAY")
		}
	}
	if normalCount > 0 {
		lp = uintptr(unsafe.Pointer(&indices[frozenCount]))

		if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_GETCOLUMNORDERARRAY, uintptr(normalCount), lp) {
			return newError("LVM_GETCOLUMNORDERARRAY")
		}
	}

	tvs.ColumnDisplayOrder = make([]string, len(visibleCols))
	for i, j := range indices {
		if i >= frozenCount {
			j += int32(frozenCount)
		}
		tvs.ColumnDisplayOrder[i] = visibleCols[j].name
	}

	state, err := json.Marshal(tvs)
	if err != nil {
		return err
	}

	return tv.WriteState(string(state))
}

// RestoreState restores the UI state of the *TableView from the settings.
func (tv *TableView) RestoreState() error {
	state, err := tv.ReadState()
	if err != nil {
		return err
	}
	if state == "" {
		return nil
	}

	tv.SetSuspended(true)
	defer tv.SetSuspended(false)

	if tv.state == nil {
		tv.state = new(tableViewState)
	}

	tvs := tv.state

	if err := json.Unmarshal(([]byte)(state), tvs); err != nil {
		return err
	}

	name2tvc := make(map[string]*TableViewColumn)

	for _, tvc := range tv.columns.items {
		name2tvc[tvc.name] = tvc
	}

	name2tvcs := make(map[string]*tableViewColumnState)

	tvcsRetained := make([]*tableViewColumnState, 0, len(tvs.Columns))
	for _, tvcs := range tvs.Columns {
		if tvcs.LastSeenDate != "" {
			if lastSeen, err := time.Parse("2006-02-01", tvcs.LastSeenDate); err != nil {
				tvcs.LastSeenDate = ""
			} else if name2tvc[tvcs.Name] == nil && lastSeen.Add(time.Hour*24*90).Before(time.Now()) {
				continue
			}
		}
		tvcsRetained = append(tvcsRetained, tvcs)

		name2tvcs[tvcs.Name] = tvcsRetained[len(tvcsRetained)-1]

		if tvc := name2tvc[tvcs.Name]; tvc != nil {
			if err := tvc.SetTitleOverride(tvcs.Title); err != nil {
				return err
			}
			if err := tvc.SetWidth(tvcs.Width); err != nil {
				return err
			}
			var visible bool
			for _, name := range tvs.ColumnDisplayOrder {
				if name == tvc.name {
					visible = true
					break
				}
			}
			if err := tvc.SetVisible(tvc.visible && (visible || tvcs.Visible)); err != nil {
				return err
			}
			if err := tvc.SetFrozen(tvcs.Frozen); err != nil {
				return err
			}
		}
	}
	tvs.Columns = tvcsRetained

	visibleCount := tv.visibleColumnCount()
	frozenCount := tv.visibleFrozenColumnCount()
	normalCount := visibleCount - frozenCount

	indices := make([]int32, visibleCount)

	knownNames := make(map[string]struct{})

	displayOrder := make([]string, 0, visibleCount)
	for _, name := range tvs.ColumnDisplayOrder {
		knownNames[name] = struct{}{}
		if tvc, ok := name2tvc[name]; ok && tvc.visible {
			displayOrder = append(displayOrder, name)
		}
	}
	for _, tvc := range tv.visibleColumns() {
		if _, ok := knownNames[tvc.name]; !ok {
			displayOrder = append(displayOrder, tvc.name)
		}
	}

	for i, tvc := range tv.visibleColumns() {
		for j, name := range displayOrder {
			if tvc.name == name && j < visibleCount {
				idx := i
				if j >= frozenCount {
					idx -= frozenCount
				}
				indices[j] = int32(idx)
				break
			}
		}
	}

	var lp uintptr
	if frozenCount > 0 {
		lp = uintptr(unsafe.Pointer(&indices[0]))

		if 0 == win.SendMessage(tv.hwndFrozenLV, win.LVM_SETCOLUMNORDERARRAY, uintptr(frozenCount), lp) {
			return newError("LVM_SETCOLUMNORDERARRAY")
		}
	}
	if normalCount > 0 {
		lp = uintptr(unsafe.Pointer(&indices[frozenCount]))

		if 0 == win.SendMessage(tv.hwndNormalLV, win.LVM_SETCOLUMNORDERARRAY, uintptr(normalCount), lp) {
			return newError("LVM_SETCOLUMNORDERARRAY")
		}
	}

	for i, c := range tvs.Columns {
		if c.Name == tvs.SortColumnName && i < visibleCount {
			tv.sortedColumnIndex = i
			tv.sortOrder = tvs.SortOrder
			break
		}
	}

	if sorter, ok := tv.model.(Sorter); ok {
		if !sorter.ColumnSortable(tv.sortedColumnIndex) {
			for i := range tvs.Columns {
				if sorter.ColumnSortable(i) {
					tv.sortedColumnIndex = i
					break
				}
			}
		}

		sorter.Sort(tv.sortedColumnIndex, tvs.SortOrder)
	}

	return nil
}

func (tv *TableView) toggleItemChecked(index int) error {
	checked := tv.itemChecker.Checked(index)

	if err := tv.itemChecker.SetChecked(index, !checked); err != nil {
		return wrapError(err)
	}

	if win.FALSE == win.SendMessage(tv.hwndFrozenLV, win.LVM_UPDATE, uintptr(index), 0) {
		return newError("SendMessage(LVM_UPDATE)")
	}
	if win.FALSE == win.SendMessage(tv.hwndNormalLV, win.LVM_UPDATE, uintptr(index), 0) {
		return newError("SendMessage(LVM_UPDATE)")
	}

	return nil
}

func (tv *TableView) applyImageListForImage(image interface{}) {
	tv.hIml, tv.usingSysIml, _ = imageListForImage(image, tv.DPI())

	tv.applyImageList()

	tv.imageUintptr2Index = make(map[uintptr]int32)
	tv.filePath2IconIndex = make(map[string]int32)
}

func (tv *TableView) applyImageList() {
	win.SendMessage(tv.hwndFrozenLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, uintptr(tv.hIml))
	win.SendMessage(tv.hwndNormalLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, uintptr(tv.hIml))
}

func (tv *TableView) disposeImageListAndCaches() {
	if tv.hIml != 0 && !tv.usingSysIml {
		win.SendMessage(tv.hwndFrozenLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, 0)
		win.SendMessage(tv.hwndNormalLV, win.LVM_SETIMAGELIST, win.LVSIL_SMALL, 0)

		win.ImageList_Destroy(tv.hIml)
	}
	tv.hIml = 0

	tv.imageUintptr2Index = nil
	tv.filePath2IconIndex = nil
}

func (tv *TableView) Focused() bool {
	focused := win.GetFocus()

	return focused == tv.hwndFrozenLV || focused == tv.hwndNormalLV
}

func (tv *TableView) maybePublishFocusChanged(hwnd win.HWND, msg uint32, wp uintptr) {
	focused := msg == win.WM_SETFOCUS

	if focused != tv.focused && wp != uintptr(tv.hwndFrozenLV) && wp != uintptr(tv.hwndNormalLV) {
		tv.focused = focused
		tv.focusedChangedPublisher.Publish()
	}
}

func tableViewFrozenLVWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr {
	tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(hwnd)).AsWindowBase()))

	ensureWindowLongBits(hwnd, win.GWL_STYLE, win.WS_HSCROLL|win.WS_VSCROLL, false)

	switch msg {
	case win.WM_SETFOCUS:
		win.SetFocus(tv.hwndNormalLV)
		tv.maybePublishFocusChanged(hwnd, msg, wp)

	case win.WM_KILLFOCUS:
		tv.maybePublishFocusChanged(hwnd, msg, wp)

	case win.WM_MOUSEWHEEL:
		tableViewNormalLVWndProc(tv.hwndNormalLV, msg, wp, lp)
	}

	return tv.lvWndProc(tv.frozenLVOrigWndProcPtr, hwnd, msg, wp, lp)
}

func tableViewNormalLVWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr {
	tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(hwnd)).AsWindowBase()))

	switch msg {
	case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN:
		win.SetFocus(tv.hwndFrozenLV)

	case win.WM_SETFOCUS:
		tv.WndProc(tv.hWnd, msg, wp, lp)
		tv.maybePublishFocusChanged(hwnd, msg, wp)

	case win.WM_KILLFOCUS:
		win.SendMessage(tv.hwndFrozenLV, msg, wp, lp)
		tv.WndProc(tv.hWnd, msg, wp, lp)
		tv.maybePublishFocusChanged(hwnd, msg, wp)
	}

	result := tv.lvWndProc(tv.normalLVOrigWndProcPtr, hwnd, msg, wp, lp)

	var off uint32 = win.WS_HSCROLL | win.WS_VSCROLL
	if tv.scrollbarOrientation&Horizontal != 0 {
		off &^= win.WS_HSCROLL
	}
	if tv.scrollbarOrientation&Vertical != 0 {
		off &^= win.WS_VSCROLL
	}
	if off != 0 {
		ensureWindowLongBits(hwnd, win.GWL_STYLE, off, false)
	}

	return result
}

func (tv *TableView) lvWndProc(origWndProcPtr uintptr, hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr {
	var hwndOther win.HWND
	if hwnd == tv.hwndFrozenLV {
		hwndOther = tv.hwndNormalLV
	} else {
		hwndOther = tv.hwndFrozenLV
	}

	var maybeStretchLastColumn bool

	switch msg {
	case win.WM_ERASEBKGND:
		maybeStretchLastColumn = true

	case win.WM_WINDOWPOSCHANGED:
		wp := (*win.WINDOWPOS)(unsafe.Pointer(lp))

		if wp.Flags&win.SWP_NOSIZE != 0 {
			break
		}

		maybeStretchLastColumn = int(wp.Cx) < tv.WidthPixels()

	case win.WM_GETDLGCODE:
		if wp == win.VK_RETURN {
			return win.DLGC_WANTALLKEYS
		}

	case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN, win.WM_LBUTTONDBLCLK, win.WM_RBUTTONDBLCLK:
		var hti win.LVHITTESTINFO
		hti.Pt = win.POINT{win.GET_X_LPARAM(lp), win.GET_Y_LPARAM(lp)}
		win.SendMessage(hwnd, win.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))

		tv.itemIndexOfLastMouseButtonDown = int(hti.IItem)

		if hti.Flags == win.LVHT_NOWHERE {
			if tv.MultiSelection() {
				tv.publishNextSelClear = true
			} else {
				if tv.CheckBoxes() {
					if tv.currentIndex > -1 {
						tv.SetCurrentIndex(-1)
					}
				} else {
					// We keep the current item, if in single item selection mode without check boxes.
					win.SetFocus(tv.hwndFrozenLV)
					return 0
				}
			}

			if tv.IgnoreNowhere() {
				return 0
			}
		}

		switch msg {
		case win.WM_LBUTTONDOWN, win.WM_RBUTTONDOWN:
			if hti.Flags == win.LVHT_ONITEMSTATEICON &&
				tv.itemChecker != nil &&
				tv.CheckBoxes() {

				tv.toggleItemChecked(int(hti.IItem))
			}

		case win.WM_LBUTTONDBLCLK, win.WM_RBUTTONDBLCLK:
			if tv.currentIndex != tv.prevIndex && tv.itemStateChangedEventDelay > 0 {
				tv.prevIndex = tv.currentIndex
				tv.currentIndexChangedPublisher.Publish()
			}
		}

	case win.WM_LBUTTONUP, win.WM_RBUTTONUP:
		tv.itemIndexOfLastMouseButtonDown = -1

	case win.WM_MOUSEMOVE, win.WM_MOUSELEAVE:
		if tv.inMouseEvent {
			break
		}
		tv.inMouseEvent = true
		defer func() {
			tv.inMouseEvent = false
		}()

		if msg == win.WM_MOUSEMOVE {
			y := int(win.GET_Y_LPARAM(lp))
			lp = uintptr(win.MAKELONG(0, uint16(y)))
		}

		win.SendMessage(hwndOther, msg, wp, lp)

	case win.WM_KEYDOWN:
		if wp == win.VK_SPACE &&
			tv.currentIndex > -1 &&
			tv.itemChecker != nil &&
			tv.CheckBoxes() {

			tv.toggleItemChecked(tv.currentIndex)
		}

		tv.handleKeyDown(wp, lp)

	case win.WM_KEYUP:
		tv.handleKeyUp(wp, lp)

	case win.WM_NOTIFY:
		nmh := ((*win.NMHDR)(unsafe.Pointer(lp)))
		switch nmh.HwndFrom {
		case tv.hwndFrozenHdr, tv.hwndNormalHdr:
			if nmh.Code == win.NM_CUSTOMDRAW {
				return tableViewHdrWndProc(nmh.HwndFrom, msg, wp, lp)
			}
		}

		switch nmh.Code {
		case win.LVN_GETDISPINFO:
			di := (*win.NMLVDISPINFO)(unsafe.Pointer(lp))

			row := int(di.Item.IItem)
			col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, di.Item.ISubItem)
			if col == -1 {
				break
			}

			if di.Item.Mask&win.LVIF_TEXT > 0 {
				value := tv.model.Value(row, col)
				var text string
				if format := tv.columns.items[col].formatFunc; format != nil {
					text = format(value)
				} else {
					switch val := value.(type) {
					case string:
						text = val

					case float32:
						prec := tv.columns.items[col].precision
						if prec == 0 {
							prec = 2
						}
						text = FormatFloatGrouped(float64(val), prec)

					case float64:
						prec := tv.columns.items[col].precision
						if prec == 0 {
							prec = 2
						}
						text = FormatFloatGrouped(val, prec)

					case time.Time:
						if val.Year() > 1601 {
							text = val.Format(tv.columns.items[col].format)
						}

					case bool:
						if val {
							text = checkmark
						}

					case *big.Rat:
						prec := tv.columns.items[col].precision
						if prec == 0 {
							prec = 2
						}
						text = formatBigRatGrouped(val, prec)

					default:
						text = fmt.Sprintf(tv.columns.items[col].format, val)
					}
				}

				utf16 := syscall.StringToUTF16(text)
				buf := (*[264]uint16)(unsafe.Pointer(di.Item.PszText))
				max := mini(len(utf16), int(di.Item.CchTextMax))
				copy((*buf)[:], utf16[:max])
				(*buf)[max-1] = 0
			}

			if (tv.imageProvider != nil || tv.styler != nil) && di.Item.Mask&win.LVIF_IMAGE > 0 {
				var image interface{}
				if di.Item.ISubItem == 0 {
					if ip := tv.imageProvider; ip != nil && image == nil {
						image = ip.Image(row)
					}
				}
				if styler := tv.styler; styler != nil && image == nil {
					tv.style.row = row
					tv.style.col = col
					tv.style.bounds = Rectangle{}
					tv.style.dpi = tv.DPI()
					tv.style.Image = nil

					styler.StyleCell(&tv.style)

					image = tv.style.Image
				}

				if image != nil {
					if tv.hIml == 0 {
						tv.applyImageListForImage(image)
					}

					di.Item.IImage = imageIndexMaybeAdd(
						image,
						tv.hIml,
						tv.usingSysIml,
						tv.imageUintptr2Index,
						tv.filePath2IconIndex,
						tv.DPI())
				}
			}

			if di.Item.ISubItem == 0 && di.Item.StateMask&win.LVIS_STATEIMAGEMASK > 0 &&
				tv.itemChecker != nil {
				checked := tv.itemChecker.Checked(row)

				if checked {
					di.Item.State = 0x2000
				} else {
					di.Item.State = 0x1000
				}
			}

		case win.NM_CUSTOMDRAW:
			nmlvcd := (*win.NMLVCUSTOMDRAW)(unsafe.Pointer(lp))

			if nmlvcd.IIconPhase == 0 {
				row := int(nmlvcd.Nmcd.DwItemSpec)
				col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, nmlvcd.ISubItem)
				if col == -1 {
					break
				}

				applyCellStyle := func() int {
					if tv.styler != nil {
						dpi := tv.DPI()

						tv.style.row = row
						tv.style.col = col
						tv.style.bounds = rectangleFromRECT(nmlvcd.Nmcd.Rc)
						tv.style.dpi = dpi
						tv.style.hdc = nmlvcd.Nmcd.Hdc
						tv.style.BackgroundColor = tv.itemBGColor
						tv.style.TextColor = tv.itemTextColor
						tv.style.Font = nil
						tv.style.Image = nil

						tv.styler.StyleCell(&tv.style)

						defer func() {
							tv.style.bounds = Rectangle{}
							if tv.style.canvas != nil {
								tv.style.canvas.Dispose()
								tv.style.canvas = nil
							}
							tv.style.hdc = 0
						}()

						if tv.style.canvas != nil {
							return win.CDRF_SKIPDEFAULT
						}

						nmlvcd.ClrTextBk = win.COLORREF(tv.style.BackgroundColor)
						nmlvcd.ClrText = win.COLORREF(tv.style.TextColor)

						if font := tv.style.Font; font != nil {
							win.SelectObject(nmlvcd.Nmcd.Hdc, win.HGDIOBJ(font.handleForDPI(dpi)))
						}
					}

					return 0
				}

				switch nmlvcd.Nmcd.DwDrawStage {
				case win.CDDS_PREPAINT:
					return win.CDRF_NOTIFYITEMDRAW

				case win.CDDS_ITEMPREPAINT:
					var selected bool
					if itemState := win.SendMessage(hwnd, win.LVM_GETITEMSTATE, nmlvcd.Nmcd.DwItemSpec, win.LVIS_SELECTED); itemState&win.LVIS_SELECTED != 0 {
						selected = true

						tv.itemBGColor = tv.themeSelectedBGColor
						tv.itemTextColor = tv.themeSelectedTextColor
					} else {
						tv.itemBGColor = tv.themeNormalBGColor
						tv.itemTextColor = tv.themeNormalTextColor
					}

					if !selected && tv.alternatingRowBG && row%2 == 1 {
						tv.itemBGColor = tv.alternatingRowBGColor
						tv.itemTextColor = tv.alternatingRowTextColor
					}

					tv.style.BackgroundColor = tv.itemBGColor
					tv.style.TextColor = tv.itemTextColor

					if tv.styler != nil {
						tv.style.row = row
						tv.style.col = -1
						tv.style.bounds = rectangleFromRECT(nmlvcd.Nmcd.Rc)
						tv.style.dpi = tv.DPI()
						tv.style.hdc = 0
						tv.style.Font = nil
						tv.style.Image = nil

						tv.styler.StyleCell(&tv.style)

						tv.itemFont = tv.style.Font
					}

					if selected {
						tv.style.BackgroundColor = tv.itemBGColor
						tv.style.TextColor = tv.itemTextColor
					} else {
						tv.itemBGColor = tv.style.BackgroundColor
						tv.itemTextColor = tv.style.TextColor
					}

					if tv.style.BackgroundColor != tv.themeNormalBGColor {
						var color Color
						if selected && !tv.Focused() {
							color = tv.themeSelectedNotFocusedBGColor
						} else {
							color = tv.style.BackgroundColor
						}

						if brush, _ := NewSolidColorBrush(color); brush != nil {
							defer brush.Dispose()

							canvas, _ := newCanvasFromHDC(nmlvcd.Nmcd.Hdc)
							canvas.FillRectanglePixels(brush, rectangleFromRECT(nmlvcd.Nmcd.Rc))
						}
					}

					nmlvcd.ClrText = win.COLORREF(tv.style.TextColor)
					nmlvcd.ClrTextBk = win.COLORREF(tv.style.BackgroundColor)

					return win.CDRF_NOTIFYSUBITEMDRAW

				case win.CDDS_ITEMPREPAINT | win.CDDS_SUBITEM:
					if tv.itemFont != nil {
						win.SelectObject(nmlvcd.Nmcd.Hdc, win.HGDIOBJ(tv.itemFont.handleForDPI(tv.DPI())))
					}

					if applyCellStyle() == win.CDRF_SKIPDEFAULT && win.IsAppThemed() {
						return win.CDRF_SKIPDEFAULT
					}

					return win.CDRF_NEWFONT | win.CDRF_SKIPPOSTPAINT | win.CDRF_NOTIFYPOSTPAINT

				case win.CDDS_ITEMPOSTPAINT | win.CDDS_SUBITEM:
					if applyCellStyle() == win.CDRF_SKIPDEFAULT {
						return win.CDRF_SKIPDEFAULT
					}

					return win.CDRF_NEWFONT | win.CDRF_SKIPPOSTPAINT
				}

				return win.CDRF_SKIPPOSTPAINT
			}

			return win.CDRF_SKIPPOSTPAINT

		case win.LVN_BEGINSCROLL:
			if tv.scrolling {
				break
			}
			tv.scrolling = true
			defer func() {
				tv.scrolling = false
			}()

			var rc win.RECT
			win.SendMessage(hwnd, win.LVM_GETITEMRECT, 0, uintptr(unsafe.Pointer(&rc)))

			nmlvs := (*win.NMLVSCROLL)(unsafe.Pointer(lp))
			win.SendMessage(hwndOther, win.LVM_SCROLL, 0, uintptr(nmlvs.Dy*(rc.Bottom-rc.Top)))

		case win.LVN_COLUMNCLICK:
			nmlv := (*win.NMLISTVIEW)(unsafe.Pointer(lp))

			col := tv.fromLVColIdx(hwnd == tv.hwndFrozenLV, nmlv.ISubItem)

			if sorter, ok := tv.model.(Sorter); ok && sorter.ColumnSortable(col) {
				prevCol := sorter.SortedColumn()
				var order SortOrder
				if col != prevCol || sorter.SortOrder() == SortDescending {
					order = SortAscending
				} else {
					order = SortDescending
				}
				tv.sortedColumnIndex = col
				tv.sortOrder = order
				sorter.Sort(col, order)
			}

			tv.columnClickedPublisher.Publish(col)

		case win.LVN_ITEMCHANGED:
			nmlv := (*win.NMLISTVIEW)(unsafe.Pointer(lp))

			if tv.hwndItemChanged != 0 && tv.hwndItemChanged != hwnd {
				break
			}
			tv.hwndItemChanged = hwnd
			defer func() {
				tv.hwndItemChanged = 0
			}()

			tv.copySelectedIndexes(hwndOther, hwnd)

			if nmlv.IItem == -1 && !tv.publishNextSelClear {
				break
			}
			tv.publishNextSelClear = false

			selectedNow := nmlv.UNewState&win.LVIS_SELECTED > 0
			selectedBefore := nmlv.UOldState&win.LVIS_SELECTED > 0
			if tv.itemIndexOfLastMouseButtonDown != -1 && selectedNow && !selectedBefore && ModifiersDown()&(ModControl|ModShift) == 0 {
				tv.prevIndex = tv.currentIndex
				tv.currentIndex = int(nmlv.IItem)
				if tv.itemStateChangedEventDelay > 0 {
					tv.delayedCurrentIndexChangedCanceled = false
					if 0 == win.SetTimer(
						tv.hWnd,
						tableViewCurrentIndexChangedTimerId,
						uint32(tv.itemStateChangedEventDelay),
						0) {

						lastError("SetTimer")
					}

					tv.SetCurrentIndex(int(nmlv.IItem))
				} else {
					tv.SetCurrentIndex(int(nmlv.IItem))
				}
			}

			if selectedNow != selectedBefore {
				if !tv.inSetSelectedIndexes && tv.MultiSelection() {
					tv.updateSelectedIndexes()
				}
			}

		case win.LVN_ODSTATECHANGED:
			if tv.hwndItemChanged != 0 && tv.hwndItemChanged != hwnd {
				break
			}
			tv.hwndItemChanged = hwnd
			defer func() {
				tv.hwndItemChanged = 0
			}()

			tv.copySelectedIndexes(hwndOther, hwnd)

			tv.updateSelectedIndexes()

		case win.LVN_ITEMACTIVATE:
			nmia := (*win.NMITEMACTIVATE)(unsafe.Pointer(lp))

			if tv.itemStateChangedEventDelay > 0 {
				tv.delayedCurrentIndexChangedCanceled = true
			}

			if int(nmia.IItem) != tv.currentIndex {
				tv.SetCurrentIndex(int(nmia.IItem))
				tv.currentIndexChangedPublisher.Publish()
			}

			tv.itemActivatedPublisher.Publish()

		case win.HDN_ITEMCHANGING:
			tv.updateLVSizes()
		}

	case win.WM_UPDATEUISTATE:
		switch win.LOWORD(uint32(wp)) {
		case win.UIS_SET:
			wp |= win.UISF_HIDEFOCUS << 16

		case win.UIS_CLEAR, win.UIS_INITIALIZE:
			wp &^= ^uintptr(win.UISF_HIDEFOCUS << 16)
		}
	}

	lpFixed := lp
	fixXInLP := func() {
		// fmt.Printf("hwnd == tv.hwndNormalLV: %t, tv.hasFrozenColumn: %t\n", hwnd == tv.hwndNormalLV, tv.hasFrozenColumn)
		if hwnd == tv.hwndNormalLV && tv.hasFrozenColumn {
			var rc win.RECT
			if win.GetWindowRect(tv.hwndFrozenLV, &rc) {
				x := int(win.GET_X_LPARAM(lp)) + int(rc.Right-rc.Left)
				y := int(win.GET_Y_LPARAM(lp))

				lpFixed = uintptr(win.MAKELONG(uint16(x), uint16(y)))
			}
		}
	}

	switch msg {
	case win.WM_LBUTTONDOWN, win.WM_MBUTTONDOWN, win.WM_RBUTTONDOWN:
		fixXInLP()
		tv.publishMouseEvent(&tv.mouseDownPublisher, msg, wp, lpFixed)

	case win.WM_LBUTTONUP, win.WM_MBUTTONUP, win.WM_RBUTTONUP:
		fixXInLP()
		tv.publishMouseEvent(&tv.mouseUpPublisher, msg, wp, lpFixed)

	case win.WM_MOUSEMOVE:
		fixXInLP()
		tv.publishMouseEvent(&tv.mouseMovePublisher, msg, wp, lpFixed)

	case win.WM_MOUSEWHEEL:
		fixXInLP()
		tv.publishMouseWheelEvent(&tv.mouseWheelPublisher, wp, lpFixed)
	}

	if maybeStretchLastColumn {
		if tv.lastColumnStretched && !tv.busyStretchingLastColumn {
			if normalVisColCount := tv.visibleColumnCount() - tv.visibleFrozenColumnCount(); normalVisColCount == 0 || normalVisColCount > 0 == (hwnd == tv.hwndNormalLV) {
				tv.busyStretchingLastColumn = true
				defer func() {
					tv.busyStretchingLastColumn = false
				}()
				tv.StretchLastColumn()
			}
		}

		if msg == win.WM_ERASEBKGND {
			return 1
		}
	}

	return win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp)
}

func tableViewHdrWndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr {
	tv := (*TableView)(unsafe.Pointer(windowFromHandle(win.GetParent(win.GetParent(hwnd))).AsWindowBase()))

	var origWndProcPtr uintptr
	if hwnd == tv.hwndFrozenHdr {
		origWndProcPtr = tv.frozenHdrOrigWndProcPtr
	} else {
		origWndProcPtr = tv.normalHdrOrigWndProcPtr
	}

	switch msg {
	case win.WM_NOTIFY:
		switch ((*win.NMHDR)(unsafe.Pointer(lp))).Code {
		case win.NM_CUSTOMDRAW:
			if tv.customHeaderHeight == 0 {
				break
			}

			nmcd := (*win.NMCUSTOMDRAW)(unsafe.Pointer(lp))

			switch nmcd.DwDrawStage {
			case win.CDDS_PREPAINT:
				return win.CDRF_NOTIFYITEMDRAW

			case win.CDDS_ITEMPREPAINT:
				return win.CDRF_NOTIFYPOSTPAINT

			case win.CDDS_ITEMPOSTPAINT:
				col := tv.fromLVColIdx(hwnd == tv.hwndFrozenHdr, int32(nmcd.DwItemSpec))
				if tv.styler != nil && col > -1 {
					tv.style.row = -1
					tv.style.col = col
					tv.style.bounds = rectangleFromRECT(nmcd.Rc)
					tv.style.dpi = tv.DPI()
					tv.style.hdc = nmcd.Hdc
					tv.style.TextColor = tv.themeNormalTextColor
					tv.style.Font = nil

					tv.styler.StyleCell(&tv.style)

					defer func() {
						tv.style.bounds = Rectangle{}
						if tv.style.canvas != nil {
							tv.style.canvas.Dispose()
							tv.style.canvas = nil
						}
						tv.style.hdc = 0
					}()
				}

				return win.CDRF_DODEFAULT
			}

			return win.CDRF_DODEFAULT
		}

	case win.HDM_LAYOUT:
		if tv.customHeaderHeight == 0 {
			break
		}

		result := win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp)

		hdl := (*win.HDLAYOUT)(unsafe.Pointer(lp))
		hdl.Prc.Top = int32(tv.customHeaderHeight)
		hdl.Pwpos.Cy = int32(tv.customHeaderHeight)

		return result

	case win.WM_MOUSEMOVE, win.WM_LBUTTONDOWN, win.WM_LBUTTONUP, win.WM_MBUTTONDOWN, win.WM_MBUTTONUP, win.WM_RBUTTONDOWN, win.WM_RBUTTONUP:
		hti := win.HDHITTESTINFO{Pt: win.POINT{int32(win.GET_X_LPARAM(lp)), int32(win.GET_Y_LPARAM(lp))}}
		win.SendMessage(hwnd, win.HDM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))
		if hti.IItem == -1 {
			tv.group.toolTip.setText(hwnd, "")
			break
		}

		col := tv.fromLVColIdx(hwnd == tv.hwndFrozenHdr, hti.IItem)
		text := tv.columns.At(col).TitleEffective()

		var rc win.RECT
		if 0 == win.SendMessage(hwnd, win.HDM_GETITEMRECT, uintptr(hti.IItem), uintptr(unsafe.Pointer(&rc))) {
			tv.group.toolTip.setText(hwnd, "")
			break
		}

		size := calculateTextSize(text, tv.Font(), tv.DPI(), 0, hwnd)
		if size.Width <= rectangleFromRECT(rc).Width-int(win.SendMessage(hwnd, win.HDM_GETBITMAPMARGIN, 0, 0)) {
			tv.group.toolTip.setText(hwnd, "")
			break
		}

		if tv.group.toolTip.text(hwnd) == text {
			break
		}

		tv.group.toolTip.setText(hwnd, text)

		m := win.MSG{
			HWnd:    hwnd,
			Message: msg,
			WParam:  wp,
			LParam:  lp,
			Pt:      hti.Pt,
		}

		tv.group.toolTip.SendMessage(win.TTM_RELAYEVENT, 0, uintptr(unsafe.Pointer(&m)))
	}

	return win.CallWindowProc(origWndProcPtr, hwnd, msg, wp, lp)
}

func (tv *TableView) WndProc(hwnd win.HWND, msg uint32, wp, lp uintptr) uintptr {
	switch msg {
	case win.WM_NOTIFY:
		nmh := (*win.NMHDR)(unsafe.Pointer(lp))
		switch nmh.HwndFrom {
		case tv.hwndFrozenLV:
			return tableViewFrozenLVWndProc(nmh.HwndFrom, msg, wp, lp)

		case tv.hwndNormalLV:
			return tableViewNormalLVWndProc(nmh.HwndFrom, msg, wp, lp)
		}

	case win.WM_WINDOWPOSCHANGED:
		wp := (*win.WINDOWPOS)(unsafe.Pointer(lp))

		if wp.Flags&win.SWP_NOSIZE != 0 {
			break
		}

		if tv.formActivatingHandle == -1 {
			if form := tv.Form(); form != nil {
				tv.formActivatingHandle = form.Activating().Attach(func() {
					if tv.hwndNormalLV == win.GetFocus() {
						win.SetFocus(tv.hwndFrozenLV)
					}
				})
			}
		}

		tv.updateLVSizes()

		// FIXME: The InvalidateRect and redrawItems calls below prevent
		// painting glitches on resize. Though this seems to work reasonably
		// well, in the long run we would like to find the root cause of this
		// issue and come up with a better fix.
		dpi := uint32(tv.DPI())
		var rc win.RECT

		vsbWidth := win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, dpi)
		rc = win.RECT{wp.Cx - vsbWidth - 1, 0, wp.Cx, wp.Cy}
		win.InvalidateRect(tv.hWnd, &rc, true)

		hsbHeight := win.GetSystemMetricsForDpi(win.SM_CYHSCROLL, dpi)
		rc = win.RECT{0, wp.Cy - hsbHeight - 1, wp.Cx, wp.Cy}
		win.InvalidateRect(tv.hWnd, &rc, true)

		tv.redrawItems()

	case win.WM_TIMER:
		if !win.KillTimer(tv.hWnd, wp) {
			lastError("KillTimer")
		}

		switch wp {
		case tableViewCurrentIndexChangedTimerId:
			if !tv.delayedCurrentIndexChangedCanceled {
				tv.currentIndexChangedPublisher.Publish()
			}

		case tableViewSelectedIndexesChangedTimerId:
			tv.selectedIndexesChangedPublisher.Publish()
		}

	case win.WM_MEASUREITEM:
		mis := (*win.MEASUREITEMSTRUCT)(unsafe.Pointer(lp))
		mis.ItemHeight = uint32(tv.customRowHeight)

		ensureWindowLongBits(tv.hwndFrozenLV, win.GWL_STYLE, win.LVS_OWNERDRAWFIXED, false)
		ensureWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.LVS_OWNERDRAWFIXED, false)

	case win.WM_SETFOCUS:
		win.SetFocus(tv.hwndNormalLV)

	case win.WM_DESTROY:
		// As we subclass all windows of system classes, we prevented the
		// clean-up code in the WM_NCDESTROY handlers of some windows from
		// being called. To fix this, we restore the original window
		// procedures here.
		if tv.frozenHdrOrigWndProcPtr != 0 {
			win.SetWindowLongPtr(tv.hwndFrozenHdr, win.GWLP_WNDPROC, tv.frozenHdrOrigWndProcPtr)
		}
		if tv.frozenLVOrigWndProcPtr != 0 {
			win.SetWindowLongPtr(tv.hwndFrozenLV, win.GWLP_WNDPROC, tv.frozenLVOrigWndProcPtr)
		}
		if tv.normalHdrOrigWndProcPtr != 0 {
			win.SetWindowLongPtr(tv.hwndNormalHdr, win.GWLP_WNDPROC, tv.normalHdrOrigWndProcPtr)
		}
		if tv.normalLVOrigWndProcPtr != 0 {
			win.SetWindowLongPtr(tv.hwndNormalLV, win.GWLP_WNDPROC, tv.normalLVOrigWndProcPtr)
		}
	}

	return tv.WidgetBase.WndProc(hwnd, msg, wp, lp)
}

func (tv *TableView) updateLVSizes() {
	tv.updateLVSizesWithSpecialCare(false)
}

func (tv *TableView) updateLVSizesWithSpecialCare(needSpecialCare bool) {
	var width int
	for i := tv.columns.Len() - 1; i >= 0; i-- {
		if col := tv.columns.At(i); col.frozen {
			width += col.Width()
		}
	}

	dpi := tv.DPI()
	widthPixels := IntFrom96DPI(width, dpi)

	cb := tv.ClientBoundsPixels()

	win.MoveWindow(tv.hwndNormalLV, int32(widthPixels), 0, int32(cb.Width-widthPixels), int32(cb.Height), true)

	var sbh int
	if hasWindowLongBits(tv.hwndNormalLV, win.GWL_STYLE, win.WS_HSCROLL) {
		sbh = int(win.GetSystemMetricsForDpi(win.SM_CYHSCROLL, uint32(dpi)))
	}

	win.MoveWindow(tv.hwndFrozenLV, 0, 0, int32(widthPixels), int32(cb.Height-sbh), true)

	if needSpecialCare {
		tv.updateLVSizesNeedsSpecialCare = true
	}

	if tv.updateLVSizesNeedsSpecialCare {
		win.ShowWindow(tv.hwndNormalLV, win.SW_HIDE)
		win.ShowWindow(tv.hwndNormalLV, win.SW_SHOW)
	}

	if !needSpecialCare {
		tv.updateLVSizesNeedsSpecialCare = false
	}
}

func (*TableView) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
	return NewGreedyLayoutItem()
}

func (tv *TableView) SetScrollbarOrientation(orientation Orientation) {
	tv.scrollbarOrientation = orientation
}

func (tv *TableView) ScrollbarOrientation() Orientation {
	return tv.scrollbarOrientation
}
