// Copyright 2010 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 (
	"syscall"
	"unsafe"

	"github.com/lxn/win"
)

type treeViewItemInfo struct {
	handle       win.HTREEITEM
	child2Handle map[TreeItem]win.HTREEITEM
}

type TreeView struct {
	WidgetBase
	model                          TreeModel
	lazyPopulation                 bool
	itemsResetEventHandlerHandle   int
	itemChangedEventHandlerHandle  int
	itemInsertedEventHandlerHandle int
	itemRemovedEventHandlerHandle  int
	item2Info                      map[TreeItem]*treeViewItemInfo
	handle2Item                    map[win.HTREEITEM]TreeItem
	currItem                       TreeItem
	hIml                           win.HIMAGELIST
	usingSysIml                    bool
	imageUintptr2Index             map[uintptr]int32
	filePath2IconIndex             map[string]int32
	expandedChangedPublisher       TreeItemEventPublisher
	currentItemChangedPublisher    EventPublisher
	itemActivatedPublisher         EventPublisher
}

func NewTreeView(parent Container) (*TreeView, error) {
	tv := new(TreeView)

	if err := InitWidget(
		tv,
		parent,
		"SysTreeView32",
		win.WS_TABSTOP|win.WS_VISIBLE|win.TVS_HASBUTTONS|win.TVS_LINESATROOT|win.TVS_SHOWSELALWAYS|win.TVS_TRACKSELECT,
		win.WS_EX_CLIENTEDGE); err != nil {
		return nil, err
	}

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

	if hr := win.HRESULT(tv.SendMessage(win.TVM_SETEXTENDEDSTYLE, win.TVS_EX_DOUBLEBUFFER, win.TVS_EX_DOUBLEBUFFER)); win.FAILED(hr) {
		return nil, errorFromHRESULT("TVM_SETEXTENDEDSTYLE", hr)
	}

	if err := tv.setTheme("Explorer"); err != nil {
		return nil, err
	}

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

	tv.MustRegisterProperty("CurrentItem", NewReadOnlyProperty(
		func() interface{} {
			return tv.CurrentItem()
		},
		tv.CurrentItemChanged()))

	tv.MustRegisterProperty("CurrentItemLevel", NewReadOnlyProperty(
		func() interface{} {
			level := -1
			item := tv.CurrentItem()

			for item != nil {
				level++
				item = item.Parent()
			}

			return level
		},
		tv.CurrentItemChanged()))

	tv.MustRegisterProperty("HasCurrentItem", NewReadOnlyBoolProperty(
		func() bool {
			return tv.CurrentItem() != nil
		},
		tv.CurrentItemChanged()))

	succeeded = true

	return tv, nil
}

func (tv *TreeView) Dispose() {
	tv.WidgetBase.Dispose()

	tv.disposeImageListAndCaches()
}

func (tv *TreeView) SetBackground(bg Brush) {
	tv.WidgetBase.SetBackground(bg)

	color := Color(win.GetSysColor(win.COLOR_WINDOW))

	if bg != nil {
		type Colorer interface {
			Color() Color
		}

		if c, ok := bg.(Colorer); ok {
			color = c.Color()
		}
	}

	tv.SendMessage(win.TVM_SETBKCOLOR, 0, uintptr(color))
}

func (tv *TreeView) Model() TreeModel {
	return tv.model
}

func (tv *TreeView) SetModel(model TreeModel) error {
	if tv.model != nil {
		tv.model.ItemsReset().Detach(tv.itemsResetEventHandlerHandle)
		tv.model.ItemChanged().Detach(tv.itemChangedEventHandlerHandle)
		tv.model.ItemInserted().Detach(tv.itemInsertedEventHandlerHandle)
		tv.model.ItemRemoved().Detach(tv.itemRemovedEventHandlerHandle)

		tv.disposeImageListAndCaches()
	}

	tv.model = model

	if model != nil {
		tv.lazyPopulation = model.LazyPopulation()

		tv.itemsResetEventHandlerHandle = model.ItemsReset().Attach(func(parent TreeItem) {
			if parent == nil {
				tv.resetItems()
			} else if tv.item2Info[parent] != nil {
				tv.SetSuspended(true)
				defer tv.SetSuspended(false)

				if err := tv.removeDescendants(parent); err != nil {
					return
				}

				if err := tv.insertChildren(parent); err != nil {
					return
				}
			}
		})

		tv.itemChangedEventHandlerHandle = model.ItemChanged().Attach(func(item TreeItem) {
			if item == nil || tv.item2Info[item] == nil {
				return
			}

			if err := tv.updateItem(item); err != nil {
				return
			}
		})

		tv.itemInsertedEventHandlerHandle = model.ItemInserted().Attach(func(item TreeItem) {
			tv.SetSuspended(true)
			defer tv.SetSuspended(false)

			var hInsertAfter win.HTREEITEM
			parent := item.Parent()
			for i := parent.ChildCount() - 1; i >= 0; i-- {
				if parent.ChildAt(i) == item {
					if i > 0 {
						hInsertAfter = tv.item2Info[parent.ChildAt(i-1)].handle
					} else {
						hInsertAfter = win.TVI_FIRST
					}
				}
			}

			if _, err := tv.insertItemAfter(item, hInsertAfter); err != nil {
				return
			}
		})

		tv.itemRemovedEventHandlerHandle = model.ItemRemoved().Attach(func(item TreeItem) {
			if err := tv.removeItem(item); err != nil {
				return
			}
		})
	}

	return tv.resetItems()
}

