// 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 (
	"bytes"
	"fmt"
	"math"
	"strconv"
	"strings"
	"syscall"
	"unsafe"

	"github.com/lxn/win"
)

const numberEditWindowClass = `\o/ Walk_NumberEdit_Class \o/`

func init() {
	AppendToWalkInit(func() {
		MustRegisterWindowClass(numberEditWindowClass)
	})
}

// NumberEdit is a widget that is suited to edit numeric values.
type NumberEdit struct {
	WidgetBase
	edit                     *numberLineEdit
	maxValueChangedPublisher EventPublisher
	minValueChangedPublisher EventPublisher
	prefixChangedPublisher   EventPublisher
	suffixChangedPublisher   EventPublisher
}

// NewNumberEdit returns a new NumberEdit widget as child of parent.
func NewNumberEdit(parent Container) (*NumberEdit, error) {
	ne := new(NumberEdit)

	if err := InitWidget(
		ne,
		parent,
		numberEditWindowClass,
		win.WS_VISIBLE,
		win.WS_EX_CONTROLPARENT); err != nil {
		return nil, err
	}

	var succeeded bool
	defer func() {
		if !succeeded {
			ne.Dispose()
		}
	}()

	var err error
	if ne.edit, err = newNumberLineEdit(ne); err != nil {
		return nil, err
	}

	ne.edit.applyFont(ne.Font())

	ne.SetRange(-math.MaxFloat64, math.MaxFloat64)

	if err = ne.SetValue(0); err != nil {
		return nil, err
	}

	ne.GraphicsEffects().Add(InteractionEffect)
	ne.GraphicsEffects().Add(FocusEffect)

	ne.MustRegisterProperty("MaxValue", NewProperty(
		func() interface{} {
			return ne.MaxValue()
		},
		func(v interface{}) error {
			return ne.SetRange(ne.MinValue(), assertFloat64Or(v, 0.0))
		},
		ne.minValueChangedPublisher.Event()))

	ne.MustRegisterProperty("MinValue", NewProperty(
		func() interface{} {
			return ne.MinValue()
		},
		func(v interface{}) error {
			return ne.SetRange(assertFloat64Or(v, 0.0), ne.MaxValue())
		},
		ne.maxValueChangedPublisher.Event()))

	ne.MustRegisterProperty("Prefix", NewProperty(
		func() interface{} {
			return ne.Prefix()
		},
		func(v interface{}) error {
			return ne.SetPrefix(assertStringOr(v, ""))
		},
		ne.prefixChangedPublisher.Event()))

	ne.MustRegisterProperty("ReadOnly", NewProperty(
		func() interface{} {
			return ne.ReadOnly()
		},
		func(v interface{}) error {
			return ne.SetReadOnly(v.(bool))
		},
		ne.edit.readOnlyChangedPublisher.Event()))

	ne.MustRegisterProperty("Suffix", NewProperty(
		func() interface{} {
			return ne.Suffix()
		},
		func(v interface{}) error {
			return ne.SetSuffix(assertStringOr(v, ""))
		},
		ne.suffixChangedPublisher.Event()))

	ne.MustRegisterProperty("Value", NewProperty(
		func() interface{} {
			return ne.Value()
		},
		func(v interface{}) error {
			return ne.SetValue(assertFloat64Or(v, 0.0))
		},
		ne.edit.valueChangedPublisher.Event()))

	succeeded = true

	return ne, nil
}

func (ne *NumberEdit) applyEnabled(enabled bool) {
	ne.WidgetBase.applyEnabled(enabled)

	if ne.edit == nil {
		return
	}

	ne.edit.applyEnabled(enabled)
}

func (ne *NumberEdit) applyFont(font *Font) {
	ne.WidgetBase.applyFont(font)

	if ne.edit == nil {
		return
	}

	ne.edit.applyFont(font)
}

// Decimals returns the number of decimal places in the NumberEdit.
func (ne *NumberEdit) Decimals() int {
	return ne.edit.decimals
}

