# encoding: utf-8

require 'socket'
require "mpd_client/version"

HELLO_PREFIX = "OK MPD "
ERROR_PREFIX = "ACK "
SUCCESS = "OK"
NEXT = "list_OK"

# MPD changelog: http://git.musicpd.org/cgit/master/mpd.git/plain/NEWS
# http://mpd.wikia.com/wiki/MusicPlayerDaemonCommands
# http://git.musicpd.org/cgit/cirrus/mpd.git/plain/doc/protocol.xml
#
COMMANDS = {
  # Status Commands
  "clearerror"         => "fetch_nothing",
  "currentsong"        => "fetch_object",
  "idle"               => "fetch_list",
  "noidle"             => "",
  "status"             => "fetch_object",
  "stats"              => "fetch_object",
  # Playback Option Commands
  "consume"            => "fetch_nothing",
  "crossfade"          => "fetch_nothing",
  "mixrampdb"          => "fetch_nothing",
  "mixrampdelay"       => "fetch_nothing",
  "random"             => "fetch_nothing",
  "repeat"             => "fetch_nothing",
  "setvol"             => "fetch_nothing",
  "single"             => "fetch_nothing",
  "replay_gain_mode"   => "fetch_nothing",
  "replay_gain_status" => "fetch_item",
  "volume"             => "fetch_nothing",
  # Playback Control Commands
  "next"               => "fetch_nothing",
  "pause"              => "fetch_nothing",
  "play"               => "fetch_nothing",
  "playid"             => "fetch_nothing",
  "previous"           => "fetch_nothing",
  "seek"               => "fetch_nothing",
  "seekid"             => "fetch_nothing",
  "seekcur"            => "fetch_nothing",
  "stop"               => "fetch_nothing",
  # Playlist Commands
  "add"                => "fetch_nothing",
  "addid"              => "fetch_item",
  "clear"              => "fetch_nothing",
  "delete"             => "fetch_nothing",
  "deleteid"           => "fetch_nothing",
  "move"               => "fetch_nothing",
  "moveid"             => "fetch_nothing",
  "playlist"           => "fetch_playlist",
  "playlistfind"       => "fetch_songs",
  "playlistid"         => "fetch_songs",
  "playlistinfo"       => "fetch_songs",
  "playlistsearch"     => "fetch_songs",
  "plchanges"          => "fetch_songs",
  "plchangesposid"     => "fetch_changes",
  "prio"               => "fetch_nothing",
  "prioid"             => "fetch_nothing",
  "shuffle"            => "fetch_nothing",
  "swap"               => "fetch_nothing",
  "swapid"             => "fetch_nothing",
  # Stored Playlist Commands
  "listplaylist"       => "fetch_list",
  "listplaylistinfo"   => "fetch_songs",
  "listplaylists"      => "fetch_playlists",
  "load"               => "fetch_nothing",
  "playlistadd"        => "fetch_nothing",
  "playlistclear"      => "fetch_nothing",
  "playlistdelete"     => "fetch_nothing",
  "playlistmove"       => "fetch_nothing",
  "rename"             => "fetch_nothing",
  "rm"                 => "fetch_nothing",
  "save"               => "fetch_nothing",
  # Database Commands
  "count"              => "fetch_object",
  "find"               => "fetch_songs",
  "findadd"            => "fetch_nothing",
  "list"               => "fetch_list",
  "listall"            => "fetch_database",
  "listallinfo"        => "fetch_database",
  "lsinfo"             => "fetch_database",
  "search"             => "fetch_songs",
  "searchadd"          => "fetch_nothing",
  "searchaddp1"        => "fetch_nothing",
  "update"             => "fetch_item",
  "rescan"             => "fetch_item",
  "readcomments"       => "fetch_item",
  # Sticker Commands
  "sticker get"        => "fetch_sticker",
  "sticker set"        => "fetch_nothing",
  "sticker delete"     => "fetch_nothing",
  "sticker list"       => "fetch_stickers",
  "sticker find"       => "fetch_songs",
  # Connection Commands
  "close"              => "",
  "kill"               => "",
  "password"           => "fetch_nothing",
  "ping"               => "fetch_nothing",
  # Audio Output Commands
  "disableoutput"      => "fetch_nothing",
  "enableoutput"       => "fetch_nothing",
  "outputs"            => "fetch_outputs",
  "toggleoutput"       => "fetch_nothing",
  # Reflection Commands
  "config"             => "fetch_item",
  "commands"           => "fetch_list",
  "notcommands"        => "fetch_list",
  "tagtypes"           => "fetch_list",
  "urlhandlers"        => "fetch_list",
  "decoders"           => "fetch_plugins",
  # Client To Client
  "subscribe"          => "fetch_nothing",
  "unsubscribe"        => "fetch_nothing",
  "channels"           => "fetch_list",
  "readmessages"       => "fetch_messages",
  "sendmessage"        => "fetch_nothing"
}

