// Copyright 2009 The GoMPD Authors. All rights reserved.
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE file.

// Package mpd provides the client side interface to MPD (Music Player Daemon).
// The protocol reference can be found at http://www.musicpd.org/doc/protocol/index.html
package mpd

import (
	"errors"
	"fmt"
	"io"
	"net/textproto"
	"strconv"
	"strings"
	"time"
)

// Quote quotes string VALUES in the format understood by MPD.
// See: https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx
// NB: this function shouldn't be used on the PROTOCOL LEVEL because it considers single quotes special chars and
// escapes them.
func quote(s string) string {
	q := make([]byte, 2+2*len(s))
	i := 0
	q[i], i = '"', i+1
	for _, c := range []byte(s) {
		// We need to escape single/double quotes and a backslash by prepending them with a '\'
		if c == '"' || c == '\\' || c == '\'' {
			q[i] = '\\'
			i++
		}
		q[i] = c
		i++
	}
	q[i], i = '"', i+1
	return string(q[:i])
}

// Quote quotes each string of args in the format understood by MPD.
// See: https://github.com/MusicPlayerDaemon/MPD/blob/master/src/util/Tokenizer.cxx
func quoteArgs(args []string) string {
	quoted := make([]string, len(args))
	for index, arg := range args {
		quoted[index] = quote(arg)
	}
	return strings.Join(quoted, " ")
}

// Client represents a client connection to a MPD server.
type Client struct {
	text    *textproto.Conn
	version string
}

// Error represents an error returned by the MPD server.
// It contains the error number, the index of the causing command in the command list,
// the name of the command in the command list and the error message.
type Error struct {
	Code             ErrorCode
	CommandListIndex int
	CommandName      string
	Message          string
}

// ErrorCode is the error code of a Error.
type ErrorCode int

// ErrorCodes as defined in MPD source (https://www.musicpd.org/doc/api/html/Ack_8hxx_source.html)
// version 0.21.
const (
	ErrorNotList       ErrorCode = 1
	ErrorArg           ErrorCode = 2
	ErrorPassword      ErrorCode = 3
	ErrorPermission    ErrorCode = 4
	ErrorUnknown       ErrorCode = 5
	ErrorNoExist       ErrorCode = 50
	ErrorPlaylistMax   ErrorCode = 51
	ErrorSystem        ErrorCode = 52
	ErrorPlaylistLoad  ErrorCode = 53
	ErrorUpdateAlready ErrorCode = 54
	ErrorPlayerSync    ErrorCode = 55
	ErrorExist         ErrorCode = 56
)

func (e Error) Error() string {
	if e.CommandName != "" {
		return fmt.Sprintf("command '%s' failed: %s", e.CommandName, e.Message)
	}
	return e.Message
}

// Attrs is a set of attributes returned by MPD.
type Attrs map[string]string

// Dial connects to MPD listening on address addr (e.g. "127.0.0.1:6600")
// on network network (e.g. "tcp").
func Dial(network, addr string) (c *Client, err error) {
	text, err := textproto.Dial(network, addr)
	if err != nil {
		return nil, err
	}
	line, err := text.ReadLine()
	if err != nil {
		return nil, err
	}
	if line[0:6] != "OK MPD" {
		return nil, textproto.ProtocolError("no greeting")
	}
	return &Client{text: text, version: line[7:]}, nil
}

// DialAuthenticated connects to MPD listening on address addr (e.g. "127.0.0.1:6600")
// on network network (e.g. "tcp"). It then authenticates with MPD
// using the plaintext password password if it's not empty.
func DialAuthenticated(network, addr, password string) (c *Client, err error) {
	c, err = Dial(network, addr)
	if err == nil && len(password) > 0 {
		err = c.Command("password %s", password).OK()
	}
	return c, err
}

// Version returns the protocol version used as provided during the handshake.
func (c *Client) Version() string {
	return c.version
}

// We are reimplemeting Cmd() and PrintfLine() from textproto here, because
// the original functions append CR-LF to the end of commands. This behavior
// violates the MPD protocol: Commands must be terminated by '\n'.
func (c *Client) cmd(format string, args ...interface{}) (uint, error) {
	id := c.text.Next()
	c.text.StartRequest(id)
	defer c.text.EndRequest(id)
	if err := c.printfLine(format, args...); err != nil {
		return 0, err
	}
	return id, nil
}

