// 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 (
	"fmt"
	"math/big"
	"reflect"
	"strconv"
	"syscall"
	"time"
	"unsafe"

	"github.com/lxn/win"
)

type ComboBox struct {
	WidgetBase
	bindingValueProvider         BindingValueProvider
	model                        ListModel
	providedModel                interface{}
	bindingMember                string
	displayMember                string
	format                       string
	precision                    int
	itemsResetHandlerHandle      int
	itemChangedHandlerHandle     int
	itemsInsertedHandlerHandle   int
	itemsRemovedHandlerHandle    int
	maxItemTextWidth             int // in native pixels
	prevCurIndex                 int
	selChangeIndex               int
	maxLength                    int
	currentIndexChangedPublisher EventPublisher
	textChangedPublisher         EventPublisher
	editingFinishedPublisher     EventPublisher
	editOrigWndProcPtr           uintptr
	editing                      bool
	persistent                   bool
}

var comboBoxEditWndProcPtr uintptr

func init() {
	AppendToWalkInit(func() {
		comboBoxEditWndProcPtr = syscall.NewCallback(comboBoxEditWndProc)
	})
}

func comboBoxEditWndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	cb := (*ComboBox)(unsafe.Pointer(win.GetWindowLongPtr(hwnd, win.GWLP_USERDATA)))

	switch msg {
	case win.WM_GETDLGCODE:
		if !cb.editing {
			if form := ancestor(cb); form != nil {
				if dlg, ok := form.(dialogish); ok {
					if dlg.DefaultButton() != nil {
						// If the ComboBox lives in a Dialog that has a
						// DefaultButton, we won't swallow the return key.
						break
					}
				}
			}
		}

		if wParam == win.VK_RETURN {
			return win.DLGC_WANTALLKEYS
		}

	case win.WM_KEYDOWN:
		if wParam != win.VK_RETURN || 0 == cb.SendMessage(win.CB_GETDROPPEDSTATE, 0, 0) {
			cb.handleKeyDown(wParam, lParam)
		}

		if cb.editing && wParam == win.VK_RETURN {
			cb.editing = false
			cb.editingFinishedPublisher.Publish()
		}

	case win.WM_KEYUP:
		if wParam != win.VK_RETURN || 0 == cb.SendMessage(win.CB_GETDROPPEDSTATE, 0, 0) {
			cb.handleKeyUp(wParam, lParam)
		}

	case win.WM_SETFOCUS, win.WM_KILLFOCUS:
		cb.invalidateBorderInParent()

		if cb.editing && msg == win.WM_KILLFOCUS {
			cb.editing = false
			cb.editingFinishedPublisher.Publish()
		}
	}

	return win.CallWindowProc(cb.editOrigWndProcPtr, hwnd, msg, wParam, lParam)
}

func NewComboBox(parent Container) (*ComboBox, error) {
	cb, err := newComboBoxWithStyle(parent, win.CBS_AUTOHSCROLL|win.CBS_DROPDOWN)
	if err != nil {
		return nil, err
	}

	editHwnd := win.GetWindow(cb.hWnd, win.GW_CHILD)

	win.SetWindowLongPtr(editHwnd, win.GWLP_USERDATA, uintptr(unsafe.Pointer(cb)))
	cb.editOrigWndProcPtr = win.SetWindowLongPtr(editHwnd, win.GWLP_WNDPROC, comboBoxEditWndProcPtr)

	return cb, nil
}

func NewDropDownBox(parent Container) (*ComboBox, error) {
	return newComboBoxWithStyle(parent, win.CBS_DROPDOWNLIST)
}

