#!/usr/bin/python

# =============================================================================
#
#    Remuco - A remote control system for media players.
#    Copyright (C) 2006-2009 by the Remuco team, see AUTHORS.
#
#    This file is part of Remuco.
#
#    Remuco is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Remuco is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with Remuco.  If not, see <http://www.gnu.org/licenses/>.
#
# =============================================================================

"""Winamp adapter for Remuco, implemented as an executable script.

TODO:
 - ctrl_*() should only re-poll the values that have changed
 - run as a Winamp in-process plugin and get asynchronous notification of player
   state changes (IPC_CB_MISC)
 - implement queue management (request_queue) using api_queue.h:
     http://stashbox.org/549052/api_queue.h
   (will most likely require us to be an in-process plugin)
 - implement album art (find_image) using api_albumart.h
     http://forums.winamp.com/showthread.php?postid=2593620#post2593620
   (requires in-process)
 - implement Media Library smart views
"""

import os.path
import urllib
import remuco
from remuco import log
import winamp

IA_JUMP = remuco.ItemAction("Jump to")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
PLAYLIST_ACTIONS = (IA_JUMP, IA_REMOVE)

FA_APPEND = remuco.ItemAction("Append", multiple=True)
FA_PLAY = remuco.ItemAction("Play", multiple=True)
FILE_ACTIONS = (FA_APPEND, FA_PLAY)

LA_LOAD = remuco.ListAction("Load")
MLIB_LIST_ACTIONS = (LA_LOAD,)

IA_APPEND = remuco.ItemAction("Enqueue", multiple=True)
IA_PLAY = remuco.ItemAction("Play now", multiple=True)
MLIB_ITEM_ACTIONS = (IA_APPEND, IA_PLAY)

SEARCH_MASK = ["Artist", "Title", "Album", "Genre", "Year"]

