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

	"github.com/lxn/win"
)

type ToolBarButtonStyle int

const (
	ToolBarButtonImageOnly ToolBarButtonStyle = iota
	ToolBarButtonTextOnly
	ToolBarButtonImageBeforeText
	ToolBarButtonImageAboveText
)

type ToolBar struct {
	WidgetBase
	imageList          *ImageList
	actions            *ActionList
	defaultButtonWidth int
	maxTextRows        int
	buttonStyle        ToolBarButtonStyle
}

func NewToolBarWithOrientationAndButtonStyle(parent Container, orientation Orientation, buttonStyle ToolBarButtonStyle) (*ToolBar, error) {
	var style uint32
	if orientation == Vertical {
		style = win.CCS_VERT | win.CCS_NORESIZE
	} else {
		style = win.TBSTYLE_WRAPABLE
	}

	if buttonStyle != ToolBarButtonImageAboveText {
		style |= win.TBSTYLE_LIST
	}

	tb := &ToolBar{
		buttonStyle: buttonStyle,
	}
	tb.actions = newActionList(tb)

	if orientation == Vertical {
		tb.defaultButtonWidth = 100
	}

	if err := InitWidget(
		tb,
		parent,
		"ToolbarWindow32",
		win.CCS_NODIVIDER|win.TBSTYLE_FLAT|win.TBSTYLE_TOOLTIPS|style,
		0); err != nil {
		return nil, err
	}

	exStyle := tb.SendMessage(win.TB_GETEXTENDEDSTYLE, 0, 0)
	exStyle |= win.TBSTYLE_EX_DRAWDDARROWS | win.TBSTYLE_EX_MIXEDBUTTONS
	tb.SendMessage(win.TB_SETEXTENDEDSTYLE, 0, exStyle)

	return tb, nil
}

func NewToolBar(parent Container) (*ToolBar, error) {
	return NewToolBarWithOrientationAndButtonStyle(parent, Horizontal, ToolBarButtonImageOnly)
}

func NewVerticalToolBar(parent Container) (*ToolBar, error) {
	return NewToolBarWithOrientationAndButtonStyle(parent, Vertical, ToolBarButtonImageAboveText)
}

func (tb *ToolBar) Dispose() {
	tb.WidgetBase.Dispose()

	tb.actions.Clear()

	if tb.imageList != nil {
		tb.imageList.Dispose()
		tb.imageList = nil
	}
}

func (tb *ToolBar) applyFont(font *Font) {
	tb.WidgetBase.applyFont(font)

	tb.applyDefaultButtonWidth()

	tb.RequestLayout()
}

func (tb *ToolBar) ApplyDPI(dpi int) {
	tb.WidgetBase.ApplyDPI(dpi)

	var maskColor Color
	var size Size
	if tb.imageList != nil {
		maskColor = tb.imageList.maskColor
		size = SizeFrom96DPI(tb.imageList.imageSize96dpi, dpi)
	} else {
		size = SizeFrom96DPI(Size{16, 16}, dpi)
	}

	iml, err := NewImageListForDPI(size, maskColor, dpi)
	if err != nil {
		return
	}

	tb.SendMessage(win.TB_SETIMAGELIST, 0, uintptr(iml.hIml))

	if tb.imageList != nil {
		tb.imageList.Dispose()
	}

	tb.imageList = iml

	for _, action := range tb.actions.actions {
		if action.image != nil {
			tb.onActionChanged(action)
		}
	}

	tb.hFont = tb.Font().handleForDPI(tb.DPI())
	setWindowFont(tb.hWnd, tb.hFont)
}

func (tb *ToolBar) Orientation() Orientation {
	style := win.GetWindowLong(tb.hWnd, win.GWL_STYLE)

	if style&win.CCS_VERT > 0 {
		return Vertical
	}

	return Horizontal
}

func (tb *ToolBar) ButtonStyle() ToolBarButtonStyle {
	return tb.buttonStyle
}

func (tb *ToolBar) applyDefaultButtonWidth() error {
	if tb.defaultButtonWidth == 0 {
		return nil
	}

	dpi := tb.DPI()
	width := IntFrom96DPI(tb.defaultButtonWidth, dpi)

	lParam := uintptr(win.MAKELONG(uint16(width), uint16(width)))
	if 0 == tb.SendMessage(win.TB_SETBUTTONWIDTH, 0, lParam) {
		return newError("SendMessage(TB_SETBUTTONWIDTH)")
	}

	size := uint32(tb.SendMessage(win.TB_GETBUTTONSIZE, 0, 0))
	height := win.HIWORD(size)

	lParam = uintptr(win.MAKELONG(uint16(width), height))
	if win.FALSE == tb.SendMessage(win.TB_SETBUTTONSIZE, 0, lParam) {
		return newError("SendMessage(TB_SETBUTTONSIZE)")
	}

	return nil
}

// DefaultButtonWidth returns the default button width of the ToolBar.
//
// The default value for a horizontal ToolBar is 0, resulting in automatic
// sizing behavior. For a vertical ToolBar, the default is 100 pixels.
func (tb *ToolBar) DefaultButtonWidth() int {
	return tb.defaultButtonWidth
}