func (tv *TreeView) CurrentItem() TreeItem {
	return tv.currItem
}

func (tv *TreeView) SetCurrentItem(item TreeItem) error {
	if item == tv.currItem {
		return nil
	}

	if item != nil {
		if err := tv.ensureItemAndAncestorsInserted(item); err != nil {
			return err
		}
	}

	handle, err := tv.handleForItem(item)
	if err != nil {
		return err
	}

	if 0 == tv.SendMessage(win.TVM_SELECTITEM, win.TVGN_CARET, uintptr(handle)) {
		return newError("SendMessage(TVM_SELECTITEM) failed")
	}

	tv.currItem = item

	return nil
}

func (tv *TreeView) EnsureVisible(item TreeItem) error {
	handle, err := tv.handleForItem(item)
	if err != nil {
		return err
	}

	tv.SendMessage(win.TVM_ENSUREVISIBLE, 0, uintptr(handle))

	return nil
}

func (tv *TreeView) handleForItem(item TreeItem) (win.HTREEITEM, error) {
	if item != nil {
		if info := tv.item2Info[item]; info == nil {
			return 0, newError("invalid item")
		} else {
			return info.handle, nil
		}
	}

	return 0, newError("invalid item")
}

// ItemAt determines the location of the specified point in native pixels relative to the client area of a tree-view control.
func (tv *TreeView) ItemAt(x, y int) TreeItem {
	hti := win.TVHITTESTINFO{Pt: Point{x, y}.toPOINT()}

	tv.SendMessage(win.TVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))

	if item, ok := tv.handle2Item[hti.HItem]; ok {
		return item
	}

	return nil
}

// ItemHeight returns the height of each item in native pixels.
func (tv *TreeView) ItemHeight() int {
	return int(tv.SendMessage(win.TVM_GETITEMHEIGHT, 0, 0))
}

// SetItemHeight sets the height of the tree-view items in native pixels.
func (tv *TreeView) SetItemHeight(height int) {
	tv.SendMessage(win.TVM_SETITEMHEIGHT, uintptr(height), 0)
}

func (tv *TreeView) resetItems() error {
	tv.SetSuspended(true)
	defer tv.SetSuspended(false)

	if err := tv.clearItems(); err != nil {
		return err
	}

	if tv.model == nil {
		return nil
	}

	if err := tv.insertRoots(); err != nil {
		return err
	}

	return nil
}

func (tv *TreeView) clearItems() error {
	if 0 == tv.SendMessage(win.TVM_DELETEITEM, 0, 0) {
		return newError("SendMessage(TVM_DELETEITEM) failed")
	}

	tv.item2Info = make(map[TreeItem]*treeViewItemInfo)
	tv.handle2Item = make(map[win.HTREEITEM]TreeItem)

	return nil
}

func (tv *TreeView) insertRoots() error {
	for i := tv.model.RootCount() - 1; i >= 0; i-- {
		if _, err := tv.insertItem(tv.model.RootAt(i)); err != nil {
			return err
		}
	}

	return nil
}

func (tv *TreeView) ApplyDPI(dpi int) {
	tv.WidgetBase.ApplyDPI(dpi)

	tv.disposeImageListAndCaches()
}

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

	tv.SendMessage(win.TVM_SETIMAGELIST, 0, uintptr(tv.hIml))

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

func (tv *TreeView) disposeImageListAndCaches() {
	if tv.hIml != 0 && !tv.usingSysIml {
		win.ImageList_Destroy(tv.hIml)
	}
	tv.hIml = 0

	tv.imageUintptr2Index = nil
	tv.filePath2IconIndex = nil
}