func (c *Client) printfLine(format string, args ...interface{}) error {
	fmt.Fprintf(c.text.W, format, args...)
	c.text.W.WriteByte('\n')
	return c.text.W.Flush()
}

// Close terminates the connection with MPD.
func (c *Client) Close() (err error) {
	if c.text != nil {
		c.printfLine("close")
		err = c.text.Close()
		c.text = nil
	}
	return
}

// Ping sends a no-op message to MPD. It's useful for keeping the connection alive.
func (c *Client) Ping() error {
	return c.Command("ping").OK()
}

func (c *Client) readList(key string) (list []string, err error) {
	list = []string{}
	key += ": "
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}
		if line == "OK" {
			break
		}
		if !strings.HasPrefix(line, key) {
			return nil, textproto.ProtocolError("unexpected: " + line)
		}
		list = append(list, line[len(key):])
	}
	return
}

func (c *Client) readLine() (string, error) {
	line, err := c.text.ReadLine()
	if err != nil {
		return "", err
	}
	if strings.HasPrefix(line, "ACK ") {
		cur := line[4:]
		var code, idx int
		if strings.HasPrefix(cur, "[") {
			sep := strings.Index(cur, "@")
			end := strings.Index(cur, "] ")
			if sep > 0 && end > 0 {
				code, err = strconv.Atoi(cur[1:sep])
				if err != nil {
					return "", err
				}
				idx, err = strconv.Atoi(cur[sep+1 : end])
				if err != nil {
					return "", err
				}
				cur = cur[end+2:]
			}
		}
		var cmd string
		if strings.HasPrefix(cur, "{") {
			if end := strings.Index(cur, "} "); end > 0 {
				cmd = cur[1:end]
				cur = cur[end+2:]
			}
		}
		msg := strings.TrimSpace(cur)
		return "", Error{
			Code:             ErrorCode(code),
			CommandListIndex: idx,
			CommandName:      cmd,
			Message:          msg,
		}
	}
	return line, nil
}

func (c *Client) readBytes(length int) ([]byte, error) {
	// Read the entire chunk of data. ReadFull() makes sure the data length matches the expectation
	data := make([]byte, length)
	if _, err := io.ReadFull(c.text.R, data); err != nil {
		return nil, err
	}

	// Verify there's a linebreak afterwards and skip it
	termByte, err := c.text.R.ReadByte()
	if err != nil {
		return nil, textproto.ProtocolError("failed to read binary data terminator: " + err.Error())
	}
	if termByte != '\n' {
		return nil, textproto.ProtocolError(fmt.Sprintf("wrong binary data terminator: want 0x0a, got %x", termByte))
	}
	return data, nil
}

func (c *Client) readAttrsList(startKey string) (attrs []Attrs, err error) {
	attrs = []Attrs{}
	startKey += ": "
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}
		if line == "OK" {
			break
		}
		if strings.HasPrefix(line, startKey) { // new entry begins
			attrs = append(attrs, Attrs{})
		}
		if len(attrs) == 0 {
			return nil, textproto.ProtocolError("unexpected: " + line)
		}
		i := strings.Index(line, ": ")
		if i < 0 {
			return nil, textproto.ProtocolError("can't parse line: " + line)
		}
		attrs[len(attrs)-1][line[0:i]] = line[i+2:]
	}
	return attrs, nil
}

func (c *Client) readAttrs(terminator string) (attrs Attrs, err error) {
	attrs = make(Attrs)
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}
		if line == terminator {
			break
		}
		z := strings.Index(line, ": ")
		if z < 0 {
			return nil, textproto.ProtocolError("can't parse line: " + line)
		}
		key := line[0:z]
		attrs[key] = line[z+2:]
	}
	return
}

