package widget

import (
	"fyne.io/fyne"
	"fyne.io/fyne/canvas"
	"fyne.io/fyne/internal/widget"
	"fyne.io/fyne/layout"
	"fyne.io/fyne/theme"
)

var _ fyne.Widget = (*Menu)(nil)
var _ fyne.Tappable = (*Menu)(nil)

// Menu is a widget for displaying a fyne.Menu.
type Menu struct {
	widget.Base
	Items       []fyne.CanvasObject
	OnDismiss   func()
	activeItem  *menuItem
	customSized bool
}

// NewMenu creates a new Menu.
func NewMenu(menu *fyne.Menu) *Menu {
	items := make([]fyne.CanvasObject, len(menu.Items))
	m := &Menu{Items: items}
	for i, item := range menu.Items {
		if item.IsSeparator {
			items[i] = newMenuItemSeparator()
		} else {
			items[i] = newMenuItem(item, m, m.activateChild)
		}
	}
	return m
}

// CreateRenderer returns a new renderer for the menu.
// Implements: fyne.Widget
func (m *Menu) CreateRenderer() fyne.WidgetRenderer {
	box := newMenuBox(m.Items)
	scroll := NewVScrollContainer(box)
	scroll.SetMinSize(box.MinSize())
	objects := []fyne.CanvasObject{scroll}
	for _, i := range m.Items {
		if item, ok := i.(*menuItem); ok && item.Child() != nil {
			objects = append(objects, item.Child())
		}
	}

	return &menuRenderer{
		widget.NewShadowingRenderer(objects, widget.MenuLevel),
		box,
		m,
		scroll,
	}
}

// DeactivateChild deactivates the active child menu.
func (m *Menu) DeactivateChild() {
	if m.activeItem != nil {
		m.activeItem.Child().Hide()
		m.activeItem = nil
	}
}

// Hide hides the menu.
// Implements: fyne.Widget
func (m *Menu) Hide() {
	widget.HideWidget(&m.Base, m)
}

// MinSize returns the minimal size of the menu.
// Implements: fyne.Widget
func (m *Menu) MinSize() fyne.Size {
	return widget.MinSizeOf(m)
}

// Move sets the position of the widget relative to its parent.
// Implements: fyne.Widget
func (m *Menu) Move(pos fyne.Position) {
	widget.MoveWidget(&m.Base, m, pos)
}

// Refresh triggers a redraw of the menu.
// Implements: fyne.Widget
func (m *Menu) Refresh() {
	widget.RefreshWidget(m)
}

// Resize has no effect because menus are always displayed with their minimal size.
// Implements: fyne.Widget
func (m *Menu) Resize(size fyne.Size) {
	widget.ResizeWidget(&m.Base, m, size)
}

// Show makes the menu visible.
// Implements: fyne.Widget
func (m *Menu) Show() {
	widget.ShowWidget(&m.Base, m)
}

// Tapped catches taps on separators and the menu background. It doesn’t perform any action.
// Implements: fyne.Tappable
func (m *Menu) Tapped(*fyne.PointEvent) {
	// Hit a separator or padding -> do nothing.
}

// Dismiss dismisses the menu by dismissing and hiding the active child and performing OnDismiss.
func (m *Menu) Dismiss() {
	if m.activeItem != nil {
		defer m.activeItem.Child().Dismiss()
		m.DeactivateChild()
	}
	if m.OnDismiss != nil {
		m.OnDismiss()
	}
}

func (m *Menu) activateChild(item *menuItem) {
	if item.Child() != nil {
		item.Child().DeactivateChild()
	}
	if m.activeItem == item {
		return
	}

	m.DeactivateChild()
	if item.Child() == nil {
		return
	}

	m.activeItem = item
	item.Child().Show()
	m.Refresh()
}

type menuRenderer struct {
	*widget.ShadowingRenderer
	box    *menuBox
	m      *Menu
	scroll *ScrollContainer
}

func (r *menuRenderer) Layout(s fyne.Size) {
	minSize := r.MinSize()
	var boxSize fyne.Size
	if r.m.customSized {
		boxSize = minSize.Max(s)
	} else {
		boxSize = minSize
	}
	scrollSize := boxSize
	if c := fyne.CurrentApp().Driver().CanvasForObject(r.m); c != nil {
		ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m)
		if ah := c.Size().Height - ap.Y; ah < boxSize.Height {
			scrollSize = fyne.NewSize(boxSize.Width, ah)
		}
	}
	if scrollSize != r.m.Size() {
		r.m.Resize(scrollSize)
		return
	}

	r.LayoutShadow(scrollSize, fyne.NewPos(0, 0))
	r.scroll.Resize(scrollSize)
	r.box.Resize(boxSize)
	r.layoutActiveChild()
}

func (r *menuRenderer) MinSize() fyne.Size {
	return r.box.MinSize()
}

func (r *menuRenderer) Refresh() {
	r.layoutActiveChild()
	canvas.Refresh(r.m)
}

func (r *menuRenderer) layoutActiveChild() {
	item := r.m.activeItem
	if item == nil {
		return
	}

	if item.Child().Size().IsZero() {
		item.Child().Resize(item.Child().MinSize())
	}

	itemSize := item.Size()
	cp := fyne.NewPos(itemSize.Width, item.Position().Y)
	d := fyne.CurrentApp().Driver()
	c := d.CanvasForObject(item)
	if c != nil {
		absPos := d.AbsolutePositionForObject(item)
		childSize := item.Child().Size()
		if absPos.X+itemSize.Width+childSize.Width > c.Size().Width {
			if absPos.X-childSize.Width >= 0 {
				cp.X = -childSize.Width
			} else {
				cp.X = c.Size().Width - absPos.X - childSize.Width
			}
		}
		requiredHeight := childSize.Height - theme.Padding()
		availableHeight := c.Size().Height - absPos.Y
		missingHeight := requiredHeight - availableHeight
		if missingHeight > 0 {
			cp.Y -= missingHeight
		}
	}
	item.Child().Move(cp)
}

type menuBox struct {
	BaseWidget
	items []fyne.CanvasObject
}

var _ fyne.Widget = (*menuBox)(nil)

func newMenuBox(items []fyne.CanvasObject) *menuBox {
	b := &menuBox{items: items}
	b.ExtendBaseWidget(b)
	return b
}

func (b *menuBox) CreateRenderer() fyne.WidgetRenderer {
	cont := fyne.NewContainerWithLayout(layout.NewVBoxLayout(), b.items...)
	return &menuBoxRenderer{
		BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{cont}),
		b:            b,
		cont:         cont,
	}
}

type menuBoxRenderer struct {
	widget.BaseRenderer
	b    *menuBox
	cont *fyne.Container
}

var _ fyne.WidgetRenderer = (*menuBoxRenderer)(nil)

func (r *menuBoxRenderer) Layout(size fyne.Size) {
	r.cont.Resize(fyne.NewSize(size.Width, size.Height+2*theme.Padding()))
	r.cont.Move(fyne.NewPos(0, theme.Padding()))
}

func (r *menuBoxRenderer) MinSize() fyne.Size {
	return r.cont.MinSize().Add(fyne.NewSize(0, 2*theme.Padding()))
}

func (r *menuBoxRenderer) Refresh() {
	canvas.Refresh(r.b)
}