func newComboBoxWithStyle(parent Container, style uint32) (*ComboBox, error) {
	cb := &ComboBox{prevCurIndex: -1, selChangeIndex: -1, precision: 2}

	if err := InitWidget(
		cb,
		parent,
		"COMBOBOX",
		win.WS_TABSTOP|win.WS_VISIBLE|win.WS_VSCROLL|style,
		0); err != nil {
		return nil, err
	}

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

	var event *Event
	if style&win.CBS_DROPDOWNLIST == win.CBS_DROPDOWNLIST {
		event = cb.CurrentIndexChanged()
	} else {
		event = cb.TextChanged()
	}

	cb.GraphicsEffects().Add(InteractionEffect)
	cb.GraphicsEffects().Add(FocusEffect)

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

	cb.MustRegisterProperty("Text", NewProperty(
		func() interface{} {
			return cb.Text()
		},
		func(v interface{}) error {
			return cb.SetText(assertStringOr(v, ""))
		},
		event))

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

			return nil
		},
		cb.CurrentIndexChanged()))

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

	cb.MustRegisterProperty("TextNotEmpty", NewReadOnlyBoolProperty(
		func() bool {
			return cb.Text() != ""
		},
		cb.CurrentIndexChanged()))

	cb.MustRegisterProperty("Value", NewProperty(
		func() interface{} {
			if cb.Editable() {
				return cb.Text()
			}

			index := cb.CurrentIndex()

			if cb.bindingValueProvider == nil || index == -1 {
				return nil
			}

			return cb.bindingValueProvider.BindingValue(index)
		},
		func(v interface{}) error {
			if cb.Editable() {
				return cb.SetText(assertStringOr(v, ""))
			}

			if cb.bindingValueProvider == nil {
				if cb.model == nil {
					return nil
				} else {
					return newError("Data binding is only supported using a model that implements BindingValueProvider.")
				}
			}

			index := -1

			count := cb.model.ItemCount()
			for i := 0; i < count; i++ {
				if cb.bindingValueProvider.BindingValue(i) == v {
					index = i
					break
				}
			}

			return cb.SetCurrentIndex(index)
		},
		event))

	succeeded = true

	return cb, nil
}

func (cb *ComboBox) applyFont(font *Font) {
	cb.WidgetBase.applyFont(font)

	if cb.model != nil {
		cb.maxItemTextWidth = cb.calculateMaxItemTextWidth()
		cb.RequestLayout()
	}
}

func (cb *ComboBox) Editable() bool {
	return !cb.hasStyleBits(win.CBS_DROPDOWNLIST)
}

func (cb *ComboBox) itemString(index int) string {
	switch val := cb.model.Value(index).(type) {
	case string:
		return val

	case time.Time:
		return val.Format(cb.format)

	case *big.Rat:
		return val.FloatString(cb.precision)

	default:
		return fmt.Sprintf(cb.format, val)
	}

	panic("unreachable")
}

func (cb *ComboBox) insertItemAt(index int) error {
	str := cb.itemString(index)
	lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str)))

	if win.CB_ERR == cb.SendMessage(win.CB_INSERTSTRING, uintptr(index), lp) {
		return newError("SendMessage(CB_INSERTSTRING)")
	}

	return nil
}

func (cb *ComboBox) removeItem(index int) error {
	if win.CB_ERR == cb.SendMessage(win.CB_DELETESTRING, uintptr(index), 0) {
		return newError("SendMessage(CB_DELETESTRING")
	}

	return nil
}

func (cb *ComboBox) resetItems() error {
	cb.SetSuspended(true)
	defer cb.SetSuspended(false)

	cb.selChangeIndex = -1

	if win.FALSE == cb.SendMessage(win.CB_RESETCONTENT, 0, 0) {
		return newError("SendMessage(CB_RESETCONTENT)")
	}

	cb.maxItemTextWidth = 0

	cb.SetCurrentIndex(-1)

	if cb.model == nil {
		return nil
	}

	count := cb.model.ItemCount()

	for i := 0; i < count; i++ {
		if err := cb.insertItemAt(i); err != nil {
			return err
		}
	}

	cb.RequestLayout()

	return nil
}