func (tv *TreeView) setTVITEMImageInfo(tvi *win.TVITEM, item TreeItem) {
	if imager, ok := item.(Imager); ok {
		if tv.hIml == 0 {
			tv.applyImageListForImage(imager.Image())
		}

		// FIXME: If not setting TVIF_SELECTEDIMAGE and tvi.ISelectedImage,
		// some default icon will show up, even though we have not asked for it.

		tvi.Mask |= win.TVIF_IMAGE | win.TVIF_SELECTEDIMAGE
		tvi.IImage = imageIndexMaybeAdd(
			imager.Image(),
			tv.hIml,
			tv.usingSysIml,
			tv.imageUintptr2Index,
			tv.filePath2IconIndex,
			tv.DPI())

		tvi.ISelectedImage = tvi.IImage
	}
}

func (tv *TreeView) insertItem(item TreeItem) (win.HTREEITEM, error) {
	return tv.insertItemAfter(item, win.TVI_FIRST)
}

func (tv *TreeView) insertItemAfter(item TreeItem, hInsertAfter win.HTREEITEM) (win.HTREEITEM, error) {
	var tvins win.TVINSERTSTRUCT
	tvi := &tvins.Item

	tvi.Mask = win.TVIF_CHILDREN | win.TVIF_TEXT
	tvi.PszText = win.LPSTR_TEXTCALLBACK
	tvi.CChildren = win.I_CHILDRENCALLBACK

	tv.setTVITEMImageInfo(tvi, item)

	parent := item.Parent()

	if parent == nil {
		tvins.HParent = win.TVI_ROOT
	} else {
		info := tv.item2Info[parent]
		if info == nil {
			return 0, newError("invalid parent")
		}
		tvins.HParent = info.handle
	}

	tvins.HInsertAfter = hInsertAfter

	hItem := win.HTREEITEM(tv.SendMessage(win.TVM_INSERTITEM, 0, uintptr(unsafe.Pointer(&tvins))))
	if hItem == 0 {
		return 0, newError("TVM_INSERTITEM failed")
	}
	tv.item2Info[item] = &treeViewItemInfo{hItem, make(map[TreeItem]win.HTREEITEM)}
	tv.handle2Item[hItem] = item

	if !tv.lazyPopulation {
		if err := tv.insertChildren(item); err != nil {
			return 0, err
		}
	}

	return hItem, nil
}

func (tv *TreeView) insertChildren(parent TreeItem) error {
	info := tv.item2Info[parent]

	for i := parent.ChildCount() - 1; i >= 0; i-- {
		child := parent.ChildAt(i)

		if handle, err := tv.insertItem(child); err != nil {
			return err
		} else {
			info.child2Handle[child] = handle
		}
	}

	return nil
}

func (tv *TreeView) updateItem(item TreeItem) error {
	tvi := &win.TVITEM{
		Mask:    win.TVIF_TEXT,
		HItem:   tv.item2Info[item].handle,
		PszText: win.LPSTR_TEXTCALLBACK,
	}

	tv.setTVITEMImageInfo(tvi, item)

	if 0 == tv.SendMessage(win.TVM_SETITEM, 0, uintptr(unsafe.Pointer(tvi))) {
		return newError("SendMessage(TVM_SETITEM) failed")
	}

	return nil
}

func (tv *TreeView) removeItem(item TreeItem) error {
	if err := tv.removeDescendants(item); err != nil {
		return err
	}

	info := tv.item2Info[item]
	if info == nil {
		return newError("invalid item")
	}

	if 0 == tv.SendMessage(win.TVM_DELETEITEM, 0, uintptr(info.handle)) {
		return newError("SendMessage(TVM_DELETEITEM) failed")
	}

	if parentInfo := tv.item2Info[item.Parent()]; parentInfo != nil {
		delete(parentInfo.child2Handle, item)
	}
	delete(tv.item2Info, item)
	delete(tv.handle2Item, info.handle)

	return nil
}

func (tv *TreeView) removeDescendants(parent TreeItem) error {
	for item, _ := range tv.item2Info[parent].child2Handle {
		if err := tv.removeItem(item); err != nil {
			return err
		}
	}

	return nil
}

func (tv *TreeView) ensureItemAndAncestorsInserted(item TreeItem) error {
	if item == nil {
		return newError("invalid item")
	}

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

	var hierarchy []TreeItem

	for item != nil && tv.item2Info[item] == nil {
		item = item.Parent()

		if item != nil {
			hierarchy = append(hierarchy, item)
		} else {
			return newError("invalid item")
		}
	}

	for i := len(hierarchy) - 1; i >= 0; i-- {
		if err := tv.insertChildren(hierarchy[i]); err != nil {
			return err
		}
	}

	return nil
}

