package spotify

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// SimpleAlbum contains basic data about an album.
type SimpleAlbum struct {
	// The name of the album.
	Name string `json:"name"`
	// The type of the album: one of "album",
	// "single", or "compilation".
	AlbumType string `json:"album_type"`
	// The SpotifyID for the album.
	ID ID `json:"id"`
	// The SpotifyURI for the album.
	URI URI `json:"uri"`
	// The markets in which the album is available,
	// identified using ISO 3166-1 alpha-2 country
	// codes.  Note that al album is considered
	// available in a market when at least 1 of its
	// tracks is available in that market.
	AvailableMarkets []string `json:"available_markets"`
	// A link to the Web API enpoint providing full
	// details of the album.
	Endpoint string `json:"href"`
	// The cover art for the album in various sizes,
	// widest first.
	Images []Image `json:"images"`
	// Known external URLs for this album.
	ExternalURLs map[string]string `json:"external_urls"`
}

// Copyright contains the copyright statement associated with an album.
type Copyright struct {
	// The copyright text for the album.
	Text string `json:"text"`
	// The type of copyright.
	Type string `json:"type"`
}

// FullAlbum provides extra album data in addition to the data provided by SimpleAlbum.
type FullAlbum struct {
	SimpleAlbum
	Artists    []SimpleArtist `json:"artists"`
	Copyrights []Copyright    `json:"copyrights"`
	Genres     []string       `json:"genres"`
	// The popularity of the album, represented as an integer between 0 and 100,
	// with 100 being the most popular.  Popularity of an album is calculated
	// from the popularify of the album's individual tracks.
	Popularity int `json:"popularity"`
	// The date the album was first released.  For example, "1981-12-15".
	// Depending on the ReleaseDatePrecision, it might be shown as
	// "1981" or "1981-12". You can use ReleaseDateTime to convert this
	// to a time.Time value.
	ReleaseDate string `json:"release_date"`
	// The precision with which ReleaseDate value is known: "year", "month", or "day"
	ReleaseDatePrecision string            `json:"release_date_precision"`
	Tracks               SimpleTrackPage   `json:"tracks"`
	ExternalIDs          map[string]string `json:"external_ids"`
}

// SavedAlbum provides info about an album saved to an user's account.
type SavedAlbum struct {
	// The date and time the track was saved, represented as an ISO
	// 8601 UTC timestamp with a zero offset (YYYY-MM-DDTHH:MM:SSZ).
	// You can use the TimestampLayout constant to convert this to
	// a time.Time value.
	AddedAt   string `json:"added_at"`
	FullAlbum `json:"album"`
}

// ReleaseDateTime converts the album's ReleaseDate to a time.TimeValue.
// All of the fields in the result may not be valid.  For example, if
// f.ReleaseDatePrecision is "month", then only the month and year
// (but not the day) of the result are valid.
func (f *FullAlbum) ReleaseDateTime() time.Time {
	if f.ReleaseDatePrecision == "day" {
		result, _ := time.Parse(DateLayout, f.ReleaseDate)
		return result
	}
	if f.ReleaseDatePrecision == "month" {
		ym := strings.Split("-", f.ReleaseDate)
		year, _ := strconv.Atoi(ym[0])
		month, _ := strconv.Atoi(ym[1])
		return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
	}
	year, _ := strconv.Atoi(f.ReleaseDate)
	return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
}

// GetAlbum gets Spotify catalog information for a single album, given its Spotify ID.
func (c *Client) GetAlbum(id ID) (*FullAlbum, error) {
	spotifyURL := fmt.Sprintf("%salbums/%s", baseAddress, id)
	resp, err := c.http.Get(string(spotifyURL))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, decodeError(resp.Body)
	}
	var a FullAlbum
	err = json.NewDecoder(resp.Body).Decode(&a)
	if err != nil {
		return nil, err
	}
	return &a, nil
}