func (cb *ComboBox) attachModel() {
	itemsResetHandler := func() {
		cb.resetItems()
	}
	cb.itemsResetHandlerHandle = cb.model.ItemsReset().Attach(itemsResetHandler)

	itemChangedHandler := func(index int) {
		if win.CB_ERR == cb.SendMessage(win.CB_DELETESTRING, uintptr(index), 0) {
			newError("SendMessage(CB_DELETESTRING)")
		}

		cb.insertItemAt(index)

		cb.SetCurrentIndex(cb.prevCurIndex)
	}
	cb.itemChangedHandlerHandle = cb.model.ItemChanged().Attach(itemChangedHandler)

	cb.itemsInsertedHandlerHandle = cb.model.ItemsInserted().Attach(func(from, to int) {
		for i := from; i <= to; i++ {
			cb.insertItemAt(i)
		}
	})

	cb.itemsRemovedHandlerHandle = cb.model.ItemsRemoved().Attach(func(from, to int) {
		for i := to; i >= from; i-- {
			cb.removeItem(i)
		}
	})
}

func (cb *ComboBox) detachModel() {
	cb.model.ItemsReset().Detach(cb.itemsResetHandlerHandle)
	cb.model.ItemChanged().Detach(cb.itemChangedHandlerHandle)
	cb.model.ItemsInserted().Detach(cb.itemsInsertedHandlerHandle)
	cb.model.ItemsRemoved().Detach(cb.itemsRemovedHandlerHandle)
}

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

// SetModel sets the model of the ComboBox.
//
// It is required that mdl either implements walk.ListModel or
// walk.ReflectListModel or be a slice of pointers to struct or a []string.
func (cb *ComboBox) SetModel(mdl interface{}) error {
	model, ok := mdl.(ListModel)
	if !ok && mdl != nil {
		var err error
		if model, err = newReflectListModel(mdl); err != nil {
			return err
		}

		if _, ok := mdl.([]string); !ok {
			if badms, ok := model.(bindingAndDisplayMemberSetter); ok {
				var bindingMember string
				if cb.Editable() {
					bindingMember = cb.displayMember
				} else {
					bindingMember = cb.bindingMember
				}
				badms.setBindingMember(bindingMember)
				badms.setDisplayMember(cb.displayMember)
			}
		}
	}
	cb.providedModel = mdl

	if cb.model != nil {
		cb.detachModel()
	}

	cb.model = model
	cb.bindingValueProvider, _ = model.(BindingValueProvider)

	if model != nil {
		cb.attachModel()
	}

	if err := cb.resetItems(); err != nil {
		return err
	}

	if !cb.Editable() && model != nil && model.ItemCount() == 1 {
		cb.SetCurrentIndex(0)
	}

	return cb.Invalidate()
}

// BindingMember returns the member from the model of the ComboBox that is bound
// to a field of the data source managed by an associated DataBinder.
//
// This is only applicable to walk.ReflectListModel models and simple slices of
// pointers to struct.
func (cb *ComboBox) BindingMember() string {
	return cb.bindingMember
}

// SetBindingMember sets the member from the model of the ComboBox that is bound
// to a field of the data source managed by an associated DataBinder.
//
// This is only applicable to walk.ReflectListModel models and simple slices of
// pointers to struct.
//
// For a model consisting of items of type S, data source field of type T and
// bindingMember "Foo", this can be one of the following:
//
//	A field		Foo T
//	A method	func (s S) Foo() T
//	A method	func (s S) Foo() (T, error)
//
// If bindingMember is not a simple member name like "Foo", but a path to a
// member like "A.B.Foo", members "A" and "B" both must be one of the options
// mentioned above, but with T having type pointer to struct.
func (cb *ComboBox) SetBindingMember(bindingMember string) error {
	if bindingMember != "" {
		if _, ok := cb.providedModel.([]string); ok {
			return newError("invalid for []string model")
		}
	}

	cb.bindingMember = bindingMember

	if badms, ok := cb.model.(bindingAndDisplayMemberSetter); ok {
		badms.setBindingMember(bindingMember)
	}

	return nil
}

