package widget

import (
	"fmt"
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/data/binding"
	"fyne.io/fyne/v2/driver/desktop"
	"fyne.io/fyne/v2/internal/widget"
	"fyne.io/fyne/v2/theme"
)

// Check widget has a text label and a checked (or unchecked) icon and triggers an event func when toggled
type Check struct {
	DisableableWidget
	Text    string
	Checked bool

	OnChanged func(bool) `json:"-"`

	focused bool
	hovered bool

	binder basicBinder

	minSize fyne.Size // cached for hover/tap position calculations
}

// NewCheck creates a new check widget with the set label and change handler
func NewCheck(label string, changed func(bool)) *Check {
	c := &Check{
		Text:      label,
		OnChanged: changed,
	}

	c.ExtendBaseWidget(c)
	return c
}

// NewCheckWithData returns a check widget connected with the specified data source.
//
// Since: 2.0
func NewCheckWithData(label string, data binding.Bool) *Check {
	check := NewCheck(label, nil)
	check.Bind(data)

	return check
}

// Bind connects the specified data source to this Check.
// The current value will be displayed and any changes in the data will cause the widget to update.
// User interactions with this Check will set the value into the data source.
//
// Since: 2.0
func (c *Check) Bind(data binding.Bool) {
	c.binder.SetCallback(c.updateFromData)
	c.binder.Bind(data)

	c.OnChanged = func(_ bool) {
		c.binder.CallWithData(c.writeData)
	}
}

// SetChecked sets the checked state and refreshes widget
func (c *Check) SetChecked(checked bool) {
	c.propertyLock.Lock()
	if checked == c.Checked {
		c.propertyLock.Unlock()
		return
	}

	c.Checked = checked
	onChanged := c.OnChanged
	c.propertyLock.Unlock()

	if onChanged != nil {
		onChanged(checked)
	}

	c.Refresh()
}

// Hide this widget, if it was previously visible
func (c *Check) Hide() {
	if c.focused {
		c.FocusLost()
		impl := c.super()

		if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
			c.Focus(nil)
		}
	}

	c.BaseWidget.Hide()
}

// MouseIn is called when a desktop pointer enters the widget
func (c *Check) MouseIn(me *desktop.MouseEvent) {
	c.MouseMoved(me)
}

// MouseOut is called when a desktop pointer exits the widget
func (c *Check) MouseOut() {
	if c.hovered {
		c.hovered = false
		c.Refresh()
	}
}

// MouseMoved is called when a desktop pointer hovers over the widget
func (c *Check) MouseMoved(me *desktop.MouseEvent) {
	if c.Disabled() {
		return
	}

	oldHovered := c.hovered

	// only hovered if cached minSize has not been initialized (test code)
	// or the pointer is within the "active" area of the widget (its minSize)
	c.hovered = c.minSize.IsZero() ||
		(me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height)

	if oldHovered != c.hovered {
		c.Refresh()
	}
}

// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (c *Check) Tapped(pe *fyne.PointEvent) {
	if c.Disabled() {
		return
	}
	if !c.minSize.IsZero() &&
		(pe.Position.X > c.minSize.Width || pe.Position.Y > c.minSize.Height) {
		// tapped outside the active area of the widget
		return
	}

	if !c.focused {
		focusIfNotMobile(c.super())
	}
	c.SetChecked(!c.Checked)
}

// MinSize returns the size that this widget should not shrink below
func (c *Check) MinSize() fyne.Size {
	c.ExtendBaseWidget(c)
	c.minSize = c.BaseWidget.MinSize()
	return c.minSize
}

// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (c *Check) CreateRenderer() fyne.WidgetRenderer {
	th := c.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	c.ExtendBaseWidget(c)
	bg := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButtonFill))
	icon := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButton))

	c.propertyLock.RLock()
	defer c.propertyLock.RUnlock()
	text := canvas.NewText(c.Text, th.Color(theme.ColorNameForeground, v))
	text.Alignment = fyne.TextAlignLeading

	focusIndicator := canvas.NewCircle(th.Color(theme.ColorNameBackground, v))
	r := &checkRenderer{
		widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}),
		bg,
		icon,
		text,
		focusIndicator,
		c,
	}
	r.applyTheme(th, v)
	r.updateLabel()
	r.updateResource(th)
	r.updateFocusIndicator(th, v)
	return r
}

// FocusGained is called when the Check has been given focus.
func (c *Check) FocusGained() {
	if c.Disabled() {
		return
	}
	c.focused = true

	c.Refresh()
}

// FocusLost is called when the Check has had focus removed.
func (c *Check) FocusLost() {
	c.focused = false

	c.Refresh()
}

// TypedRune receives text input events when the Check is focused.
func (c *Check) TypedRune(r rune) {
	if c.Disabled() {
		return
	}
	if r == ' ' {
		c.SetChecked(!c.Checked)
	}
}

// TypedKey receives key input events when the Check is focused.
func (c *Check) TypedKey(key *fyne.KeyEvent) {}