// SetDefaultButtonWidth sets the default button width of the ToolBar.
//
// Calling this method affects all buttons in the ToolBar, no matter if they are
// added before or after the call. A width of 0 results in automatic sizing
// behavior. Negative values are not allowed.
func (tb *ToolBar) SetDefaultButtonWidth(width int) error {
	if width == tb.defaultButtonWidth {
		return nil
	}

	if width < 0 {
		return newError("width must be >= 0")
	}

	old := tb.defaultButtonWidth

	tb.defaultButtonWidth = width

	for _, action := range tb.actions.actions {
		if err := tb.onActionChanged(action); err != nil {
			tb.defaultButtonWidth = old

			return err
		}
	}

	return tb.applyDefaultButtonWidth()
}

func (tb *ToolBar) MaxTextRows() int {
	return tb.maxTextRows
}

func (tb *ToolBar) SetMaxTextRows(maxTextRows int) error {
	if 0 == tb.SendMessage(win.TB_SETMAXTEXTROWS, uintptr(maxTextRows), 0) {
		return newError("SendMessage(TB_SETMAXTEXTROWS)")
	}

	tb.maxTextRows = maxTextRows

	return nil
}

func (tb *ToolBar) Actions() *ActionList {
	return tb.actions
}

func (tb *ToolBar) ImageList() *ImageList {
	return tb.imageList
}

func (tb *ToolBar) SetImageList(value *ImageList) {
	var hIml win.HIMAGELIST

	if tb.buttonStyle != ToolBarButtonTextOnly && value != nil {
		hIml = value.hIml
	}

	tb.SendMessage(win.TB_SETIMAGELIST, 0, uintptr(hIml))

	tb.imageList = value
}

func (tb *ToolBar) imageIndex(image Image) (imageIndex int32, err error) {
	if tb.imageList == nil {
		dpi := tb.DPI()
		iml, err := NewImageListForDPI(SizeFrom96DPI(Size{16, 16}, dpi), 0, dpi)
		if err != nil {
			return 0, err
		}

		tb.SetImageList(iml)
	}

	imageIndex = -1
	if image != nil {
		if imageIndex, err = tb.imageList.AddImage(image); err != nil {
			return
		}
	}

	return
}

func (tb *ToolBar) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_MOUSEMOVE, win.WM_MOUSELEAVE, win.WM_LBUTTONDOWN:
		tb.Invalidate()

	case win.WM_COMMAND:
		switch win.HIWORD(uint32(wParam)) {
		case win.BN_CLICKED:
			actionId := uint16(win.LOWORD(uint32(wParam)))
			if action, ok := actionsById[actionId]; ok {
				action.raiseTriggered()
				return 0
			}
		}

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

		switch int32(nmhdr.Code) {
		case win.TBN_DROPDOWN:
			nmtb := (*win.NMTOOLBAR)(unsafe.Pointer(lParam))
			actionId := uint16(nmtb.IItem)
			if action := actionsById[actionId]; action != nil {
				var r win.RECT
				if 0 == tb.SendMessage(win.TB_GETRECT, uintptr(actionId), uintptr(unsafe.Pointer(&r))) {
					break
				}

				p := win.POINT{r.Left, r.Bottom}

				if !win.ClientToScreen(tb.hWnd, &p) {
					break
				}

				action.menu.updateItemsWithImageForWindow(tb)

				win.TrackPopupMenuEx(
					action.menu.hMenu,
					win.TPM_NOANIMATION,
					p.X,
					p.Y,
					tb.hWnd,
					nil)

				return win.TBDDRET_DEFAULT
			}
		}

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

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

		tb.SendMessage(win.TB_AUTOSIZE, 0, 0)
	}

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

func (tb *ToolBar) initButtonForAction(action *Action, state, style *byte, image *int32, text *uintptr) (err error) {
	if tb.hasStyleBits(win.CCS_VERT) {
		*state |= win.TBSTATE_WRAP
	} else if tb.defaultButtonWidth == 0 {
		*style |= win.BTNS_AUTOSIZE
	}

	if action.checked {
		*state |= win.TBSTATE_CHECKED
	}

	if action.enabled {
		*state |= win.TBSTATE_ENABLED
	}

	if action.checkable {
		*style |= win.BTNS_CHECK
	}

	if action.exclusive {
		*style |= win.BTNS_GROUP
	}

	if tb.buttonStyle != ToolBarButtonImageOnly && len(action.text) > 0 {
		*style |= win.BTNS_SHOWTEXT
	}

	if action.menu != nil {
		if len(action.Triggered().handlers) > 0 {
			*style |= win.BTNS_DROPDOWN
		} else {
			*style |= win.BTNS_WHOLEDROPDOWN
		}
	}

	if action.IsSeparator() {
		*style = win.BTNS_SEP
	}

	if tb.buttonStyle != ToolBarButtonTextOnly {
		if *image, err = tb.imageIndex(action.image); err != nil {
			return err
		}
	}

	var actionText string
	if s := action.shortcut; tb.buttonStyle == ToolBarButtonImageOnly && s.Key != 0 {
		actionText = fmt.Sprintf("%s (%s)", action.Text(), s.String())
	} else {
		actionText = action.Text()
	}

	if len(actionText) != 0 {
		*text = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(actionText)))
	} else if len(action.toolTip) != 0 {
		*text = uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(action.toolTip)))
	}

	return
}