func (c *Client) readBinary() ([]byte, int, error) {
	size := -1
	for {
		line, err := c.readLine()
		switch {
		case err != nil:
			return nil, 0, err

		// Check for the size key
		case strings.HasPrefix(line, "size: "):
			if size, err = strconv.Atoi(line[6:]); err != nil {
				return nil, 0, textproto.ProtocolError("failed to parse size: " + err.Error())
			}

		// Check for the binary key
		case strings.HasPrefix(line, "binary: "):
			length := -1
			if length, err = strconv.Atoi(line[8:]); err != nil {
				return nil, 0, textproto.ProtocolError("failed to parse binary: " + err.Error())
			}

			// If no size is given, assume it's equal to the provided data's length
			if size < 0 {
				size = length
			}

			// The binary data must follow the 'binary:' key
			data, err := c.readBytes(length)
			if err != nil {
				return nil, 0, err
			}

			// The binary data must be followed by the "OK" line
			if s, err := c.readLine(); err != nil {
				return nil, 0, err
			} else if s != "OK" {
				return nil, 0, textproto.ProtocolError("expected 'OK', got " + s)
			}
			return data, size, nil

		// No more data. Obviously, no binary data encountered
		case line == "", line == "OK":
			return nil, 0, textproto.ProtocolError("no binary data found in response")
		}
	}
}

// CurrentSong returns information about the current song in the playlist.
func (c *Client) CurrentSong() (Attrs, error) {
	return c.Command("currentsong").Attrs()
}

// Status returns information about the current status of MPD.
func (c *Client) Status() (Attrs, error) {
	return c.Command("status").Attrs()
}

// Stats displays statistics (number of artists, songs, playtime, etc)
func (c *Client) Stats() (Attrs, error) {
	return c.Command("stats").Attrs()
}

func (c *Client) readOKLine(terminator string) (err error) {
	line, err := c.readLine()
	if err != nil {
		return
	}
	if line == terminator {
		return nil
	}
	return textproto.ProtocolError("unexpected response: " + line)
}

func (c *Client) idle(subsystems ...string) ([]string, error) {
	return c.Command("idle %s", Quoted(strings.Join(subsystems, " "))).Strings("changed")
}

func (c *Client) noIdle() (err error) {
	id, err := c.cmd("noidle")
	if err == nil {
		c.text.StartResponse(id)
		c.text.EndResponse(id)
	}
	return
}

//
// Playback control
//

// Next plays next song in the playlist.
func (c *Client) Next() error {
	return c.Command("next").OK()
}

// Pause pauses playback if pause is true; resumes playback otherwise.
func (c *Client) Pause(pause bool) error {
	if pause {
		return c.Command("pause 1").OK()
	}
	return c.Command("pause 0").OK()
}

// Play starts playing the song at playlist position pos. If pos is negative,
// start playing at the current position in the playlist.
func (c *Client) Play(pos int) error {
	if pos < 0 {
		return c.Command("play").OK()
	}
	return c.Command("play %d", pos).OK()
}

// PlayID plays the song identified by id. If id is negative, start playing
// at the current position in playlist.
func (c *Client) PlayID(id int) error {
	if id < 0 {
		return c.Command("playid").OK()
	}
	return c.Command("playid %d", id).OK()
}

// Previous plays previous song in the playlist.
func (c *Client) Previous() error {
	return c.Command("previous").OK()
}

// Seek seeks to the position time (in seconds) of the song at playlist position pos.
// Deprecated: Use SeekPos instead.
func (c *Client) Seek(pos, time int) error {
	return c.Command("seek %d %d", pos, time).OK()
}

// SeekID is identical to Seek except the song is identified by it's id
// (not position in playlist).
// Deprecated: Use SeekSongID instead.
func (c *Client) SeekID(id, time int) error {
	return c.Command("seekid %d %d", id, time).OK()
}

// SeekPos seeks to the position d of the song at playlist position pos.
func (c *Client) SeekPos(pos int, d time.Duration) error {
	return c.Command("seek %d %f", pos, d.Seconds()).OK()
}

// SeekSongID seeks to the position d of the song identified by id.
func (c *Client) SeekSongID(id int, d time.Duration) error {
	return c.Command("seekid %d %f", id, d.Seconds()).OK()
}

// SeekCur seeks to the position d within the current song.
// If relative is true, then the time is relative to the current playing position.
func (c *Client) SeekCur(d time.Duration, relative bool) error {
	if relative {
		return c.Command("seekcur %+f", d.Seconds()).OK()
	}
	return c.Command("seekcur %f", d.Seconds()).OK()
}

// Stop stops playback.
func (c *Client) Stop() error {
	return c.Command("stop").OK()
}