func toStringSlice(ids []ID) []string {
	result := make([]string, len(ids))
	for i, str := range ids {
		result[i] = str.String()
	}
	return result
}

// GetAlbums is a wrapper around DefaultClient.GetAlbums.
func GetAlbums(ids ...ID) ([]*FullAlbum, error) {
	return DefaultClient.GetAlbums(ids...)
}

// GetAlbums gets Spotify Catalog information for multiple albums, given their
// Spotify IDs.  It supports up to 20 IDs in a single call.  Albums are returned
// in the order requested.  If an album is not found, that position in the
// result slice will be nil.
func (c *Client) GetAlbums(ids ...ID) ([]*FullAlbum, error) {
	if len(ids) > 20 {
		return nil, errors.New("spotify: exceeded maximum number of albums")
	}
	spotifyURL := fmt.Sprintf("%salbums?ids=%s", baseAddress, strings.Join(toStringSlice(ids), ","))
	resp, err := c.http.Get(spotifyURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, decodeError(resp.Body)
	}
	var a struct {
		Albums []*FullAlbum `json:"albums"`
	}
	err = json.NewDecoder(resp.Body).Decode(&a)
	if err != nil {
		return nil, err
	}
	return a.Albums, nil
}

// AlbumType represents the type of an album. It can be used to filter
// results when searching for albums.
type AlbumType int

// AlbumType values that can be used to filter which types of albums are
// searched for.  These are flags that can be bitwise OR'd together
// to search for multiple types of albums simultaneously.
const (
	AlbumTypeAlbum       AlbumType = 1 << iota
	AlbumTypeSingle                = 1 << iota
	AlbummTypeAppearsOn            = 1 << iota
	AlbumTypeCompilation           = 1 << iota
)

func (at AlbumType) encode() string {
	types := []string{}
	if at&AlbumTypeAlbum != 0 {
		types = append(types, "album")
	}
	if at&AlbumTypeSingle != 0 {
		types = append(types, "single")
	}
	if at&AlbummTypeAppearsOn != 0 {
		types = append(types, "appears_on")
	}
	if at&AlbumTypeCompilation != 0 {
		types = append(types, "compilation")
	}
	return strings.Join(types, ",")
}

// GetAlbumTracks is a wrapper around DefaultClient.GetAlbumTracks.
func GetAlbumTracks(id ID) (*SimpleTrackPage, error) {
	return DefaultClient.GetAlbumTracks(id)
}

// GetAlbumTracks gets the tracks for a particular album.
// If you only care about the tracks, this call is more efficient
// than GetAlbum.
func (c *Client) GetAlbumTracks(id ID) (*SimpleTrackPage, error) {
	return c.GetAlbumTracksOpt(id, -1, -1)
}

// GetAlbumTracksOpt is a wrapper around DefaultClient.GetAlbumTracksOpt.
func GetAlbumTracksOpt(id ID, limit, offset int) (*SimpleTrackPage, error) {
	return DefaultClient.GetAlbumTracksOpt(id, limit, offset)
}

// GetAlbumTracksOpt behaves like GetAlbumTracks, with the exception that it
// allows you to specify extra parameters that limit the number of results returned.
// The maximum number of results to return is specified by limit.
// The offset argument can be used to specify the index of the first track to return.
// It can be used along with limit to reqeust the next set of results.
func (c *Client) GetAlbumTracksOpt(id ID, limit, offset int) (*SimpleTrackPage, error) {
	spotifyURL := fmt.Sprintf("%salbums/%s/tracks", baseAddress, id)
	v := url.Values{}
	if limit != -1 {
		v.Set("limit", strconv.Itoa(limit))
	}
	if offset != -1 {
		v.Set("offset", strconv.Itoa(offset))
	}
	optional := v.Encode()
	if optional != "" {
		spotifyURL = spotifyURL + "?" + optional
	}
	resp, err := c.http.Get(spotifyURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result SimpleTrackPage
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}