// SetDecimals sets the number of decimal places in the NumberEdit.
func (ne *NumberEdit) SetDecimals(decimals int) error {
	if decimals < 0 || decimals > 8 {
		return newError("decimals must >= 0 && <= 8")
	}

	ne.edit.decimals = decimals

	return ne.SetValue(ne.edit.value)
}

// Prefix returns the text that appears in the NumberEdit before the number.
func (ne *NumberEdit) Prefix() string {
	return syscall.UTF16ToString(ne.edit.prefix)
}

// SetPrefix sets the text that appears in the NumberEdit before the number.
func (ne *NumberEdit) SetPrefix(prefix string) error {
	if prefix == ne.Prefix() {
		return nil
	}

	p, err := syscall.UTF16FromString(prefix)
	if err != nil {
		return err
	}

	old := ne.edit.prefix
	ne.edit.prefix = p[:len(p)-1]

	if err := ne.edit.setTextFromValue(ne.edit.value); err != nil {
		ne.edit.prefix = old
		return err
	}

	ne.prefixChangedPublisher.Publish()

	return nil
}

// PrefixChanged returns the event that is published when the prefix changed.
func (ne *NumberEdit) PrefixChanged() *Event {
	return ne.prefixChangedPublisher.Event()
}

// Suffix returns the text that appears in the NumberEdit after the number.
func (ne *NumberEdit) Suffix() string {
	return syscall.UTF16ToString(ne.edit.suffix)
}

// SetSuffix sets the text that appears in the NumberEdit after the number.
func (ne *NumberEdit) SetSuffix(suffix string) error {
	if suffix == ne.Suffix() {
		return nil
	}

	s, err := syscall.UTF16FromString(suffix)
	if err != nil {
		return err
	}

	old := ne.edit.suffix
	ne.edit.suffix = s[:len(s)-1]

	if err := ne.edit.setTextFromValue(ne.edit.value); err != nil {
		ne.edit.suffix = old
		return err
	}

	ne.suffixChangedPublisher.Publish()

	return nil
}

// SuffixChanged returns the event that is published when the suffix changed.
func (ne *NumberEdit) SuffixChanged() *Event {
	return ne.suffixChangedPublisher.Event()
}

// Increment returns the amount by which the NumberEdit increments or decrements
// its value, when the user presses the KeyDown or KeyUp keys, or when the mouse
// wheel is rotated.
func (ne *NumberEdit) Increment() float64 {
	return ne.edit.increment
}

// SetIncrement sets the amount by which the NumberEdit increments or decrements
// its value, when the user presses the KeyDown or KeyUp keys, or when the mouse
// wheel is rotated.
func (ne *NumberEdit) SetIncrement(increment float64) error {
	ne.edit.increment = increment

	return nil
}

// MinValue returns the minimum value the NumberEdit will accept.
func (ne *NumberEdit) MinValue() float64 {
	return ne.edit.minValue
}

// MinValue returns the maximum value the NumberEdit will accept.
func (ne *NumberEdit) MaxValue() float64 {
	return ne.edit.maxValue
}

// SetRange sets the minimum and maximum values the NumberEdit will accept.
//
// If the current value is out of this range, it will be adjusted.
func (ne *NumberEdit) SetRange(min, max float64) error {
	if min > max {
		return newError(fmt.Sprintf("invalid range - min: %f, max: %f", min, max))
	}

	minChanged := min != ne.edit.minValue
	maxChanged := max != ne.edit.maxValue

	ne.edit.minValue = min
	ne.edit.maxValue = max
	if min != max {
		if ne.edit.value < min {
			if err := ne.edit.setValue(min, true); err != nil {
				return err
			}
		} else if ne.edit.value > max {
			if err := ne.edit.setValue(max, true); err != nil {
				return err
			}
		}
	}

	if minChanged {
		ne.minValueChangedPublisher.Publish()
	}
	if maxChanged {
		ne.maxValueChangedPublisher.Publish()
	}

	return nil
}