// SetVolume sets the volume to volume. The range of volume is 0-100.
func (c *Client) SetVolume(volume int) error {
	return c.Command("setvol %d", volume).OK()
}

// Random enables random playback, if random is true, disables it otherwise.
func (c *Client) Random(random bool) error {
	if random {
		return c.Command("random 1").OK()
	}
	return c.Command("random 0").OK()
}

// Repeat enables repeat mode, if repeat is true, disables it otherwise.
func (c *Client) Repeat(repeat bool) error {
	if repeat {
		return c.Command("repeat 1").OK()
	}
	return c.Command("repeat 0").OK()
}

// Single enables single song mode, if single is true, disables it otherwise.
func (c *Client) Single(single bool) error {
	if single {
		return c.Command("single 1").OK()
	}
	return c.Command("single 0").OK()
}

// Consume enables consume mode, if consume is true, disables it otherwise.
func (c *Client) Consume(consume bool) error {
	if consume {
		return c.Command("consume 1").OK()
	}
	return c.Command("consume 0").OK()
}

//
// Playlist related functions
//

// PlaylistInfo returns attributes for songs in the current playlist. If
// both start and end are negative, it does this for all songs in
// playlist. If end is negative but start is positive, it does it for the
// song at position start. If both start and end are positive, it does it
// for positions in range [start, end).
func (c *Client) PlaylistInfo(start, end int) ([]Attrs, error) {
	var cmd *Command
	switch {
	case start < 0 && end < 0:
		// Request all playlist items.
		cmd = c.Command("playlistinfo")
	case start >= 0 && end >= 0:
		// Request this range of playlist items.
		cmd = c.Command("playlistinfo %d:%d", start, end)
	case start >= 0 && end < 0:
		// Request the single playlist item at this position.
		cmd = c.Command("playlistinfo %d", start)
	case start < 0 && end >= 0:
		return nil, errors.New("negative start index")
	default:
		panic("unreachable")
	}
	return cmd.AttrsList("file")
}

// SetPriority set the priority of the specified songs. If end is negative but
// start is non-negative, it does it for the song at position start. If both
// start and end are non-negative, it does it for positions in range
// [start, end).
func (c *Client) SetPriority(priority, start, end int) error {
	switch {
	case start < 0 && end < 0:
		return errors.New("negative start and end index")
	case start >= 0 && end >= 0:
		// Update the prio for this range of playlist items.
		return c.Command("prio %d %d:%d", priority, start, end).OK()
	case start >= 0 && end < 0:
		// Update the prio for a single playlist item at this position.
		return c.Command("prio %d %d", priority, start).OK()
	case start < 0 && end >= 0:
		return errors.New("negative start index")
	default:
		panic("unreachable")
	}
}

// SetPriorityID sets the prio of the song with the given id.
func (c *Client) SetPriorityID(priority, id int) error {
	return c.Command("prioid %d %d", priority, id).OK()
}

// Delete deletes songs from playlist. If both start and end are positive,
// it deletes those at positions in range [start, end). If end is negative,
// it deletes the song at position start.
func (c *Client) Delete(start, end int) error {
	if start < 0 {
		return errors.New("negative start index")
	}
	if end < 0 {
		return c.Command("delete %d", start).OK()
	}
	return c.Command("delete %d:%d", start, end).OK()
}

// DeleteID deletes the song identified by id.
func (c *Client) DeleteID(id int) error {
	return c.Command("deleteid %d", id).OK()
}

// Move moves the songs between the positions start and end to the new position
// position. If end is negative, only the song at position start is moved.
func (c *Client) Move(start, end, position int) error {
	if start < 0 {
		return errors.New("negative start index")
	}
	if end < 0 {
		return c.Command("move %d %d", start, position).OK()
	}
	return c.Command("move %d:%d %d", start, end, position).OK()
}

// MoveID moves songid to position on the plyalist.
func (c *Client) MoveID(songid, position int) error {
	return c.Command("moveid %d %d", songid, position).OK()
}

// Add adds the file/directory uri to playlist. Directories add recursively.
func (c *Client) Add(uri string) error {
	return c.Command("add %s", uri).OK()
}

