package mediaprovider

import (
	"image"
	"io"
	"net/url"
	"strings"

	"github.com/deluan/sanitize"
)

type MediaIterator[M any] interface {
	Next() *M
}

type ArtistIterator = MediaIterator[Artist]
type AlbumIterator = MediaIterator[Album]
type TrackIterator = MediaIterator[Track]

type MediaFilter[M, F any] interface {
	Options() F
	SetOptions(F)
	Clone() MediaFilter[M, F]
	IsNil() bool
	Matches(*M) bool
}

type AlbumFilter = MediaFilter[Album, AlbumFilterOptions]

type AlbumFilterOptions struct {
	MinYear int
	MaxYear int      // 0 == unset/match any
	Genres  []string // len(0) == unset/match any

	ExcludeFavorited   bool // mut. exc. with ExcludeUnfavorited
	ExcludeUnfavorited bool // mut. exc. with ExcludeFavorited
}

// Clone returns a deep copy of the filter options
func (o AlbumFilterOptions) Clone() AlbumFilterOptions {
	genres := make([]string, len(o.Genres))
	copy(genres, o.Genres)
	return AlbumFilterOptions{
		MinYear:            o.MinYear,
		MaxYear:            o.MaxYear,
		Genres:             genres,
		ExcludeFavorited:   o.ExcludeFavorited,
		ExcludeUnfavorited: o.ExcludeUnfavorited,
	}
}

type albumFilter struct {
	options AlbumFilterOptions
}

func NewAlbumFilter(options AlbumFilterOptions) *albumFilter {
	return &albumFilter{options}
}

func (a albumFilter) Options() AlbumFilterOptions {
	return a.options
}

func (a *albumFilter) SetOptions(options AlbumFilterOptions) {
	a.options = options
}

// Clone returns a deep copy of the filter
func (a albumFilter) Clone() AlbumFilter {
	return NewAlbumFilter(a.options.Clone())
}

// Returns true if the filter is the nil filter - i.e. matches everything
func (a albumFilter) IsNil() bool {
	return a.options.MinYear == 0 && a.options.MaxYear == 0 &&
		len(a.options.Genres) == 0 &&
		!a.options.ExcludeFavorited && !a.options.ExcludeUnfavorited
}

func (f albumFilter) Matches(album *Album) bool {
	if album == nil {
		return false
	}
	if f.options.ExcludeFavorited && album.Favorite {
		return false
	}
	if f.options.ExcludeUnfavorited && !album.Favorite {
		return false
	}
	if y := album.Year; y < f.options.MinYear || (f.options.MaxYear > 0 && y > f.options.MaxYear) {
		return false
	}
	if len(f.options.Genres) == 0 {
		return true
	}
	return genresMatch(f.options.Genres, album.Genres)
}

type ArtistFilter = MediaFilter[Artist, ArtistFilterOptions]

type ArtistFilterOptions struct {
	SearchQuery string
}

// Clone returns a deep copy of the filter options
func (o ArtistFilterOptions) Clone() ArtistFilterOptions {
	return ArtistFilterOptions{
		SearchQuery: o.SearchQuery,
	}
}

type artistFilter struct {
	options ArtistFilterOptions
}

func NewArtistFilter(options ArtistFilterOptions) *artistFilter {
	return &artistFilter{options}
}

func (a artistFilter) Options() ArtistFilterOptions {
	return a.options
}

func (a *artistFilter) SetOptions(o ArtistFilterOptions) {
	a.options = o
}

// Clone returns a deep copy of the filter
func (a artistFilter) Clone() ArtistFilter {
	return NewArtistFilter(a.options.Clone())
}

// Returns true if the filter is the nil filter - i.e. matches everything
func (a artistFilter) IsNil() bool {
	return a.options.SearchQuery == ""
}

func (f artistFilter) Matches(artist *Artist) bool {
	if artist == nil {
		return false
	}
	if f.options.SearchQuery != "" && !strings.Contains(
		sanitize.Accents(strings.ToLower(artist.Name)),
		sanitize.Accents(strings.ToLower(f.options.SearchQuery)),
	) {
		return false
	}
	return true
}