// Value returns the value of the NumberEdit.
func (ne *NumberEdit) Value() float64 {
	return ne.edit.value
}

// SetValue sets the value of the NumberEdit.
func (ne *NumberEdit) SetValue(value float64) error {
	if ne.edit.minValue != ne.edit.maxValue &&
		(value < ne.edit.minValue || value > ne.edit.maxValue) {

		return newError("value out of range")
	}

	return ne.edit.setValue(value, true)
}

// ValueChanged returns an Event that can be used to track changes to Value.
func (ne *NumberEdit) ValueChanged() *Event {
	return ne.edit.valueChangedPublisher.Event()
}

// SetFocus sets the keyboard input focus to the NumberEdit.
func (ne *NumberEdit) SetFocus() error {
	if win.SetFocus(ne.edit.hWnd) == 0 {
		return lastError("SetFocus")
	}

	return nil
}

// TextSelection returns the range of the current text selection of the
// NumberEdit.
func (ne *NumberEdit) TextSelection() (start, end int) {
	return ne.edit.TextSelection()
}

// SetTextSelection sets the range of the current text selection of the
// NumberEdit.
func (ne *NumberEdit) SetTextSelection(start, end int) {
	ne.edit.SetTextSelection(start, end)
}

// ReadOnly returns whether the NumberEdit is in read-only mode.
func (ne *NumberEdit) ReadOnly() bool {
	return ne.edit.ReadOnly()
}

// SetReadOnly sets whether the NumberEdit is in read-only mode.
func (ne *NumberEdit) SetReadOnly(readOnly bool) error {
	if readOnly != ne.ReadOnly() {
		ne.invalidateBorderInParent()
	}

	return ne.edit.SetReadOnly(readOnly)
}

// Background returns the background Brush of the NumberEdit.
//
// By default this is nil.
func (ne *NumberEdit) Background() Brush {
	return ne.edit.Background()
}

// SetBackground sets the background Brush of the NumberEdit.
func (ne *NumberEdit) SetBackground(bg Brush) {
	ne.edit.SetBackground(bg)
}

// TextColor returns the Color used to draw the text of the NumberEdit.
func (ne *NumberEdit) TextColor() Color {
	return ne.edit.TextColor()
}

// TextColor sets the Color used to draw the text of the NumberEdit.
func (ne *NumberEdit) SetTextColor(c Color) {
	ne.edit.SetTextColor(c)
}

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

// WndProc is the window procedure of the NumberEdit.
//
// When implementing your own WndProc to add or modify behavior, call the
// WndProc of the embedded NumberEdit for messages you don't handle yourself.
func (ne *NumberEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_CTLCOLOREDIT, win.WM_CTLCOLORSTATIC:
		if hBrush := ne.handleWMCTLCOLOR(wParam, lParam); hBrush != 0 {
			return hBrush
		}

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

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

		if ne.edit == nil {
			break
		}

		cb := ne.ClientBoundsPixels()
		if err := ne.edit.SetBoundsPixels(cb); err != nil {
			break
		}
	}

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

func (ne *NumberEdit) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
	return &numberEditLayoutItem{
		idealSize: ne.dialogBaseUnitsToPixels(Size{50, 12}),
		minSize:   ne.dialogBaseUnitsToPixels(Size{20, 12}),
	}
}

type numberEditLayoutItem struct {
	LayoutItemBase
	idealSize Size // in native pixels
	minSize   Size // in native pixels
}

func (*numberEditLayoutItem) LayoutFlags() LayoutFlags {
	return ShrinkableHorz | GrowableHorz
}

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

func (li *numberEditLayoutItem) MinSize() Size {
	return li.minSize
}