func (tb *ToolBar) onActionChanged(action *Action) error {
	tbbi := win.TBBUTTONINFO{
		DwMask: win.TBIF_IMAGE | win.TBIF_STATE | win.TBIF_STYLE | win.TBIF_TEXT,
		IImage: win.I_IMAGENONE,
	}

	tbbi.CbSize = uint32(unsafe.Sizeof(tbbi))

	if err := tb.initButtonForAction(
		action,
		&tbbi.FsState,
		&tbbi.FsStyle,
		&tbbi.IImage,
		&tbbi.PszText); err != nil {

		return err
	}

	if 0 == tb.SendMessage(
		win.TB_SETBUTTONINFO,
		uintptr(action.id),
		uintptr(unsafe.Pointer(&tbbi))) {

		return newError("SendMessage(TB_SETBUTTONINFO) failed")
	}

	tb.RequestLayout()

	return nil
}

func (tb *ToolBar) onActionVisibleChanged(action *Action) error {
	if !action.IsSeparator() {
		defer tb.actions.updateSeparatorVisibility()
	}

	if action.Visible() {
		return tb.insertAction(action, true)
	}

	return tb.removeAction(action, true)
}

func (tb *ToolBar) insertAction(action *Action, visibleChanged bool) (err error) {
	if !visibleChanged {
		action.addChangedHandler(tb)
		defer func() {
			if err != nil {
				action.removeChangedHandler(tb)
			}
		}()
	}

	if !action.Visible() {
		return
	}

	index := tb.actions.indexInObserver(action)

	tbb := win.TBBUTTON{
		IdCommand: int32(action.id),
	}

	if err = tb.initButtonForAction(
		action,
		&tbb.FsState,
		&tbb.FsStyle,
		&tbb.IBitmap,
		&tbb.IString); err != nil {

		return
	}

	tb.SetVisible(true)

	tb.SendMessage(win.TB_BUTTONSTRUCTSIZE, uintptr(unsafe.Sizeof(tbb)), 0)

	if win.FALSE == tb.SendMessage(win.TB_INSERTBUTTON, uintptr(index), uintptr(unsafe.Pointer(&tbb))) {
		return newError("SendMessage(TB_ADDBUTTONS)")
	}

	if err = tb.applyDefaultButtonWidth(); err != nil {
		return
	}

	tb.SendMessage(win.TB_AUTOSIZE, 0, 0)

	tb.RequestLayout()

	return
}

func (tb *ToolBar) removeAction(action *Action, visibleChanged bool) error {
	index := tb.actions.indexInObserver(action)

	if !visibleChanged {
		action.removeChangedHandler(tb)
	}

	if 0 == tb.SendMessage(win.TB_DELETEBUTTON, uintptr(index), 0) {
		return newError("SendMessage(TB_DELETEBUTTON) failed")
	}

	tb.RequestLayout()

	return nil
}

func (tb *ToolBar) onInsertedAction(action *Action) error {
	return tb.insertAction(action, false)
}

func (tb *ToolBar) onRemovingAction(action *Action) error {
	return tb.removeAction(action, false)
}

func (tb *ToolBar) onClearingActions() error {
	for i := tb.actions.Len() - 1; i >= 0; i-- {
		if action := tb.actions.At(i); action.Visible() {
			if err := tb.onRemovingAction(action); err != nil {
				return err
			}
		}
	}

	return nil
}

func (tb *ToolBar) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
	buttonSize := uint32(tb.SendMessage(win.TB_GETBUTTONSIZE, 0, 0))

	dpi := tb.DPI()
	width := IntFrom96DPI(tb.defaultButtonWidth, dpi)
	if width == 0 {
		width = int(win.LOWORD(buttonSize))
	}

	height := int(win.HIWORD(buttonSize))

	var size win.SIZE
	var wp uintptr
	var layoutFlags LayoutFlags

	if tb.Orientation() == Vertical {
		wp = win.TRUE
		layoutFlags = ShrinkableVert | GrowableVert | GreedyVert
	} else {
		wp = win.FALSE
		// FIXME: Since reimplementation of BoxLayout we must use 0 here,
		// otherwise the ToolBar contained in MainWindow will eat half the space.
		//layoutFlags = ShrinkableHorz | GrowableHorz
	}

	if win.FALSE != tb.SendMessage(win.TB_GETIDEALSIZE, wp, uintptr(unsafe.Pointer(&size))) {
		if wp == win.TRUE {
			height = int(size.CY)
		} else {
			width = int(size.CX)
		}
	}

	return &toolBarLayoutItem{
		layoutFlags: layoutFlags,
		idealSize:   Size{width, height},
	}
}

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

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

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

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