// DisplayMember returns the member from the model of the ComboBox that is
// displayed in the ComboBox.
//
// This is only applicable to walk.ReflectListModel models and simple slices of
// pointers to struct.
func (cb *ComboBox) DisplayMember() string {
	return cb.displayMember
}

// SetDisplayMember sets the member from the model of the ComboBox that is
// displayed in the ComboBox.
//
// This is only applicable to walk.ReflectListModel models and simple slices of
// pointers to struct.
//
// For a model consisting of items of type S, the type of the specified member T
// and displayMember "Foo", this can be one of the following:
//
//	A field		Foo T
//	A method	func (s S) Foo() T
//	A method	func (s S) Foo() (T, error)
//
// If displayMember is not a simple member name like "Foo", but a path to a
// member like "A.B.Foo", members "A" and "B" both must be one of the options
// mentioned above, but with T having type pointer to struct.
func (cb *ComboBox) SetDisplayMember(displayMember string) error {
	if displayMember != "" {
		if _, ok := cb.providedModel.([]string); ok {
			return newError("invalid for []string model")
		}
	}

	cb.displayMember = displayMember

	if badms, ok := cb.model.(bindingAndDisplayMemberSetter); ok {
		badms.setDisplayMember(displayMember)
	}

	return nil
}

func (cb *ComboBox) Format() string {
	return cb.format
}

func (cb *ComboBox) SetFormat(value string) {
	cb.format = value
}

func (cb *ComboBox) Precision() int {
	return cb.precision
}

func (cb *ComboBox) SetPrecision(value int) {
	cb.precision = value
}

func (cb *ComboBox) MaxLength() int {
	return cb.maxLength
}

func (cb *ComboBox) SetMaxLength(value int) {
	cb.SendMessage(win.CB_LIMITTEXT, uintptr(value), 0)

	cb.maxLength = value
}

// calculateMaxItemTextWidth returns maximum item text width in native pixels.
func (cb *ComboBox) calculateMaxItemTextWidth() int {
	hdc := win.GetDC(cb.hWnd)
	if hdc == 0 {
		newError("GetDC failed")
		return -1
	}
	defer win.ReleaseDC(cb.hWnd, hdc)

	hFontOld := win.SelectObject(hdc, win.HGDIOBJ(cb.Font().handleForDPI(cb.DPI())))
	defer win.SelectObject(hdc, hFontOld)

	var maxWidth int

	count := cb.model.ItemCount()
	for i := 0; i < count; i++ {
		var s win.SIZE
		str := syscall.StringToUTF16(cb.itemString(i))

		if !win.GetTextExtentPoint32(hdc, &str[0], int32(len(str)-1), &s) {
			newError("GetTextExtentPoint32 failed")
			return -1
		}

		maxWidth = maxi(maxWidth, int(s.CX))
	}

	return maxWidth
}

func (cb *ComboBox) CurrentIndex() int {
	return int(int32(cb.SendMessage(win.CB_GETCURSEL, 0, 0)))
}

func (cb *ComboBox) SetCurrentIndex(value int) error {
	index := int(int32(cb.SendMessage(win.CB_SETCURSEL, uintptr(value), 0)))

	if index != value {
		return newError("invalid index")
	}

	if value != cb.prevCurIndex {
		cb.prevCurIndex = value
		cb.currentIndexChangedPublisher.Publish()
	}

	return nil
}

func (cb *ComboBox) CurrentIndexChanged() *Event {
	return cb.currentIndexChangedPublisher.Event()
}

func (cb *ComboBox) Text() string {
	return cb.text()
}

func (cb *ComboBox) SetText(value string) error {
	if err := cb.setText(value); err != nil {
		return err
	}

	cb.textChangedPublisher.Publish()

	return nil
}