// SetText sets the text of the Check
//
// Since: 2.4
func (c *Check) SetText(text string) {
	c.propertyLock.Lock()
	c.Text = text
	c.propertyLock.Unlock()
	c.Refresh()
}

// Unbind disconnects any configured data source from this Check.
// The current value will remain at the last value of the data source.
//
// Since: 2.0
func (c *Check) Unbind() {
	c.OnChanged = nil
	c.binder.Unbind()
}

func (c *Check) updateFromData(data binding.DataItem) {
	if data == nil {
		return
	}
	boolSource, ok := data.(binding.Bool)
	if !ok {
		return
	}
	val, err := boolSource.Get()
	if err != nil {
		fyne.LogError("Error getting current data value", err)
		return
	}
	c.SetChecked(val) // if val != c.Checked, this will call updateFromData again, but only once
}

func (c *Check) writeData(data binding.DataItem) {
	if data == nil {
		return
	}
	boolTarget, ok := data.(binding.Bool)
	if !ok {
		return
	}
	currentValue, err := boolTarget.Get()
	if err != nil {
		return
	}
	if currentValue != c.Checked {
		err := boolTarget.Set(c.Checked)
		if err != nil {
			fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", c.Checked), err)
		}
	}
}

type checkRenderer struct {
	widget.BaseRenderer
	bg, icon       *canvas.Image
	label          *canvas.Text
	focusIndicator *canvas.Circle
	check          *Check
}

// MinSize calculates the minimum size of a check.
// This is based on the contained text, the check icon and a standard amount of padding added.
func (c *checkRenderer) MinSize() fyne.Size {
	th := c.check.Theme()

	pad4 := th.Size(theme.SizeNameInnerPadding) * 2
	min := c.label.MinSize().Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+pad4, pad4))

	c.check.propertyLock.RLock()
	text := c.check.Text
	c.check.propertyLock.RUnlock()
	if text != "" {
		min.Add(fyne.NewSize(th.Size(theme.SizeNamePadding), 0))
	}

	return min
}

// Layout the components of the check widget
func (c *checkRenderer) Layout(size fyne.Size) {
	th := c.check.Theme()
	innerPadding := th.Size(theme.SizeNameInnerPadding)
	borderSize := th.Size(theme.SizeNameInputBorder)
	iconInlineSize := th.Size(theme.SizeNameInlineIcon)

	focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding)
	c.focusIndicator.Resize(focusIndicatorSize)
	c.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2))

	xOff := focusIndicatorSize.Width + borderSize*2
	labelSize := size.SubtractWidthHeight(xOff, 0)
	c.label.Resize(labelSize)
	c.label.Move(fyne.NewPos(xOff, 0))

	iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2)
	iconSize := fyne.NewSquareSize(iconInlineSize)
	c.bg.Move(iconPos)
	c.bg.Resize(iconSize)
	c.icon.Move(iconPos)
	c.icon.Resize(iconSize)
}

// applyTheme updates this Check to the current theme
func (c *checkRenderer) applyTheme(th fyne.Theme, v fyne.ThemeVariant) {
	c.label.Color = th.Color(theme.ColorNameForeground, v)
	c.label.TextSize = th.Size(theme.SizeNameText)
	if c.check.disabled.Load() {
		c.label.Color = th.Color(theme.ColorNameDisabled, v)
	}
}

func (c *checkRenderer) Refresh() {
	th := c.check.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	c.check.propertyLock.RLock()
	c.applyTheme(th, v)
	c.updateLabel()
	c.updateResource(th)
	c.updateFocusIndicator(th, v)
	c.check.propertyLock.RUnlock()
	canvas.Refresh(c.check.super())
}

// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateLabel() {
	c.label.Text = c.check.Text
}

// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateResource(th fyne.Theme) {
	res := theme.NewThemedResource(th.Icon(theme.IconNameCheckButton))
	res.ColorName = theme.ColorNameInputBorder
	bgRes := theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonFill))
	bgRes.ColorName = theme.ColorNameInputBackground

	if c.check.Checked {
		res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonChecked))
		res.ColorName = theme.ColorNamePrimary
		bgRes.ColorName = theme.ColorNameBackground
	}
	if c.check.disabled.Load() {
		if c.check.Checked {
			res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
		}
		res.ColorName = theme.ColorNameDisabled
		bgRes.ColorName = theme.ColorNameBackground
	}
	c.icon.Resource = res
	c.icon.Refresh()
	c.bg.Resource = bgRes
	c.bg.Refresh()
}

// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateFocusIndicator(th fyne.Theme, v fyne.ThemeVariant) {
	if c.check.disabled.Load() {
		c.focusIndicator.FillColor = color.Transparent
	} else if c.check.focused {
		c.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v)
	} else if c.check.hovered {
		c.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v)
	} else {
		c.focusIndicator.FillColor = color.Transparent
	}
}

func focusIfNotMobile(w fyne.Widget) {
	if !fyne.CurrentDevice().IsMobile() {
		if c := fyne.CurrentApp().Driver().CanvasForObject(w); c != nil {
			c.Focus(w.(fyne.Focusable))
		}
	}

}
