// 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 Menu struct {
	hMenu   win.HMENU
	window  Window
	actions *ActionList
	getDPI  func() int
}

func newMenuBar(window Window) (*Menu, error) {
	hMenu := win.CreateMenu()
	if hMenu == 0 {
		return nil, lastError("CreateMenu")
	}

	m := &Menu{
		hMenu:  hMenu,
		window: window,
	}
	m.actions = newActionList(m)

	return m, nil
}

func NewMenu() (*Menu, error) {
	hMenu := win.CreatePopupMenu()
	if hMenu == 0 {
		return nil, lastError("CreatePopupMenu")
	}

	var mi win.MENUINFO
	mi.CbSize = uint32(unsafe.Sizeof(mi))

	if !win.GetMenuInfo(hMenu, &mi) {
		return nil, lastError("GetMenuInfo")
	}

	mi.FMask |= win.MIM_STYLE
	mi.DwStyle = win.MNS_CHECKORBMP

	if !win.SetMenuInfo(hMenu, &mi) {
		return nil, lastError("SetMenuInfo")
	}

	m := &Menu{
		hMenu: hMenu,
	}
	m.actions = newActionList(m)

	return m, nil
}

func (m *Menu) Dispose() {
	m.actions.Clear()

	if m.hMenu != 0 {
		win.DestroyMenu(m.hMenu)
		m.hMenu = 0
	}
}

func (m *Menu) IsDisposed() bool {
	return m.hMenu == 0
}

func (m *Menu) Actions() *ActionList {
	return m.actions
}

func (m *Menu) updateItemsWithImageForWindow(window Window) {
	if m.window == nil {
		m.window = window
		defer func() {
			m.window = nil
		}()
	}

	for _, action := range m.actions.actions {
		if action.image != nil {
			m.onActionChanged(action)
		}
		if action.menu != nil {
			action.menu.updateItemsWithImageForWindow(window)
		}
	}
}

func (m *Menu) initMenuItemInfoFromAction(mii *win.MENUITEMINFO, action *Action) {
	mii.CbSize = uint32(unsafe.Sizeof(*mii))
	mii.FMask = win.MIIM_FTYPE | win.MIIM_ID | win.MIIM_STATE | win.MIIM_STRING
	if action.image != nil {
		mii.FMask |= win.MIIM_BITMAP
		dpi := 96
		if m.getDPI != nil {
			dpi = m.getDPI()
		} else if m.window != nil {
			dpi = m.window.DPI()
		} else {
			dpi = screenDPI()
		}
		if bmp, err := iconCache.Bitmap(action.image, dpi); err == nil {
			mii.HbmpItem = bmp.hBmp
		}
	}
	if action.IsSeparator() {
		mii.FType |= win.MFT_SEPARATOR
	} else {
		mii.FType |= win.MFT_STRING
		var text string
		if s := action.shortcut; s.Key != 0 {
			text = fmt.Sprintf("%s\t%s", action.text, s.String())
		} else {
			text = action.text
		}
		mii.DwTypeData = syscall.StringToUTF16Ptr(text)
		mii.Cch = uint32(len([]rune(action.text)))
	}
	mii.WID = uint32(action.id)

	if action.Enabled() {
		mii.FState &^= win.MFS_DISABLED
	} else {
		mii.FState |= win.MFS_DISABLED
	}

	if action.Checkable() {
		mii.FMask |= win.MIIM_CHECKMARKS
	}
	if action.Checked() {
		mii.FState |= win.MFS_CHECKED
	}
	if action.Exclusive() {
		mii.FType |= win.MFT_RADIOCHECK
	}

	menu := action.menu
	if menu != nil {
		mii.FMask |= win.MIIM_SUBMENU
		mii.HSubMenu = menu.hMenu
	}
}

func (m *Menu) handleDefaultState(action *Action) {
	if action.Default() {
		// Unset other default actions before we set this one. Otherwise insertion fails.
		win.SetMenuDefaultItem(m.hMenu, ^uint32(0), false)
		for _, otherAction := range m.actions.actions {
			if otherAction != action {
				otherAction.SetDefault(false)
			}
		}
	}
}

func (m *Menu) onActionChanged(action *Action) error {
	m.handleDefaultState(action)

	if !action.Visible() {
		return nil
	}

	var mii win.MENUITEMINFO

	m.initMenuItemInfoFromAction(&mii, action)

	if !win.SetMenuItemInfo(m.hMenu, uint32(m.actions.indexInObserver(action)), true, &mii) {
		return newError("SetMenuItemInfo failed")
	}

	if action.Default() {
		win.SetMenuDefaultItem(m.hMenu, uint32(m.actions.indexInObserver(action)), true)
	}

	if action.Exclusive() && action.Checked() {
		var first, last int

		index := m.actions.Index(action)

		for i := index; i >= 0; i-- {
			first = i
			if !m.actions.At(i).Exclusive() {
				break
			}
		}

		for i := index; i < m.actions.Len(); i++ {
			last = i
			if !m.actions.At(i).Exclusive() {
				break
			}
		}

		if !win.CheckMenuRadioItem(m.hMenu, uint32(first), uint32(last), uint32(index), win.MF_BYPOSITION) {
			return newError("CheckMenuRadioItem failed")
		}
	}

	return nil
}

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

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

	return m.removeAction(action, true)
}

func (m *Menu) insertAction(action *Action, visibleChanged bool) (err error) {
	m.handleDefaultState(action)

	if !visibleChanged {
		action.addChangedHandler(m)
		defer func() {
			if err != nil {
				action.removeChangedHandler(m)
			}
		}()
	}

	if !action.Visible() {
		return
	}

	index := m.actions.indexInObserver(action)

	var mii win.MENUITEMINFO

	m.initMenuItemInfoFromAction(&mii, action)

	if !win.InsertMenuItem(m.hMenu, uint32(index), true, &mii) {
		return newError("InsertMenuItem failed")
	}

	if action.Default() {
		win.SetMenuDefaultItem(m.hMenu, uint32(m.actions.indexInObserver(action)), true)
	}

	menu := action.menu
	if menu != nil {
		menu.window = m.window
	}

	m.ensureMenuBarRedrawn()

	return
}

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

	if !win.RemoveMenu(m.hMenu, uint32(index), win.MF_BYPOSITION) {
		return lastError("RemoveMenu")
	}

	if !visibleChanged {
		action.removeChangedHandler(m)
	}

	m.ensureMenuBarRedrawn()

	return nil
}

func (m *Menu) ensureMenuBarRedrawn() {
	if m.window != nil {
		if mw, ok := m.window.(*MainWindow); ok && mw != nil {
			win.DrawMenuBar(mw.Handle())
		}
	}
}

func (m *Menu) onInsertedAction(action *Action) error {
	return m.insertAction(action, false)
}

func (m *Menu) onRemovingAction(action *Action) error {
	return m.removeAction(action, false)
}

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

	return nil
}