// AddID adds the file/directory uri to playlist and returns the identity
// id of the song added. If pos is positive, the song is added to position
// pos.
func (c *Client) AddID(uri string, pos int) (int, error) {
	var cmd *Command
	if pos >= 0 {
		cmd = c.Command("addid %s %d", uri, pos)
	} else {
		cmd = c.Command("addid %s", uri)
	}
	attrs, err := cmd.Attrs()
	if err != nil {
		return -1, err
	}
	tok, ok := attrs["Id"]
	if !ok {
		return -1, textproto.ProtocolError("addid did not return Id")
	}
	return strconv.Atoi(tok)
}

// Clear clears the current playlist.
func (c *Client) Clear() error {
	return c.Command("clear").OK()
}

// Shuffle shuffles the tracks from position start to position end in the
// current playlist. If start or end is negative, the whole playlist is
// shuffled.
func (c *Client) Shuffle(start, end int) error {
	if start < 0 || end < 0 {
		return c.Command("shuffle").OK()
	}
	return c.Command("shuffle %d:%d", start, end).OK()
}

// Database related commands

// GetFiles returns the entire list of files in MPD database.
func (c *Client) GetFiles() ([]string, error) {
	return c.Command("list file").Strings("file")
}

// Update updates MPD's database: find new files, remove deleted files, update
// modified files. uri is a particular directory or file to update. If it is an
// empty string, everything is updated.
//
// The returned jobID identifies the update job, enqueued by MPD.
func (c *Client) Update(uri string) (jobID int, err error) {
	id, err := c.cmd("update %s", quote(uri))
	if err != nil {
		return
	}
	c.text.StartResponse(id)
	defer c.text.EndResponse(id)

	line, err := c.readLine()
	if err != nil {
		return
	}
	if !strings.HasPrefix(line, "updating_db: ") {
		return 0, textproto.ProtocolError("unexpected response: " + line)
	}
	jobID, err = strconv.Atoi(line[13:])
	if err != nil {
		return
	}
	return jobID, c.readOKLine("OK")
}

// Rescan updates MPD's database like Update, but it also rescans unmodified
// files. uri is a particular directory or file to update. If it is an empty
// string, everything is updated.
//
// The returned jobID identifies the update job, enqueued by MPD.
func (c *Client) Rescan(uri string) (jobID int, err error) {
	id, err := c.cmd("rescan %s", quote(uri))
	if err != nil {
		return
	}
	c.text.StartResponse(id)
	defer c.text.EndResponse(id)

	line, err := c.readLine()
	if err != nil {
		return
	}
	if !strings.HasPrefix(line, "updating_db: ") {
		return 0, textproto.ProtocolError("unexpected response: " + line)
	}
	jobID, err = strconv.Atoi(line[13:])
	if err != nil {
		return
	}
	return jobID, c.readOKLine("OK")
}

// ListAllInfo returns attributes for songs in the library. Information about
// any song that is either inside or matches the passed in uri is returned.
// To get information about every song in the library, pass in "/".
func (c *Client) ListAllInfo(uri string) ([]Attrs, error) {
	id, err := c.cmd("listallinfo %s ", quote(uri))
	if err != nil {
		return nil, err
	}
	c.text.StartResponse(id)
	defer c.text.EndResponse(id)

	attrs := []Attrs{}
	inEntry := false
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}
		if line == "OK" {
			break
		} else if strings.HasPrefix(line, "file: ") { // new entry begins
			attrs = append(attrs, Attrs{})
			inEntry = true
		} else if strings.HasPrefix(line, "directory: ") {
			inEntry = false
		}

		if inEntry {
			i := strings.Index(line, ": ")
			if i < 0 {
				return nil, textproto.ProtocolError("can't parse line: " + line)
			}
			attrs[len(attrs)-1][line[0:i]] = line[i+2:]
		}
	}
	return attrs, nil
}

// ListInfo lists the contents of the directory URI using MPD's lsinfo command.
func (c *Client) ListInfo(uri string) ([]Attrs, error) {
	id, err := c.cmd("lsinfo %s", quote(uri))
	if err != nil {
		return nil, err
	}
	c.text.StartResponse(id)
	defer c.text.EndResponse(id)
	attrs := []Attrs{}
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}
		if line == "OK" {
			break
		}
		if strings.HasPrefix(line, "file: ") ||
			strings.HasPrefix(line, "directory: ") ||
			strings.HasPrefix(line, "playlist: ") {
			attrs = append(attrs, Attrs{})
		}
		i := strings.Index(line, ": ")
		if i < 0 {
			return nil, textproto.ProtocolError("can't parse line: " + line)
		}
		attrs[len(attrs)-1][strings.ToLower(line[0:i])] = line[i+2:]
	}
	return attrs, nil
}