type numberLineEdit struct {
	*LineEdit
	buf                   *bytes.Buffer
	prefix                []uint16
	suffix                []uint16
	value                 float64
	minValue              float64
	maxValue              float64
	increment             float64
	decimals              int
	valueChangedPublisher EventPublisher
	inEditMode            bool
}

func newNumberLineEdit(parent Widget) (*numberLineEdit, error) {
	nle := &numberLineEdit{
		buf:       new(bytes.Buffer),
		increment: 1,
	}

	var err error
	if nle.LineEdit, err = newLineEdit(parent); err != nil {
		return nil, err
	}

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

	if err := nle.LineEdit.setAndClearStyleBits(win.ES_RIGHT, win.ES_LEFT|win.ES_CENTER); err != nil {
		return nil, err
	}

	if err := InitWrapperWindow(nle); err != nil {
		return nil, err
	}

	succeeded = true

	return nle, nil
}

func (nle *numberLineEdit) TextColor() Color {
	return nle.LineEdit.TextColor()
}

func (nle *numberLineEdit) SetTextColor(c Color) {
	nle.LineEdit.SetTextColor(c)
}

func (nle *numberLineEdit) setValue(value float64, setText bool) error {
	if setText {
		if err := nle.setTextFromValue(value); err != nil {
			return err
		}
	}

	if value == nle.value {
		return nil
	}

	nle.value = value

	nle.valueChangedPublisher.Publish()

	return nil
}

func (nle *numberLineEdit) setTextFromValue(value float64) error {
	nle.buf.Reset()

	nle.buf.WriteString(syscall.UTF16ToString(nle.prefix))

	if nle.decimals > 0 {
		nle.buf.WriteString(FormatFloatGrouped(value, nle.decimals))
	} else {
		nle.buf.WriteString(FormatFloat(value, nle.decimals))
	}

	nle.buf.WriteString(syscall.UTF16ToString(nle.suffix))

	return nle.SetText(nle.buf.String())
}

func (nle *numberLineEdit) endEdit() error {
	if err := nle.setTextFromValue(nle.value); err != nil {
		return err
	}

	nle.inEditMode = false

	return nil
}

func (nle *numberLineEdit) processChar(text []uint16, start, end int, key Key, char uint16) {
	hadSelection := start != end

	if !nle.inEditMode {
		var groupSepsBeforeStart int
		if nle.decimals > 0 {
			groupSepsBeforeStart = uint16CountUint16(text[:start], groupSepUint16)
		}

		if hadSelection {
			text = append(text[:start], text[end:]...)
		}

		if nle.decimals > 0 {
			text = uint16RemoveUint16(text, groupSepUint16)
			start -= groupSepsBeforeStart
		}

		nle.inEditMode = true
	} else {
		if hadSelection {
			text = append(text[:start], text[end:]...)
		}
	}

	end = start

	switch key {
	case KeyBack:
		if !hadSelection && start > 0 {
			start -= 1
			text = append(text[:start], text[start+1:]...)
		}

	case KeyDelete:
		if !hadSelection && start < len(text) {
			text = append(text[:start], text[start+1:]...)
		}

	default:
		t := make([]uint16, len(text[:start]), len(text)+1)
		copy(t, text[:start])
		t = append(t, char)
		text = append(t, text[start:]...)
		start += 1
	}

	nle.buf.Reset()

	str := syscall.UTF16ToString(text)

	nle.buf.WriteString(syscall.UTF16ToString(nle.prefix))
	nle.buf.WriteString(str)
	nle.buf.WriteString(syscall.UTF16ToString(nle.suffix))

	nle.SetText(nle.buf.String())

	start += len(nle.prefix)
	nle.SetTextSelection(start, start)

	nle.tryUpdateValue(false)
}

func (nle *numberLineEdit) tryUpdateValue(setText bool) bool {
	t := nle.textUTF16()
	t = t[len(nle.prefix) : len(t)-len(nle.suffix)]

	text := strings.Replace(syscall.UTF16ToString(t), decimalSepS, ".", 1)

	switch text {
	case "", ".":
		text = "0"
	}

	if value, err := strconv.ParseFloat(text, 64); err == nil {
		if nle.minValue == nle.maxValue || value >= nle.minValue && value <= nle.maxValue {
			return nle.setValue(value, setText) == nil
		}
	}

	return false
}

