package widget

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

// Card widget groups title, subtitle with content and a header image
//
// Since: 1.4
type Card struct {
	BaseWidget
	Title, Subtitle string
	Image           *canvas.Image
	Content         fyne.CanvasObject
}

// NewCard creates a new card widget with the specified title, subtitle and content (all optional).
//
// Since: 1.4
func NewCard(title, subtitle string, content fyne.CanvasObject) *Card {
	card := &Card{
		Title:    title,
		Subtitle: subtitle,
		Content:  content,
	}

	card.ExtendBaseWidget(card)
	return card
}

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

	header := canvas.NewText(c.Title, th.Color(theme.ColorNameForeground, v))
	header.TextStyle.Bold = true
	subHeader := canvas.NewText(c.Subtitle, header.Color)

	objects := []fyne.CanvasObject{header, subHeader}
	if c.Image != nil {
		objects = append(objects, c.Image)
	}
	if c.Content != nil {
		objects = append(objects, c.Content)
	}
	r := &cardRenderer{widget.NewShadowingRenderer(objects, widget.CardLevel),
		header, subHeader, c}
	r.applyTheme()
	return r
}

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

// SetContent changes the body of this card to have the specified content.
func (c *Card) SetContent(obj fyne.CanvasObject) {
	c.Content = obj

	c.Refresh()
}

// SetImage changes the image displayed above the title for this card.
func (c *Card) SetImage(img *canvas.Image) {
	c.Image = img

	c.Refresh()
}

// SetSubTitle updates the secondary title for this card.
func (c *Card) SetSubTitle(text string) {
	c.Subtitle = text

	c.Refresh()
}

// SetTitle updates the main title for this card.
func (c *Card) SetTitle(text string) {
	c.Title = text

	c.Refresh()
}

type cardRenderer struct {
	*widget.ShadowingRenderer

	header, subHeader *canvas.Text

	card *Card
}

const (
	cardMediaHeight = 128
)

// Layout the components of the card container.
func (c *cardRenderer) Layout(size fyne.Size) {
	padding := c.card.Theme().Size(theme.SizeNamePadding)
	pos := fyne.NewSquareOffsetPos(padding / 2)
	size = size.Subtract(fyne.NewSquareSize(padding))
	c.LayoutShadow(size, pos)

	if c.card.Image != nil {
		c.card.Image.Move(pos)
		c.card.Image.Resize(fyne.NewSize(size.Width, cardMediaHeight))
		pos.Y += cardMediaHeight
	}

	if c.card.Title != "" || c.card.Subtitle != "" {
		titlePad := padding * 2
		size.Width -= titlePad * 2
		pos.X += titlePad
		pos.Y += titlePad

		if c.card.Title != "" {
			height := c.header.MinSize().Height
			c.header.Move(pos)
			c.header.Resize(fyne.NewSize(size.Width, height))
			pos.Y += height + padding
		}

		if c.card.Subtitle != "" {
			height := c.subHeader.MinSize().Height
			c.subHeader.Move(pos)
			c.subHeader.Resize(fyne.NewSize(size.Width, height))
			pos.Y += height + padding
		}

		size.Width = size.Width + titlePad*2
		pos.X = pos.X - titlePad
		pos.Y += titlePad
	}

	size.Width -= padding * 2
	pos.X += padding
	if c.card.Content != nil {
		height := size.Height - padding*2 - (pos.Y - padding/2) // adjust for content and initial offset
		if c.card.Title != "" || c.card.Subtitle != "" {
			height += padding
			pos.Y -= padding
		}
		c.card.Content.Move(pos.Add(fyne.NewPos(0, padding)))
		c.card.Content.Resize(fyne.NewSize(size.Width, height))
	}
}

// MinSize calculates the minimum size of a card.
// This is based on the contained text, image and content.
func (c *cardRenderer) MinSize() fyne.Size {
	hasHeader := c.card.Title != ""
	hasSubHeader := c.card.Subtitle != ""
	hasImage := c.card.Image != nil
	hasContent := c.card.Content != nil

	padding := c.card.Theme().Size(theme.SizeNamePadding)
	if !hasHeader && !hasSubHeader && !hasContent { // just image, or nothing
		if c.card.Image == nil {
			return fyne.NewSize(padding, padding) // empty, just space for border
		}
		return fyne.NewSize(c.card.Image.MinSize().Width+padding, cardMediaHeight+padding)
	}

	min := fyne.NewSize(padding, padding)
	if hasImage {
		min = fyne.NewSize(min.Width, min.Height+cardMediaHeight)
	}

	if hasHeader || hasSubHeader {
		titlePad := padding * 2
		min = min.Add(fyne.NewSize(0, titlePad*2))
		if hasHeader {
			headerMin := c.header.MinSize()
			min = fyne.NewSize(fyne.Max(min.Width, headerMin.Width+titlePad*2+padding),
				min.Height+headerMin.Height)
			if hasSubHeader {
				min.Height += padding
			}
		}
		if hasSubHeader {
			subHeaderMin := c.subHeader.MinSize()
			min = fyne.NewSize(fyne.Max(min.Width, subHeaderMin.Width+titlePad*2+padding),
				min.Height+subHeaderMin.Height)
		}
	}

	if hasContent {
		contentMin := c.card.Content.MinSize()
		min = fyne.NewSize(fyne.Max(min.Width, contentMin.Width+padding*3),
			min.Height+contentMin.Height+padding*2)
	}

	return min
}

func (c *cardRenderer) Refresh() {
	c.header.Text = c.card.Title
	c.header.Refresh()
	c.subHeader.Text = c.card.Subtitle
	c.subHeader.Refresh()

	objects := []fyne.CanvasObject{c.header, c.subHeader}
	if c.card.Image != nil {
		objects = append(objects, c.card.Image)
	}
	if c.card.Content != nil {
		objects = append(objects, c.card.Content)
	}
	c.ShadowingRenderer.SetObjects(objects)

	c.applyTheme()
	c.Layout(c.card.Size())
	c.ShadowingRenderer.RefreshShadow()
	canvas.Refresh(c.card.super())
}

// applyTheme updates this button to match the current theme
func (c *cardRenderer) applyTheme() {
	th := c.card.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	if c.header != nil {
		c.header.TextSize = th.Size(theme.SizeNameHeadingText)
		c.header.Color = th.Color(theme.ColorNameForeground, v)
	}
	if c.subHeader != nil {
		c.subHeader.TextSize = th.Size(theme.SizeNameText)
		c.subHeader.Color = th.Color(theme.ColorNameForeground, v)
	}
	if c.card.Content != nil {
		c.card.Content.Refresh()
	}
}