// ReadComments reads "comments" (audio metadata) from the song URI using
// MPD's readcomments command.
func (c *Client) ReadComments(uri string) (Attrs, error) {
	return c.Command("readcomments %s", uri).Attrs()
}

// Find searches the library for songs and returns attributes for each matching song.
// The args are the raw arguments passed to MPD. For example, to search for
// songs that belong to a specific artist and album:
//
//	Find("artist", "Artist Name", "album", "Album Name")
//
// Searches are case sensitive. Use Search for case insensitive search.
func (c *Client) Find(args ...string) ([]Attrs, error) {
	return c.Command("find " + quoteArgs(args)).AttrsList("file")
}

// Search behaves exactly the same as Find, but the searches are not case sensitive.
func (c *Client) Search(args ...string) ([]Attrs, error) {
	return c.Command("search " + quoteArgs(args)).AttrsList("file")
}

// List searches the database for your query. You can use something simple like
// `artist` for your search, or something like `artist album <Album Name>` if
// you want the artist that has an album with a specified album name.
func (c *Client) List(args ...string) ([]string, error) {
	id, err := c.cmd("list " + quoteArgs(args))
	if err != nil {
		return nil, err
	}
	c.text.StartResponse(id)
	defer c.text.EndResponse(id)

	var ret []string
	for {
		line, err := c.readLine()
		if err != nil {
			return nil, err
		}

		i := strings.Index(line, ": ")
		if i > 0 {
			ret = append(ret, line[i+2:])
		} else if line == "OK" {
			break
		} else {
			return nil, textproto.ProtocolError("can't parse line: " + line)
		}
	}
	return ret, nil
}

// Output related commands.

// ListOutputs lists all configured outputs with their name, id & enabled state.
func (c *Client) ListOutputs() ([]Attrs, error) {
	return c.Command("outputs").AttrsList("outputid")
}

// EnableOutput enables the audio output with the given id.
func (c *Client) EnableOutput(id int) error {
	return c.Command("enableoutput %d", id).OK()
}

// DisableOutput disables the audio output with the given id.
func (c *Client) DisableOutput(id int) error {
	return c.Command("disableoutput %d", id).OK()
}

// Stored playlists related commands

// ListPlaylists lists all stored playlists.
func (c *Client) ListPlaylists() ([]Attrs, error) {
	return c.Command("listplaylists").AttrsList("playlist")
}

// PlaylistContents returns a list of attributes for songs in the specified
// stored playlist.
func (c *Client) PlaylistContents(name string) ([]Attrs, error) {
	return c.Command("listplaylistinfo %s", name).AttrsList("file")
}

// PlaylistLoad loads the specfied playlist into the current queue.
// If start and end are non-negative, only songs in this range are loaded.
func (c *Client) PlaylistLoad(name string, start, end int) error {
	if start < 0 || end < 0 {
		return c.Command("load %s", name).OK()
	}
	return c.Command("load %s %d:%d", name, start, end).OK()
}

// PlaylistAdd adds a song identified by uri to a stored playlist identified
// by name.
func (c *Client) PlaylistAdd(name string, uri string) error {
	return c.Command("playlistadd %s %s", name, uri).OK()
}

// PlaylistClear clears the specified playlist.
func (c *Client) PlaylistClear(name string) error {
	return c.Command("playlistclear %s", name).OK()
}

// PlaylistDelete deletes the song at position pos from the specified playlist.
func (c *Client) PlaylistDelete(name string, pos int) error {
	return c.Command("playlistdelete %s %d", name, pos).OK()
}

// PlaylistMove moves a song identified by id in a playlist identified by name
// to the position pos.
func (c *Client) PlaylistMove(name string, id, pos int) error {
	return c.Command("playlistmove %s %d %d", name, id, pos).OK()
}