func (cb *ComboBox) TextSelection() (start, end int) {
	cb.SendMessage(win.CB_GETEDITSEL, uintptr(unsafe.Pointer(&start)), uintptr(unsafe.Pointer(&end)))
	return
}

func (cb *ComboBox) SetTextSelection(start, end int) {
	cb.SendMessage(win.CB_SETEDITSEL, 0, uintptr(win.MAKELONG(uint16(start), uint16(end))))
}

func (cb *ComboBox) TextChanged() *Event {
	return cb.textChangedPublisher.Event()
}

func (cb *ComboBox) EditingFinished() *Event {
	return cb.editingFinishedPublisher.Event()
}

func (cb *ComboBox) Persistent() bool {
	return cb.persistent
}

func (cb *ComboBox) SetPersistent(value bool) {
	cb.persistent = value
}

func (cb *ComboBox) SaveState() error {
	cb.WriteState(strconv.Itoa(cb.CurrentIndex()))

	return nil
}

func (cb *ComboBox) RestoreState() error {
	state, err := cb.ReadState()
	if err != nil {
		return err
	}
	if state == "" {
		return nil
	}

	if i, err := strconv.Atoi(state); err == nil {
		cb.SetCurrentIndex(i)
	}

	return nil
}

func (cb *ComboBox) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_COMMAND:
		code := win.HIWORD(uint32(wParam))
		selIndex := cb.CurrentIndex()

		switch code {
		case win.CBN_EDITCHANGE:
			cb.editing = true
			cb.selChangeIndex = -1
			cb.textChangedPublisher.Publish()

		case win.CBN_SELCHANGE:
			cb.selChangeIndex = selIndex

		case win.CBN_SELENDCANCEL:
			if cb.selChangeIndex != -1 {
				if cb.selChangeIndex < cb.model.ItemCount() {
					cb.SetCurrentIndex(cb.selChangeIndex)
				}

				cb.selChangeIndex = -1
			}

		case win.CBN_SELENDOK:
			if editable := cb.Editable(); editable || selIndex != cb.prevCurIndex {
				if editable && selIndex > -1 {
					cb.Property("Value").Set(cb.model.Value(selIndex))
				}
				cb.currentIndexChangedPublisher.Publish()
				cb.prevCurIndex = selIndex
				return 0
			}

			cb.selChangeIndex = -1
		}

	case win.WM_MOUSEWHEEL:
		if !cb.Enabled() {
			return 0
		}

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

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

		if cb.Editable() {
			result := cb.WidgetBase.WndProc(hwnd, msg, wParam, lParam)

			cb.SetTextSelection(0, 0)

			return result
		}
	}

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

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

func (cb *ComboBox) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
	var layoutFlags LayoutFlags
	if cb.Editable() {
		layoutFlags = GrowableHorz | GreedyHorz
	} else {
		layoutFlags = GrowableHorz
	}

	defaultSize := cb.dialogBaseUnitsToPixels(Size{30, 12})

	if cb.model != nil && cb.maxItemTextWidth <= 0 {
		cb.maxItemTextWidth = cb.calculateMaxItemTextWidth()
	}

	// FIXME: Use GetThemePartSize instead of guessing
	w := maxi(defaultSize.Width, cb.maxItemTextWidth+int(win.GetSystemMetricsForDpi(win.SM_CXVSCROLL, uint32(ctx.dpi)))+8)
	h := defaultSize.Height + 1

	return &comboBoxLayoutItem{
		layoutFlags: layoutFlags,
		idealSize:   Size{w, h},
	}
}

type comboBoxLayoutItem struct {
	LayoutItemBase
	layoutFlags LayoutFlags
	idealSize   Size // in native pixels
}

func (li *comboBoxLayoutItem) LayoutFlags() LayoutFlags {
	return li.layoutFlags
}

func (li *comboBoxLayoutItem) IdealSize() Size {
	return li.idealSize
}

func (li *comboBoxLayoutItem) MinSize() Size {
	return li.idealSize
}