type RatingFavoriteParameters struct {
	AlbumIDs  []string
	ArtistIDs []string
	TrackIDs  []string
}

type Favorites struct {
	Albums  []*Album
	Artists []*Artist
	Tracks  []*Track
}

type LoginResponse struct {
	Error       error
	IsAuthError bool
}

type Server interface {
	Login(username, password string) LoginResponse
	MediaProvider() MediaProvider
}

type MediaProvider interface {
	SetPrefetchCoverCallback(cb func(coverArtID string))

	GetTrack(trackID string) (*Track, error)

	GetAlbum(albumID string) (*AlbumWithTracks, error)

	GetAlbumInfo(albumID string) (*AlbumInfo, error)

	GetArtist(artistID string) (*ArtistWithAlbums, error)

	GetArtistInfo(artistID string) (*ArtistInfo, error)

	GetPlaylist(playlistID string) (*PlaylistWithTracks, error)

	GetCoverArt(coverArtID string, size int) (image.Image, error)

	AlbumSortOrders() []string

	IterateAlbums(sortOrder string, filter AlbumFilter) AlbumIterator

	IterateTracks(searchQuery string) TrackIterator

	SearchAlbums(searchQuery string, filter AlbumFilter) AlbumIterator

	SearchAll(searchQuery string, maxResults int) ([]*SearchResult, error)

	GetRandomTracks(genre string, count int) ([]*Track, error)

	GetSimilarTracks(artistID string, count int) ([]*Track, error)

	GetSongRadio(trackID string, count int) ([]*Track, error)

	ArtistSortOrders() []string

	IterateArtists(sortOrder string, filter ArtistFilter) ArtistIterator

	SearchArtists(searchQuery string, filter ArtistFilter) ArtistIterator

	GetGenres() ([]*Genre, error)

	GetFavorites() (Favorites, error)

	GetStreamURL(trackID string, forceRaw bool) (string, error)

	GetTopTracks(artist Artist, count int) ([]*Track, error)

	SetFavorite(params RatingFavoriteParameters, favorite bool) error

	GetPlaylists() ([]*Playlist, error)

	CreatePlaylist(name string, trackIDs []string) error

	CanMakePublicPlaylist() bool

	EditPlaylist(id, name, description string, public bool) error

	AddPlaylistTracks(id string, trackIDsToAdd []string) error

	RemovePlaylistTracks(id string, trackIdxsToRemove []int) error

	ReplacePlaylistTracks(id string, trackIDs []string) error

	DeletePlaylist(id string) error

	// True if the `submission` parameter to TrackEndedPlayback will be respected
	// If false, the begin playback scrobble registers a play count immediately
	// when TrackBeganPlayback is invoked.
	ClientDecidesScrobble() bool

	TrackBeganPlayback(trackID string) error

	TrackEndedPlayback(trackID string, positionSecs int, submission bool) error

	DownloadTrack(trackID string) (io.Reader, error)

	RescanLibrary() error
}

type SupportsRating interface {
	SetRating(params RatingFavoriteParameters, rating int) error
}

type SupportsSharing interface {
	CreateShareURL(id string) (*url.URL, error)
	CanShareArtists() bool
}

type CanSavePlayQueue interface {
	SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error
	GetPlayQueue() (*SavedPlayQueue, error)
}

type LyricsProvider interface {
	GetLyrics(track *Track) (*Lyrics, error)
}

type JukeboxProvider interface {
	JukeboxStart() error
	JukeboxStop() error
	JukeboxSeek(idx, seconds int) error
	JukeboxClear() error
	JukeboxSet(trackID string) error
	JukeboxAdd(trackID string) error
	JukeboxRemove(idx int) error
	JukeboxSetVolume(vol int) error
	JukeboxGetStatus() (*JukeboxStatus, error)
}

type JukeboxStatus struct {
	Volume          int
	CurrentTrack    int
	Playing         bool
	PositionSeconds float64
}

func genresMatch(filterGenres, albumGenres []string) bool {
	for _, g1 := range filterGenres {
		for _, g2 := range albumGenres {
			if strings.EqualFold(g1, g2) {
				return true
			}
		}
	}
	return false
}
