package widgets

import (
	"context"
	"fmt"
	"sync"

	"github.com/dweymouth/supersonic/backend/mediaprovider"
	"github.com/dweymouth/supersonic/sharedutil"
	myTheme "github.com/dweymouth/supersonic/ui/theme"
	"github.com/dweymouth/supersonic/ui/util"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

const batchFetchSize = 6

type BatchingIterator[M any] struct {
	iter mediaprovider.MediaIterator[M]
}

func NewBatchingIterator[M any](iter mediaprovider.MediaIterator[M]) BatchingIterator[M] {
	return BatchingIterator[M]{iter}
}

func (b *BatchingIterator[M]) NextN(n int) []*M {
	results := make([]*M, 0, n)
	i := 0
	for i < n {
		value := b.iter.Next()
		if value == nil {
			break
		}
		results = append(results, value)
		i++
	}
	return results
}

type GridViewIterator interface {
	NextN(int) []GridViewItemModel
}

type gridViewAlbumIterator struct {
	iter BatchingIterator[mediaprovider.Album]
}

func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel {
	albums := g.iter.NextN(n)
	return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel {
		return GridViewItemModel{
			Name:         al.Name,
			ID:           al.ID,
			CoverArtID:   al.CoverArtID,
			Secondary:    al.ArtistNames,
			SecondaryIDs: al.ArtistIDs,
		}
	})
}

func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator {
	return gridViewAlbumIterator{iter: NewBatchingIterator(iter)}
}

type gridViewArtistIterator struct {
	iter BatchingIterator[mediaprovider.Artist]
}

func (g gridViewArtistIterator) NextN(n int) []GridViewItemModel {
	artists := g.iter.NextN(n)
	return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) GridViewItemModel {
		albumsLabel := "albums"
		if ar.AlbumCount == 1 {
			albumsLabel = "album"
		}
		return GridViewItemModel{
			Name:       ar.Name,
			ID:         ar.ID,
			CoverArtID: ar.CoverArtID,
			Secondary:  []string{fmt.Sprintf("%d %s", ar.AlbumCount, albumsLabel)},
		}
	})
}

func NewGridViewArtistIterator(iter mediaprovider.ArtistIterator) GridViewIterator {
	return gridViewArtistIterator{iter: NewBatchingIterator(iter)}
}

type GridView struct {
	widget.BaseWidget

	stateMutex  sync.RWMutex
	fetchCancel context.CancelFunc
	GridViewState

	grid               *disabledGridWrap
	loadingDots        *LoadingDots
	menu               *widget.PopUpMenu
	menuGridViewItemId string
	itemForIndex       map[int]*GridViewItem
	itemWidth          float32
	numColsCached      int
	shareMenuItem      *fyne.MenuItem
}

type GridViewState struct {
	items        []GridViewItemModel
	iter         GridViewIterator
	imageFetcher util.ImageFetcher
	Placeholder  fyne.Resource
	highestShown int
	done         bool

	DisableSharing bool

	OnPlay              func(id string, shuffle bool)
	OnPlayNext          func(id string)
	OnAddToQueue        func(id string)
	OnAddToPlaylist     func(id string)
	OnDownload          func(id string)
	OnShare             func(id string)
	OnShowItemPage      func(id string)
	OnShowSecondaryPage func(id string)

	scrollPos float32
}

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

func newGridView() *GridView {
	g := &GridView{
		loadingDots:  NewLoadingDots(),
		itemWidth:    NewGridViewItem(nil).MinSize().Width,
		itemForIndex: make(map[int]*GridViewItem),
	}
	return g
}

func NewFixedGridView(items []GridViewItemModel, fetch util.ImageFetcher, placeholder fyne.Resource) *GridView {
	g := newGridView()
	g.GridViewState = GridViewState{
		items:        items,
		done:         true,
		imageFetcher: fetch,
		Placeholder:  placeholder,
	}
	g.ExtendBaseWidget(g)
	g.createGridWrap()
	return g
}

func NewGridView(iter GridViewIterator, fetch util.ImageFetcher, placeholder fyne.Resource) *GridView {
	g := newGridView()
	g.GridViewState = GridViewState{
		iter:         iter,
		imageFetcher: fetch,
		Placeholder:  placeholder,
	}
	g.ExtendBaseWidget(g)
	g.createGridWrap()
	g.loadingDots.Start()

	// fetch initial items
	g.checkFetchMoreItems(36)
	return g
}

func (g *GridView) SaveToState() *GridViewState {
	g.stateMutex.RLock()
	s := g.GridViewState
	g.stateMutex.RUnlock()
	s.scrollPos = g.grid.GetScrollOffset()
	return &s
}

func NewGridViewFromState(state *GridViewState) *GridView {
	g := newGridView()
	g.GridViewState = *state
	g.ExtendBaseWidget(g)
	g.createGridWrap()
	g.Refresh() // needed to initialize the widget
	g.grid.ScrollToOffset(state.scrollPos)
	return g
}