// PlaylistRename renames the playlist identified by name to newName.
func (c *Client) PlaylistRename(name, newName string) error {
	return c.Command("rename %s %s", name, newName).OK()
}

// PlaylistRemove removes the playlist identified by name from the playlist
// directory.
func (c *Client) PlaylistRemove(name string) error {
	return c.Command("rm %s", name).OK()
}

// PlaylistSave saves the current playlist as name in the playlist directory.
func (c *Client) PlaylistSave(name string) error {
	return c.Command("save %s", name).OK()
}

// A Sticker represents a name/value pair associated to a song. Stickers
// are managed and shared by MPD clients, and MPD server does not assume
// any special meaning in them.
type Sticker struct {
	Name, Value string
}

func newSticker(name, value string) *Sticker {
	return &Sticker{
		Name:  name,
		Value: value,
	}
}

func parseSticker(s string) (*Sticker, error) {
	// Since '=' can appear in the sticker name and in the sticker value,
	// it's impossible to determine where the name ends and value starts.
	// Assume that '=' is more likely to occur in the value
	// (e.g. base64 encoded data -- see #39).
	i := strings.Index(s, "=")
	if i < 0 {
		return nil, textproto.ProtocolError("parsing sticker failed")
	}
	return newSticker(s[:i], s[i+1:]), nil
}

// StickerDelete deletes sticker for the song with given URI.
func (c *Client) StickerDelete(uri string, name string) error {
	return c.Command("sticker delete song %s %s", uri, name).OK()
}

// StickerFind finds songs inside directory with URI which have a sticker with given name.
// It returns a slice of URIs of matching songs and a slice of corresponding stickers.
func (c *Client) StickerFind(uri string, name string) ([]string, []Sticker, error) {
	attrs, err := c.Command("sticker find song %s %s", uri, name).AttrsList("file")
	if err != nil {
		return nil, nil, err
	}
	files := make([]string, len(attrs))
	stks := make([]Sticker, len(attrs))
	for i, attr := range attrs {
		if _, ok := attr["file"]; !ok {
			return nil, nil, textproto.ProtocolError("file attribute not found")
		}
		if _, ok := attr["sticker"]; !ok {
			return nil, nil, textproto.ProtocolError("sticker attribute not found")
		}
		files[i] = attr["file"]
		stk, err := parseSticker(attr["sticker"])
		if err != nil {
			return nil, nil, err
		}
		stks[i] = *stk
	}
	return files, stks, nil
}

// StickerGet gets sticker value for the song with given URI.
func (c *Client) StickerGet(uri string, name string) (*Sticker, error) {
	attrs, err := c.Command("sticker get song %s %s", uri, name).Attrs()
	if err != nil {
		return nil, err
	}
	attr, ok := attrs["sticker"]
	if !ok {
		return nil, textproto.ProtocolError("sticker not found")
	}
	stk, err := parseSticker(attr)
	if stk == nil {
		return nil, err
	}
	return stk, nil
}

// StickerList returns a slice of stickers for the song with given URI.
func (c *Client) StickerList(uri string) ([]Sticker, error) {
	attrs, err := c.Command("sticker list song %s", uri).AttrsList("sticker")
	if err != nil {
		return nil, err
	}
	stks := make([]Sticker, len(attrs))
	for i, attr := range attrs {
		s, ok := attr["sticker"]
		if !ok {
			return nil, textproto.ProtocolError("sticker attribute not found")
		}
		stk, err := parseSticker(s)
		if err != nil {
			return nil, err
		}
		stks[i] = *stk
	}
	return stks, nil
}

// StickerSet sets sticker value for the song with given URI.
func (c *Client) StickerSet(uri string, name string, value string) error {
	return c.Command("sticker set song %s %s %s", uri, name, value).OK()
}

// AlbumArt retrieves an album artwork image for a song with the given URI using MPD's albumart command.
func (c *Client) AlbumArt(uri string) ([]byte, error) {
	offset := 0
	var data []byte
	for {
		// Read the data in chunks
		chunk, size, err := c.Command("albumart %s %d", uri, offset).Binary()
		if err != nil {
			return nil, err
		}

		// Accumulate the data
		data = append(data, chunk...)
		offset = len(data)
		if offset >= size {
			break
		}
	}
	return data, nil
}