func (nle *numberLineEdit) selectNumber() {
	nle.SetTextSelection(len(nle.prefix), len(nle.textUTF16())-len(nle.suffix))
}

func (nle *numberLineEdit) textUTF16() []uint16 {
	textLength := nle.SendMessage(win.WM_GETTEXTLENGTH, 0, 0)
	buf := make([]uint16, textLength+1)
	nle.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0])))

	return buf[:len(buf)-1]
}

func (nle *numberLineEdit) incrementValue(delta float64) {
	value := nle.value + delta

	if nle.minValue != nle.maxValue {
		if value < nle.minValue {
			value = nle.minValue
		} else if value > nle.maxValue {
			value = nle.maxValue
		}
	}

	nle.setValue(value, true)
	nle.selectNumber()
}

func (nle *numberLineEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_CHAR:
		if nle.ReadOnly() {
			break
		}

		if AltDown() {
			return 0
		}

		if ControlDown() {
			if wParam == 1 {
				// Ctrl+A
				return 0
			}
			break
		}

		char := uint16(wParam)

		text := nle.textUTF16()
		text = text[len(nle.prefix) : len(text)-len(nle.suffix)]
		start, end := nle.TextSelection()
		start -= len(nle.prefix)
		end -= len(nle.prefix)

		if Key(wParam) == KeyBack {
			nle.processChar(text, start, end, KeyBack, 0)
			return 0
		}

		switch char {
		case uint16('0'), uint16('1'), uint16('2'), uint16('3'), uint16('4'), uint16('5'), uint16('6'), uint16('7'), uint16('8'), uint16('9'):
			if start == end && nle.decimals > 0 {
				if i := uint16IndexUint16(text, decimalSepUint16); i > -1 && i < len(text)-nle.decimals && start > i {
					return 0
				}
			}

			nle.processChar(text, start, end, 0, char)
			return 0

		case uint16('-'):
			if nle.minValue != nle.maxValue && nle.minValue >= 0 {
				return 0
			}

			if start > 0 || uint16ContainsUint16(text, uint16('-')) && end == 0 {
				return 0
			}

			nle.processChar(text, start, end, 0, char)
			return 0

		case decimalSepUint16:
			if nle.decimals == 0 {
				return 0
			}

			if start == 0 && end == 0 && len(text) > 0 && text[0] == '-' {
				return 0
			}

			if end < len(text)-nle.decimals {
				return 0
			}

			if i := uint16IndexUint16(text, decimalSepUint16); i > -1 && i <= start || i > end {
				return 0
			}

			nle.processChar(text, start, end, 0, char)
			return 0

		default:
			return 0
		}

	case win.WM_KEYDOWN:
		switch Key(wParam) {
		case KeyA:
			if ControlDown() {
				nle.selectNumber()
				return 0
			}

		case KeyDelete:
			if nle.ReadOnly() {
				break
			}

			text := nle.textUTF16()
			text = text[len(nle.prefix) : len(text)-len(nle.suffix)]
			start, end := nle.TextSelection()
			start -= len(nle.prefix)
			end -= len(nle.prefix)

			nle.processChar(text, start, end, KeyDelete, 0)
			return 0

		case KeyDown:
			if nle.ReadOnly() || nle.increment <= 0 {
				return 0
			}

			nle.incrementValue(-nle.increment)
			return 0

		case KeyEnd:
			start, end := nle.TextSelection()
			end = len(nle.textUTF16()) - len(nle.suffix)
			if !ShiftDown() {
				start = end
			}
			nle.SetTextSelection(start, end)
			return 0

		case KeyHome:
			start, end := nle.TextSelection()
			start = len(nle.prefix)
			if !ShiftDown() {
				end = start
			}
			nle.SetTextSelection(start, end)
			return 0

		case KeyLeft:
			var pos win.POINT
			win.GetCaretPos(&pos)

			lParam := uintptr(win.MAKELONG(uint16(pos.X), uint16(pos.Y)))
			i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))

			if min := len(nle.prefix); i <= min {
				if !ShiftDown() {
					nle.SetTextSelection(min, min)
				}
				return 0
			}

		case KeyReturn:
			if nle.ReadOnly() {
				break
			}

			if nle.inEditMode {
				nle.endEdit()
				nle.selectNumber()
				return 0
			}

		case KeyRight:
			var pos win.POINT
			win.GetCaretPos(&pos)

			lParam := uintptr(win.MAKELONG(uint16(pos.X), uint16(pos.Y)))
			i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))

			if max := len(nle.textUTF16()) - len(nle.suffix); i >= max {
				if !ShiftDown() {
					nle.SetTextSelection(max, max)
				}
				return 0
			}

		case KeyUp:
			if nle.ReadOnly() || nle.increment <= 0 {
				return 0
			}

			nle.incrementValue(nle.increment)
			return 0
		}

	case win.WM_GETDLGCODE:
		if !nle.inEditMode {
			if form := ancestor(nle); form != nil {
				if dlg, ok := form.(dialogish); ok {
					if dlg.DefaultButton() != nil {
						// If the NumberEdit 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_KILLFOCUS:
		nle.onFocusChanged()
		nle.endEdit()

	case win.WM_LBUTTONDOWN:
		i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))

		if min := len(nle.prefix); i < min {
			nle.SetFocus()
			nle.SetTextSelection(min, min)
			return 0
		}
		if max := len(nle.textUTF16()) - len(nle.suffix); i > max {
			nle.SetFocus()
			nle.SetTextSelection(max, max)
			return 0
		}

	case win.WM_LBUTTONDBLCLK:
		nle.selectNumber()
		return 0

	case win.WM_MOUSEMOVE:
		i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))

		if min := len(nle.prefix); i < min {
			return 0
		}
		if max := len(nle.textUTF16()) - len(nle.suffix); i > max {
			return 0
		}

	case win.WM_MOUSEWHEEL:
		if nle.ReadOnly() || nle.increment <= 0 {
			break
		}

		delta := float64(int16(win.HIWORD(uint32(wParam))))
		nle.incrementValue(delta / 120 * nle.increment)
		return 0

	case win.WM_PASTE:
		if nle.ReadOnly() {
			break
		}

		ret := nle.LineEdit.WndProc(hwnd, msg, wParam, lParam)
		if !nle.tryUpdateValue(true) {
			nle.setTextFromValue(nle.value)
		}
		nle.selectNumber()
		return ret

	case win.WM_SETFOCUS:
		nle.onFocusChanged()
		nle.selectNumber()

	case win.EM_SETSEL:
		start := int(wParam)
		end := int(lParam)
		adjusted := false
		if min := len(nle.prefix); start < min {
			start = min
			adjusted = true
		}
		if max := len(nle.textUTF16()) - len(nle.suffix); end < 0 || end > max {
			end = max
			adjusted = true
		}

		if adjusted {
			nle.SetTextSelection(start, end)
			return 0
		}
	}

	return nle.LineEdit.WndProc(hwnd, msg, wParam, lParam)
}

func (nle *numberLineEdit) onFocusChanged() {
	if ne := windowFromHandle(win.GetParent(nle.hWnd)); ne != nil {
		if wnd := windowFromHandle(win.GetParent(ne.Handle())); wnd != nil {
			if _, ok := wnd.(Container); ok {
				ne.(Widget).AsWidgetBase().invalidateBorderInParent()
			}
		}
	}
}

func (ne *NumberEdit) SetToolTipText(s string) error {
	return ne.edit.SetToolTipText(s)
}