func (g *GridView) Clear() {
	g.stateMutex.Lock()
	defer g.stateMutex.Unlock()
	g.cancelFetch()
	g.items = nil
	g.done = true
}

func (g *GridView) Reset(iter GridViewIterator) {
	g.stateMutex.Lock()
	g.cancelFetch()
	g.items = nil
	g.itemForIndex = make(map[int]*GridViewItem)
	g.done = false
	g.highestShown = 0
	g.iter = iter
	g.stateMutex.Unlock()
	g.checkFetchMoreItems(36)
	g.loadingDots.Start()
	g.Refresh()
}

func (g *GridView) ResetFromState(state *GridViewState) {
	g.stateMutex.Lock()
	g.cancelFetch()
	g.GridViewState = *state
	g.itemForIndex = make(map[int]*GridViewItem)
	g.stateMutex.Unlock()
	g.grid.Refresh()
	g.grid.ScrollToOffset(state.scrollPos)
}

func (g *GridView) ResetFixed(items []GridViewItemModel) {
	g.stateMutex.Lock()
	g.cancelFetch()
	g.items = items
	g.itemForIndex = make(map[int]*GridViewItem)
	g.done = true
	g.highestShown = 0
	g.iter = nil
	g.stateMutex.Unlock()
	g.Refresh()
}

func (g *GridView) GetScrollOffset() float32 {
	return g.grid.GetScrollOffset()
}

func (g *GridView) ScrollToOffset(offs float32) {
	g.grid.ScrollToOffset(offs)
}

func (g *GridView) Resize(size fyne.Size) {
	g.numColsCached = -1
	g.BaseWidget.Resize(size)
}

var _ fyne.Tappable = (*GridView)(nil)

func (g *GridView) Tapped(*fyne.PointEvent) {
	fyne.CurrentApp().Driver().CanvasForObject(g).Unfocus()
}

func (g *GridView) createGridWrap() {
	g.grid = NewDisabledGridWrap(
		g.lenItems,
		g.createNewItemCard,
		// update func
		func(itemID widget.GridWrapItemID, obj fyne.CanvasObject) {
			ac := obj.(*GridViewItem)
			g.doUpdateItemCard(int(itemID), ac)
		},
	)
}

func (g *GridView) createNewItemCard() fyne.CanvasObject {
	card := NewGridViewItem(g.Placeholder)
	card.ItemIndex = -1
	card.ImgLoader = util.NewThumbnailLoader(g.imageFetcher, card.Cover.SetImage)
	card.ImgLoader.OnBeforeLoad = func() { card.Cover.SetImage(nil) }
	card.OnPlay = func() { g.onPlay(card.ItemID(), false) }
	card.OnShowSecondaryPage = func(id string) {
		if g.OnShowSecondaryPage != nil {
			g.OnShowSecondaryPage(id)
		}
	}
	card.OnShowItemPage = func() {
		if g.OnShowItemPage != nil {
			g.OnShowItemPage(card.ItemID())
		}
	}
	card.OnShowContextMenu = func(p fyne.Position) {
		g.showContextMenu(card, p)
	}
	card.OnFocusNeighbor = func(neighbor int) {
		focusIndex := -1
		switch neighbor {
		case 0: // left
			focusIndex = card.ItemIndex - 1
		case 1: // right
			focusIndex = card.ItemIndex + 1
		case 2: // up
			focusIndex = card.ItemIndex - g.grid.ColumnCount()
		case 3: // down
			focusIndex = card.ItemIndex + g.grid.ColumnCount()
		}
		if focusIndex >= 0 && focusIndex < g.lenItems() {
			g.grid.ScrollTo(focusIndex)
			g.stateMutex.RLock()
			if item, ok := g.itemForIndex[focusIndex]; ok {
				fyne.CurrentApp().Driver().CanvasForObject(g).Focus(item)
			}
			g.stateMutex.RUnlock()
		}
	}
	return card
}

func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) {
	if itemIdx > g.highestShown {
		g.highestShown = itemIdx
	}
	var item GridViewItemModel
	g.stateMutex.Lock()
	// itemIdx can rarely be out of range if the data is being updated
	// as the view is requested to refresh
	if itemIdx < len(g.items) {
		item = g.items[itemIdx]
	}
	// update itemForIndex map
	if c, ok := g.itemForIndex[card.ItemIndex]; ok && c == card {
		delete(g.itemForIndex, card.ItemIndex)
	}
	card.ItemIndex = itemIdx
	g.itemForIndex[itemIdx] = card
	card.Cover.Im.CenterIcon = g.Placeholder
	if !card.NeedsUpdate(item) && card.ItemIndex == itemIdx {
		// nothing to do
		g.stateMutex.Unlock()
		return
	}
	g.stateMutex.Unlock()
	card.Update(item)
	card.ImgLoader.Load(item.CoverArtID)

	// if user has scrolled near the bottom, fetch more
	if itemIdx > g.lenItems()-10 {
		g.checkFetchMoreItems(20)
	}
}