func (tv *TreeView) Expanded(item TreeItem) bool {
	if tv.item2Info[item] == nil {
		return false
	}

	tvi := &win.TVITEM{
		HItem:     tv.item2Info[item].handle,
		Mask:      win.TVIF_STATE,
		StateMask: win.TVIS_EXPANDED,
	}

	if 0 == tv.SendMessage(win.TVM_GETITEM, 0, uintptr(unsafe.Pointer(tvi))) {
		newError("SendMessage(TVM_GETITEM) failed")
	}

	return tvi.State&win.TVIS_EXPANDED != 0
}

func (tv *TreeView) SetExpanded(item TreeItem, expanded bool) error {
	if expanded {
		if err := tv.ensureItemAndAncestorsInserted(item); err != nil {
			return err
		}
	}

	info := tv.item2Info[item]
	if info == nil {
		return newError("invalid item")
	}

	var action uintptr
	if expanded {
		action = win.TVE_EXPAND
	} else {
		action = win.TVE_COLLAPSE
	}

	if 0 == tv.SendMessage(win.TVM_EXPAND, action, uintptr(info.handle)) {
		return newError("SendMessage(TVM_EXPAND) failed")
	}

	return nil
}

func (tv *TreeView) ExpandedChanged() *TreeItemEvent {
	return tv.expandedChangedPublisher.Event()
}

func (tv *TreeView) CurrentItemChanged() *Event {
	return tv.currentItemChangedPublisher.Event()
}

func (tv *TreeView) ItemActivated() *Event {
	return tv.itemActivatedPublisher.Event()
}

func (tv *TreeView) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_GETDLGCODE:
		if wParam == win.VK_RETURN {
			return win.DLGC_WANTALLKEYS
		}

	case win.WM_NOTIFY:
		nmhdr := (*win.NMHDR)(unsafe.Pointer(lParam))

		switch nmhdr.Code {
		case win.TVN_GETDISPINFO:
			nmtvdi := (*win.NMTVDISPINFO)(unsafe.Pointer(lParam))
			item := tv.handle2Item[nmtvdi.Item.HItem]

			if nmtvdi.Item.Mask&win.TVIF_TEXT != 0 {
				var text string
				rc := win.RECT{Left: int32(nmtvdi.Item.HItem)}
				if 0 != tv.SendMessage(win.TVM_GETITEMRECT, 0, uintptr(unsafe.Pointer(&rc))) {
					// Only retrieve text if the item is visible. Why isn't Windows doing this for us?
					text = item.Text()
				}

				utf16 := syscall.StringToUTF16(text)
				buf := (*[264]uint16)(unsafe.Pointer(nmtvdi.Item.PszText))
				max := mini(len(utf16), int(nmtvdi.Item.CchTextMax))
				copy((*buf)[:], utf16[:max])
				(*buf)[max-1] = 0
			}
			if nmtvdi.Item.Mask&win.TVIF_CHILDREN != 0 {
				if hc, ok := item.(HasChilder); ok {
					if hc.HasChild() {
						nmtvdi.Item.CChildren = 1
					} else {
						nmtvdi.Item.CChildren = 0
					}
				} else {
					nmtvdi.Item.CChildren = int32(item.ChildCount())
				}
			}

		case win.TVN_ITEMEXPANDING:
			nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam))
			item := tv.handle2Item[nmtv.ItemNew.HItem]

			if nmtv.Action == win.TVE_EXPAND && tv.lazyPopulation {
				info := tv.item2Info[item]
				if len(info.child2Handle) == 0 {
					tv.insertChildren(item)
				}
			}

		case win.TVN_ITEMEXPANDED:
			nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam))
			item := tv.handle2Item[nmtv.ItemNew.HItem]

			switch nmtv.Action {
			case win.TVE_COLLAPSE:
				tv.expandedChangedPublisher.Publish(item)

			case win.TVE_COLLAPSERESET:

			case win.TVE_EXPAND:
				tv.expandedChangedPublisher.Publish(item)

			case win.TVE_EXPANDPARTIAL:

			case win.TVE_TOGGLE:
			}

		case win.NM_DBLCLK:
			tv.itemActivatedPublisher.Publish()

		case win.TVN_KEYDOWN:
			nmtvkd := (*win.NMTVKEYDOWN)(unsafe.Pointer(lParam))
			if nmtvkd.WVKey == uint16(KeyReturn) {
				tv.itemActivatedPublisher.Publish()
			}

		case win.TVN_SELCHANGED:
			nmtv := (*win.NMTREEVIEW)(unsafe.Pointer(lParam))

			tv.currItem = tv.handle2Item[nmtv.ItemNew.HItem]

			tv.currentItemChangedPublisher.Publish()
		}
	}

	return tv.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
}

func (*TreeView) NeedsWmSize() bool {
	return true
}

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