class WinampAdapter(remuco.PlayerAdapter):
    def __init__(self):
        super(WinampAdapter, self).__init__(
            "Winamp",
            playback_known=True,
            volume_known=True,
            repeat_known=True,
            shuffle_known=True,
            progress_known=True,
            max_rating=5,
            file_actions=FILE_ACTIONS,
            mime_types=["audio"],
            search_mask=SEARCH_MASK
        )

    def start(self):
        super(WinampAdapter, self).start()
        self.winamp = winamp.Winamp()

    def stop(self):
        self.winamp.detach()
        super(WinampAdapter, self).stop()

    def poll(self):
        w2r_status = {
            winamp.PLAYBACK_NOT_PLAYING: remuco.PLAYBACK_STOP,
            winamp.PLAYBACK_PLAYING: remuco.PLAYBACK_PLAY,
            winamp.PLAYBACK_PAUSE: remuco.PLAYBACK_PAUSE
        }
        self.update_playback(w2r_status[self.winamp.getPlaybackStatus()])
        self.update_repeat(self.winamp.getRepeat())
        self.update_shuffle(self.winamp.getShuffle())
        
        id = self.winamp.getPlaylistPosition()
        filename = self.winamp.getPlaylistFile(id)
        info = {
            remuco.INFO_ARTIST: self.winamp.getExtendedFileInfo(filename, "artist"),
            remuco.INFO_ALBUM: self.winamp.getExtendedFileInfo(filename, "album"),
            remuco.INFO_TITLE: self.winamp.getExtendedFileInfo(filename, "title"),
            remuco.INFO_GENRE: self.winamp.getExtendedFileInfo(filename, "genre"),
            remuco.INFO_YEAR: self.winamp.getExtendedFileInfo(filename, "year"),
            remuco.INFO_BITRATE: self.winamp.getCurrentPlayingInfo(winamp.INFO_BITRATE),
            remuco.INFO_RATING: self.winamp.getRating(),
            remuco.INFO_LENGTH: self.winamp.getPlayingTrackLength(),
        }
        if not info[remuco.INFO_TITLE]:
          info[remuco.INFO_TITLE] = os.path.basename(
            self.winamp.getPlaylistFile(id)
          )
        self.update_item(id, info, None)
        
        self.update_position(id)
        
        self.update_progress(self.winamp.getPlayingTrackPosition() / 1000,
            self.winamp.getPlayingTrackLength())

        self.update_volume(self.winamp.getVolume() * 100 / 255)

    def ctrl_toggle_playing(self):
        status = self.winamp.getPlaybackStatus()
        if status == winamp.PLAYBACK_NOT_PLAYING:
            self.winamp.play()
        elif status in (winamp.PLAYBACK_PLAYING, winamp.PLAYBACK_PAUSE):
            self.winamp.pause()
        else:
            raise ValueError("Unexpected Winamp status value %d" % status)
        self.poll()

    def ctrl_toggle_repeat(self):
        self.winamp.setRepeat(not self.winamp.getRepeat())
        self.poll()

    def ctrl_toggle_shuffle(self):
        self.winamp.setShuffle(not self.winamp.getShuffle())
        self.poll()

    def ctrl_toggle_fullscreen(self):
        self.winamp.toggleFullscreen()
        self.poll()

    def ctrl_next(self):
        self.winamp.next()
        self.poll()

    def ctrl_previous(self):
        self.winamp.previous()
        self.poll()

    def ctrl_seek(self, direction):
        self.winamp.seek(self.winamp.getPlayingTrackPosition() + 5000 * direction)
        self.poll()

    def ctrl_rate(self, rating):
        self.winamp.setRating(rating)
        self.poll()

    def ctrl_volume(self, direction):
        scale = 5 * 255 / 100
        new_volume = self.winamp.getVolume() + scale * direction
        # clamp to 0-255
        new_volume = min(max(new_volume, 0), 255)
        self.winamp.setVolume(new_volume)
        self.poll()

    def request_playlist(self, reply):
        titles = self.winamp.getPlaylistTitles()
        reply.ids = range(len(titles))
        reply.names = titles
        reply.item_actions = PLAYLIST_ACTIONS
        reply.send()

    def action_playlist_item(self, action_id, positions, ids):
        if action_id == IA_JUMP.id:
            ids = map(int, ids)

            self.winamp.setPlaylistPosition(ids[0])
            self.winamp.play()
            
            self.poll()
        elif action_id == IA_REMOVE.id:
            ids = map(int, ids)

            offset = 0
            for id in sorted(ids):
              self.winamp.removePlaylistPosition(id - offset)
              offset += 1
            
            self.poll()
        else:
            raise ValueError("Unknown action %r" % action_id)

    def action_files(self, action_id, files, uris):
        if action_id == FA_APPEND.id:
            for file in files:
                self.winamp.enqueueFile(file)
        elif action_id == FA_PLAY.id:
            self.winamp.clearPlaylist()
            for file in files:
                self.winamp.enqueueFile(file)
            self.winamp.setPlaylistPosition(0)
            self.winamp.play()
        else:
            raise ValueError("Unknown file action %r" % action_id)

    def request_mlib(self, reply, path):
        if path == []:
            children = self.winamp.getMediaLibraryChildren(path)
            reply.nested = [title for title, handle in children]
        elif path == ["Playlists"]:
            reply.nested = self.winamp.getMediaLibraryPlaylistTitles()
            reply.list_actions = MLIB_LIST_ACTIONS
        elif len(path) == 2 and path[0] == "Playlists":
            filenames = self.winamp.getMediaLibraryPlaylistFilenames(path[1])
            # could use getExtendedFileInfo but that might spin the disk a lot
            # if the metadata aren't already cached
            reply.names = [
                os.path.splitext(os.path.basename(f))[0] for f in filenames
            ]
            reply.ids = range(len(reply.names))
            reply.item_actions = MLIB_ITEM_ACTIONS
        elif path == ["Bookmarks"]:
            bookmarks = self.winamp.getBookmarks()
            reply.names = [title for title, path in self.winamp.getBookmarks()]
            reply.ids = range(len(bookmarks))
            reply.item_actions = MLIB_ITEM_ACTIONS

        reply.send()

    def action_mlib_item(self, action_id, path, positions, ids):
        if action_id in (IA_APPEND.id, IA_PLAY.id):
            if len(path) == 2 and path[0] == "Playlists":
                filenames = self.winamp.getMediaLibraryPlaylistFilenames(path[1])
                if action_id == IA_PLAY.id:
                    self.winamp.clearPlaylist()
                for filename in map(filenames.__getitem__, map(int, ids)):
                    self.winamp.enqueueFile(filename)
                if action_id == IA_PLAY.id:
                    self.winamp.play()
            elif path == ["Bookmarks"]:
                bookmarks = self.winamp.getBookmarks()
                if action_id == IA_PLAY.id:
                    self.winamp.clearPlaylist()
                for title, filename in map(bookmarks.__getitem__, map(int, ids)):
                    self.winamp.enqueueFile(filename)
                if action_id == IA_PLAY.id:
                    seld.winamp.play()
            else:
                raise ValueError("Invalid action for path %r" % path)
        else:
            raise ValueError("Unknown mlib item action %d" % action_id)

    def action_mlib_list(self, action_id, path):
        if action_id == LA_LOAD.id:
            if len(path) == 2 and path[0] == "Playlists":
                playlist = self.winamp.getMediaLibraryPlaylist(path[1])
                self.winamp.clearPlaylist()
                self.winamp.enqueueFile(playlist)
                self.winamp.play()
            else:
                raise ValueError("Invalid action LA_LOAD for path %r" % path)
        else:
            raise ValueError("Unknown mlib list action %d" % action_id)

    def request_search(self, reply, query):
        queryString = " & ".join('%s has "%s"' % (mask, urllib.quote(value))
            for mask, value in zip(SEARCH_MASK, query) if value
        )
        items = self.winamp.query(queryString)
        
        self._lastSearchFilenames = []
        for item in items:
            reply.names.append(item.artist + " - " + item.title)
            self._lastSearchFilenames.append(item.filename)
        reply.ids = range(len(reply.names))
        reply.item_actions = FILE_ACTIONS
        reply.send()

    def action_search_item(self, action_id, positions, ids):
        if not hasattr(self, "_lastSearchFilenames"):
            raise ValueError("request_search has not been called")

        files = map(self._lastSearchFilenames.__getitem__, map(int, ids))
        return self.action_files(action_id, files, files)

# =============================================================================
# main (example startup using remuco.Manager)
# =============================================================================

if __name__ == '__main__':
    
    pa = WinampAdapter() # create the player adapter
    mg = remuco.Manager(pa)# # pass it to a manager
    mg.run() # run the manager (blocks until interrupt signal)