func (g *GridView) lenItems() int {
	g.stateMutex.RLock()
	defer g.stateMutex.RUnlock()
	return len(g.items)
}

// fetches at least count more items if fetch not in progress and not done
// acquires stateMutex for atomicity
func (g *GridView) checkFetchMoreItems(count int) {
	g.stateMutex.Lock()
	defer g.stateMutex.Unlock()
	if g.done || g.fetchCancel != nil {
		return // done, or fetch already in progress
	}
	if g.iter == nil {
		g.done = true
		return
	}
	ctx, cancel := context.WithCancel(context.Background())
	g.fetchCancel = cancel
	go func() {
		// keep repeating the fetch task as long as the user
		// has scrolled near the bottom
		for !g.done && g.highestShown >= g.lenItems()-10 {
			n := 0
			for !g.done && n < count {
				items := g.iter.NextN(batchFetchSize)
				select {
				case <-ctx.Done():
					return
				default:
					g.stateMutex.Lock()
					g.items = append(g.items, items...)
					g.stateMutex.Unlock()
					g.loadingDots.Stop()
					if len(items) < batchFetchSize {
						g.done = true
					}
					n += len(items)
					if len(items) > 0 {
						g.grid.Refresh()
					}
				}
			}
		}
		// call cancelfunc to release Context resources
		g.stateMutex.Lock()
		g.cancelFetch()
		g.stateMutex.Unlock()
	}()
}

// must be called with stateMutex locked for writing
func (g *GridView) cancelFetch() {
	if g.fetchCancel != nil {
		g.fetchCancel()
		g.fetchCancel = nil
	}
}

func (g *GridView) showContextMenu(card *GridViewItem, pos fyne.Position) {
	g.menuGridViewItemId = card.ItemID()
	if g.menu == nil {
		play := fyne.NewMenuItem("Play", func() { g.onPlay(g.menuGridViewItemId, false) })
		play.Icon = theme.MediaPlayIcon()
		shuffle := fyne.NewMenuItem("Shuffle", func() { g.onPlay(g.menuGridViewItemId, true) })
		shuffle.Icon = myTheme.ShuffleIcon
		queueNext := fyne.NewMenuItem("Play next", func() {
			if g.OnPlayNext != nil {
				g.OnPlayNext(g.menuGridViewItemId)
			}
		})
		queueNext.Icon = myTheme.PlayNextIcon
		queue := fyne.NewMenuItem("Add to queue", func() {
			if g.OnAddToQueue != nil {
				g.OnAddToQueue(g.menuGridViewItemId)
			}
		})
		queue.Icon = theme.ContentAddIcon()
		playlist := fyne.NewMenuItem("Add to playlist...", func() {
			if g.OnAddToPlaylist != nil {
				g.OnAddToPlaylist(g.menuGridViewItemId)
			}
		})
		playlist.Icon = myTheme.PlaylistIcon
		download := fyne.NewMenuItem("Download...", func() {
			if g.OnDownload != nil {
				g.OnDownload(g.menuGridViewItemId)
			}
		})
		download.Icon = theme.DownloadIcon()
		g.shareMenuItem = fyne.NewMenuItem("Share...", func() {
			g.OnShare(g.menuGridViewItemId)
		})
		g.shareMenuItem.Icon = myTheme.ShareIcon
		g.menu = widget.NewPopUpMenu(fyne.NewMenu("", play, shuffle, queueNext, queue, playlist, download, g.shareMenuItem),
			fyne.CurrentApp().Driver().CanvasForObject(g))
	}
	g.shareMenuItem.Disabled = g.DisableSharing
	g.menu.ShowAtPosition(pos)
}

func (g *GridView) onPlay(itemID string, shuffle bool) {
	if g.OnPlay != nil {
		g.OnPlay(itemID, shuffle)
	}
}

func (g *GridView) CreateRenderer() fyne.WidgetRenderer {
	return widget.NewSimpleRenderer(container.NewStack(
		g.grid, container.NewCenter(g.loadingDots),
	))
}

// a disabled widget is not considered focusable by the focus manager
type disabledGridWrap struct {
	widget.GridWrap
}

func NewDisabledGridWrap(len func() int, create func() fyne.CanvasObject, update func(widget.GridWrapItemID, fyne.CanvasObject)) *disabledGridWrap {
	g := &disabledGridWrap{
		GridWrap: widget.GridWrap{
			Length:     len,
			CreateItem: create,
			UpdateItem: update,
		},
	}
	g.ExtendBaseWidget(g)
	return g
}

var _ fyne.Disableable = (*disabledGridWrap)(nil)

func (g *disabledGridWrap) Disabled() bool { return true }

func (g *disabledGridWrap) Disable() {}

func (g *disabledGridWrap) Enable() {}
