# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""A clone of the Music Player Daemon (MPD) that plays music from a
Beets library. Attempts to implement a compatible protocol to allow
use of the wide range of MPD clients.
"""

from __future__ import division, absolute_import, print_function

import re
from string import Template
import traceback
import random
import time

import beets
from beets.plugins import BeetsPlugin
import beets.ui
from beets import logging
from beets import vfs
from beets.util import bluelet
from beets.library import Item
from beets import dbcore
from beets.mediafile import MediaFile
import six

PROTOCOL_VERSION = '0.13.0'
BUFSIZE = 1024

HELLO = 'OK MPD %s' % PROTOCOL_VERSION
CLIST_BEGIN = 'command_list_begin'
CLIST_VERBOSE_BEGIN = 'command_list_ok_begin'
CLIST_END = 'command_list_end'
RESP_OK = 'OK'
RESP_CLIST_VERBOSE = 'list_OK'
RESP_ERR = 'ACK'

NEWLINE = u"\n"

ERROR_NOT_LIST = 1
ERROR_ARG = 2
ERROR_PASSWORD = 3
ERROR_PERMISSION = 4
ERROR_UNKNOWN = 5
ERROR_NO_EXIST = 50
ERROR_PLAYLIST_MAX = 51
ERROR_SYSTEM = 52
ERROR_PLAYLIST_LOAD = 53
ERROR_UPDATE_ALREADY = 54
ERROR_PLAYER_SYNC = 55
ERROR_EXIST = 56

VOLUME_MIN = 0
VOLUME_MAX = 100

SAFE_COMMANDS = (
    # Commands that are available when unauthenticated.
    u'close', u'commands', u'notcommands', u'password', u'ping',
)

ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys())

# Loggers.
log = logging.getLogger('beets.bpd')
global_log = logging.getLogger('beets')


# Gstreamer import error.
class NoGstreamerError(Exception):
    pass


# Error-handling, exceptions, parameter parsing.

class BPDError(Exception):
    """An error that should be exposed to the client to the BPD
    server.
    """
    def __init__(self, code, message, cmd_name='', index=
0):
        self.code = code
        self.message = message
        self.cmd_name = cmd_name
        self.index = index

    template = Template(u'$resp [$code@$index] {$cmd_name} $message')

    def response(self):
        """Returns a string to be used as the response code for the
        erring command.
        """
        return self.template.substitute({
          
  'resp':     RESP_ERR,
            'code':     self.code,
            'index':    self.index,
            'cmd_name': self.cmd_name,
            'message':  self.message,
        })


def make_bpd_error(s_code, s_message):
    """Create a BPDError subclass for a static code and message.
    """

    class NewBPDError(BPDError):
        code = s_code
        message = s_message
        cmd_name = ''
        index = 0

        def __init__(self):
            pass
    return NewBPDError

ArgumentTypeError = make_bpd_error(ERROR_ARG, u'invalid type for argument')
ArgumentIndexError = make_bpd_error(ERROR_ARG, u'argument out of range')
ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, u'argument not found')


def cast_arg(t, val):
    """Attempts to call t on val, raising a ArgumentTypeError
    on ValueError.

    If 't' is the special string 'intbool', attempts to cast first
    to an int and then to a bool (i.e., 1=True, 0=False).
    """
    if t == 'intbool':
        return cast_arg(bool, cast_arg(int, val))
    else:
        try:
            return t(val)
        except ValueError:
            raise ArgumentTypeError()


class BPDClose(Exception):
    """Raised by a command invocation to indicate that the connection
    should be closed.
    """

# Generic server infrastructure, implementing the basic protocol.


class BaseServer(object):
    """A MPD-compatible music player server.

    The functions with the `cmd_` prefix are invoked in response to
    client commands. For instance, if the client says `status`,
    `cmd_status` will be invoked. The arguments to the client's commands
    are used as function arguments following the connection issuing the
    command. The functions may send data on the connection. They may
    also raise BPDError exceptions to report errors.

    This is a generic superclass and doesn't support many commands.
    """

    def __init__(self, host, port, password):
        """Create a new server bound to address `host` and listening
        on port `port`. If `password` is given, it is required to do
        anything significant on the server.
        """
        self.host, self.port, self.password = host, port, password

        # Default server values.
        self.random = False
        self.repeat = False
        self.volume = VOLUME_MAX
        self.crossfade = 0
        self.playlist = []
        self.playlist_version = 0
        self.current_index = -1
        self.paused = False
        self.error = None

        # Object for random numbers generation
        self.random_obj = random.Random()

    def run(self):
        """Block and start listening for connections from clients. An
        interrupt (^C) closes the server.
        """
        self.startup_time = time.time()
        bluelet.run(bluelet.server(self.host, self.port,
                                   Connection.handler(self)))

    def _item_info(self, item):
        """An abstract method that should response lines containing a
        single song's metadata.
        """
        raise NotImplementedError

    def _item_id(self, item):
        """An abstract method returning the integer id for an item.
        """
        raise NotImplementedError

    def _id_to_index(self, track_id):
        """Searches the playlist for a song with the given id and
        returns its index in the playlist.
        """
        track_id = cast_arg(int, track_id)
        for index, track in enumerate(self.playlist):
            if self._item_id(track) == track_id:
                return index
        # Loop finished with no track found.
        raise ArgumentNotFoundError()

    def _random_idx(self):
        """Returns a random index different from the current one.
        If there are no songs in the playlist it returns -1.
        If there is only one song in the playlist it returns 0.
        """
        if len(self.playlist) < 2:
            return len(self.playlist) - 1
        new_index = self.random_obj.randint(0, len(self.playlist) - 1)
        while new_index == self.current_index:
            new_index = self.random_obj.randint(0, len(self.playlist) - 1)
        return new_index

    def _succ_idx(self):
        """Returns the index for the next song to play.
        It also considers random and repeat flags.
        No boundaries are checked.
        """
        if self.repeat:
            return self.current_index
        if self.random:
            return self._random_idx()
        return self.current_index + 1

    def _prev_idx(self):
        """Returns the index for the previous song to play.
        It also considers random and repeat flags.
        No boundaries are checked.
        """
        if self.repeat:
            return self.current_index
        if self.random:
            return self._random_idx()
        return self.current_index - 1

    def cmd_ping(self, conn):
        """Succeeds."""
        pass

    def cmd_kill(self, conn):
        """Exits the server process."""
        exit(0)

    def cmd_close(self, conn):
        """Closes the connection."""
        raise BPDClose()

    def cmd_password(self, conn, password):
        """Attempts password authentication."""
        if password == self.password:
            conn.authenticated = True
        else:
            conn.authenticated = False
            raise BPDError(ERROR_PASSWORD, u'incorrect password')

    def cmd_comman