# The MPDClient library is used for interactions with a MPD.
#
# == Example
#
#   require 'mpd_client'
#   require 'logger'
#
#   client = MPDClient.new
#   client.log = Logger.new($stderr)
#   client.connect('/var/run/mpd/socket')
#
class MPDClient
  attr_reader :mpd_version

  class << self
    # Default logger for all MPDClient instances
    #
    #   MPDClient.log = Logger.new($stderr)
    #
    attr_accessor :log

    def add_command(name, retval)
      escaped_name = name.gsub(' ', '_')
      define_method escaped_name.to_sym do |*args|
        execute(name, *args, retval)
      end
    end

    def remove_command(name)
      raise "Can't remove not existent '#{name}' command" unless method_defined? name.to_sym
      remove_method name.to_sym
    end
  end

  def initialize
    @mutex = Mutex.new
    reset
  end

  def connect(host = 'localhost', port = 6600)
    @host = host
    @port = port
    reconnect
  end

  def reconnect
    log.info("MPD (re)connect #{host}, #{port}") if log
    if @host.start_with?('/')
      @socket = UNIXSocket.new(@host)
      hello
    else
      @socket = TCPSocket.new(@host, @port)
      hello
    end
  end

  def disconnect
    log.info("MPD disconnect") if log
    @socket.close
    reset
  end

  # http://www.musicpd.org/doc/protocol/ch01s04.html
  def command_list_ok_begin
    raise "Already in command list" unless @command_list.nil?
    write_command('command_list_ok_begin')
    @command_list = []
  end

  def command_list_end
    raise "Not in command list" if @command_list.nil?
    write_command('command_list_end')

    return fetch_command_list
  end

  # The current logger. If no logger has been set MPDClient.log is used
  #
  def log
    @log || MPDClient.log
  end

  # Sets the +logger+ used by this instance of MPDClient
  #
  def log= logger
    @log = logger
  end

  private

  def execute(command, *args, retval)
    @mutex.synchronize do
      if !@command_list.nil?
        write_command(command, *args)
        @command_list << retval
      else
        write_command(command, *args)
        eval retval
      end
    end
  end

  def write_line(line)
    begin
      @socket.puts line
    rescue Errno::EPIPE
      reconnect
      @socket.puts line
    end
    @socket.flush
  end

  def write_command(command, *args)
    parts = [command]
    args.each do |arg|
      if arg.kind_of?(Array)
        parts << (arg.size == 1 ? "\"#{arg[0].to_i}:\"" : "\"#{arg[0].to_i}:#{arg[1].to_i}\"")
      else
        parts << "\"#{escape(arg)}\""
      end
    end
    #log.debug("Calling MPD: #{command}#{args}") if log
    log.debug("Calling MPD: #{parts.join(' ')}") if log
    write_line(parts.join(' '))
  end

  def read_line
    line = @socket.gets.force_encoding('utf-8')
    raise "Connection lost while reading line" unless line.end_with?("\n")
    line.chomp!
    if line.start_with?(ERROR_PREFIX)
      error = line[/#{ERROR_PREFIX}(.*)/, 1].strip
      raise error
    end

    if !@command_list.nil?
      return if line == NEXT
      raise "Got unexpected '#{SUCCESS}'" if line == SUCCESS
    elsif line == SUCCESS
      return
    end

    return line
  end

  def read_pair(separator)
    line = read_line
    return if line.nil?
    pair = line.split(separator, 2)
    raise "Could now parse pair: '#{line}'" if pair.size < 2

    return pair #Array
  end

  def read_pairs(separator = ': ')
    result = []
    pair = read_pair(separator)
    while pair
      result << pair
      pair = read_pair(separator)
    end

    return result
  end

  def fetch_item
    pairs = read_pairs
    return nil if pairs.size != 1
    return pairs[0][1]
  end

  def fetch_nothing
    line = read_line
    raise "Got unexpected return value: #{line}" unless line.nil?
  end

  def fetch_list
    result = []
    seen = nil
    read_pairs.each do |key, value|
      if key != seen
        if seen != nil
          raise "Expected key '#{seen}', got '#{key}'"
        end
        seen = key
      end
      result << value
    end

    return result
  end

  def fetch_objects(delimeters = [])
    result = []
    obj = {}
    read_pairs.each do |key, value|
      key = key.downcase
      if delimeters.include?(key)
        result << obj unless obj.empty?
        obj = {}
      elsif obj.include?(key)
        obj[key] << value
      end
      obj[key] = value
    end

    result << obj unless obj.empty?

    return result
  end

  def fetch_object
    objs = fetch_objects
    return objs ? objs[0] : {}
  end

  def fetch_changes; fetch_objects(['cpos']); end

  def fetch_songs; fetch_objects(['file']); end

  def fetch_messages; fetch_objects('channel'); end

  def fetch_outputs; fetch_objects(['outputid']); end

  def fetch_plugins; fetch_objects(['plugin']); end

  def fetch_database; fetch_objects(['file', 'directory', 'playlist']); end

  def fetch_playlists; fetch_objects(['playlist']); end

  def fetch_playlist
    result = []
    read_pairs(':').each do |key, value|
      result << value
    end

    return result
  end

  def fetch_stickers
    result = []
    read_pairs.each do |key, sticker|
      value = sticker.split('=', 2)
      raise "Could now parse sticker: #{sticker}" if value.size < 2
      result << Hash[*value]
    end

    return result
  end

  def fetch_sticker; fetch_stickers[0]; end

  def fetch_command_list
    result = []
    begin
      @command_list.each do |retval|
        result << (eval retval)
      end
    ensure
      @command_list = nil
    end

    return result
  end


  def hello
    line = @socket.gets
    raise "Connection lost while reading MPD hello" unless line.end_with?("\n")
    line.chomp!
    raise "Got invalid MPD hello: #{line}" unless line.start_with?(HELLO_PREFIX)
    @mpd_version = line[/#{HELLO_PREFIX}(.*)/, 1]
  end

  def reset
    @mpd_version = nil
    @command_list = nil
    @socket = nil
    @log = nil
  end

  def escape(text)
    text.to_s.gsub("\\", "\\\\").gsub('"', '\\"')
  end

end

COMMANDS.each_pair do |name, callback|
  MPDClient.add_command(name, callback)
end