# Copyright (c) 2008-2009 Pablo Flouret <quuxbaz@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met: Redistributions of
# source code must retain the above copyright notice, this list of conditions and
# the following disclaimer. Redistributions in binary form must reproduce the
# above copyright notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the distribution.
# Neither the name of the software nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
import urwid
import xmmsclient
 
from xmmsclient import collections as coll
from xmmsclient.sync import XMMSError
 
import collutil
import commands
import listbox
import mif
import signals
import widgets
import xmms
 
 
class RowColumns(urwid.Columns):
  def __init__(self, song_w, pos, max_pos):
    self.song_w = song_w
    self.pos_w = urwid.Text('', align='right')
    self.pos = -1
    self.max_pos = 0
 
    self.__super.__init__([self.pos_w, self.song_w], focus_column=1, dividechars=1)
 
    self.set_pos(pos, max_pos)
 
  mid = property(lambda self: self.song_w.mid)
 
  def set_pos(self, pos, max_pos):
    if pos != self.pos:
      self.pos = pos
      self.pos_w.set_text('%d.' % (pos+1))
      self._invalidate()
 
    if max_pos != self.max_pos:
      self.max_pos = max_pos
      max_pos_len = len(str(max_pos))
      self.column_types[0] = ('fixed', max_pos_len+1)
      self._invalidate()
 
 
# FIXME: cleanup all the xmms-playlist-changed mess
 
class PlaylistWalker(urwid.ListWalker):
  def __init__(self, pls, format):
    self.pls = pls
    self.format = format
    self.parser = mif.FormatParser(format)
    self.song_widgets = {}
    self.row_widgets = {}
    self.focus = 0
 
    self.feeder = collutil.PlaylistFeeder(self.pls, self.parser.fields())
 
    try:
      self.current_pos = int(self.feeder.collection.attributes.get('position', -1))
    except ValueError:
      self.current_pos = -1
 
    signals.connect('xmms-medialib-entry-changed', self.on_medialib_entry_changed)
    signals.connect('xmms-playlist-current-pos', self.on_xmms_playlist_current_pos)
    signals.connect('xmms-playlist-changed', self.on_xmms_playlist_changed)
 
  def __len__(self):
    return len(self.feeder)
 
  def on_medialib_entry_changed(self, mid):
    if mid in self.song_widgets:
      del self.song_widgets[mid]
      for pos in self.feeder.id_positions(mid):
        try:
          del self.row_widgets[pos]
        except KeyError:
          pass
      signals.emit('need-redraw')
 
  def on_xmms_playlist_changed(self, pls, type, id, pos, newpos):
    if pls != self.pls:
      return
 
    if type != xmmsclient.PLAYLIST_CHANGED_ADD:
      self.row_widgets = {}
 
    self.set_focus(self.focus)
    signals.emit('need-redraw')
 
  def on_xmms_playlist_current_pos(self, pls, pos):
    if pls == self.pls and pos != self.current_pos:
      self.current_pos = pos
      signals.emit('need-redraw')
 
  def get_pos(self, pos):
    mid = self.feeder.position_id(pos)
 
    if pos < 0 or mid is None:
      return None, None
 
    if mid not in self.song_widgets:
      text = self.parser.eval(self.feeder[pos])
      self.song_widgets[mid] = widgets.SongWidget(mid, text)
 
    try:
      w = self.row_widgets[pos]
      w.set_pos(pos, len(self.feeder))
    except KeyError:
      w = self.row_widgets[pos] = RowColumns(self.song_widgets[mid], pos, len(self.feeder))
 
    return w, pos
 
  def set_focus(self, focus):
    if focus <= 0:
      focus = 0
    elif focus >= len(self.feeder):
      focus = len(self.feeder) - 1
 
    self.focus = focus
    self._modified()
 
  def focus_current_pos(self):
    self.set_focus(self.current_pos)
 
  def set_focus_last(self): self.set_focus(len(self.feeder)-1)
  def get_focus(self): return self.get_pos(self.focus)
  def get_prev(self, pos): return self.get_pos(pos-1)
  def get_next(self, pos): return self.get_pos(pos+1)
 
 
class Playlist(listbox.SongListBox):
  context_name = 'playlist'
 
  def __init__(self, app):
    self.__super.__init__(app, [])
 
    self.body.current_pos = -1 # filthy filthy
 
    self.format = 'playlist'
 
    self._walkers = {} # pls => walker
    self.active_pls = self.xs.playlist_current_active()
    self.view_pls = self.active_pls
 
    signals.connect('xmms-collection-changed', self.on_xmms_collection_changed)
    signals.connect('xmms-playlist-loaded', self.load)
    signals.connect('xmms-playlist-changed', self.on_xmms_playlist_changed)
    signals.connect('xmms-playlist-current-pos', self.on_xmms_playlist_current_pos)
 
    self.load(self.active_pls)
 
  def load(self, pls, from_xmms=True):
    focus_active = False
    if pls not in self._walkers:
      self._walkers[pls] = PlaylistWalker(pls, self.app.config.format(self.format))
      focus_active = True
 
    self._set_active_attr(self.body.current_pos, self._walkers[pls].current_pos)
 
    self.body = self._walkers[pls]
 
    if focus_active:
      self.set_focus(self.body.current_pos)
 
    if from_xmms:
      self.active_pls = pls
 
    self.view_pls = pls
    self._invalidate()
 
  def on_xmms_collection_changed(self, pls, type, namespace, newname):
    if namespace == 'Playlists':
      if type == xmmsclient.COLLECTION_CHANGED_RENAME:
        try:
          del self._walkers[pls]
          if pls == self.active_pls:
            self.load(newname)
        except KeyError:
          pass
        signals.emit('need-redraw')
 
  def on_xmms_playlist_changed(self, pls, type, id, pos, newpos):
    try:
      # FIXME
      if pos is None:
        del self._walkers[pls]
        if pls == self.active_pls:
          self.load(pls, from_xmms=False)
    except KeyError:
      pass
 
  def on_xmms_playlist_current_pos(self, pls, pos):
    if pls != self.active_pls:
      return
 
    cp = self.body.current_pos
    self._set_active_attr(cp, pos)
 
    if self._bottom_pos is not None and self._top_pos is not None and \
       cp <= self._bottom_pos and cp >= self._top_pos and \
       (pos > self._bottom_pos or pos < self._top_pos):
      self.set_focus(pos)
 
  def cmd_activate(self, args):
    if args:
      if len(args.split()) > 1:
        raise commands.CommandError("Too many arguments, only one needed")
      try:
        pos = int(args)-1
        if pos < 0 or pos > len(self.body):
          raise ValueError
      except ValueError:
        raise commands.CommandError("valid playlist position required")
    else:
      pos = self.get_focus()[1]
    if pos is not None:
      self.xs.playlist_play(playlist=self.view_pls, pos=pos)
 
  def cmd_goto(self, args):
    if args == 'playing':
      self.body.focus_current_pos()
    else:
      try:
        p = int(args)
      except ValueError:
        return commands.CONTINUE_RUNNING_COMMANDS
      self.set_focus(p)
 
  def cmd_rm(self, args):
    m = self.marked_data
    if not m:
      w, pos = self.get_focus()
      if pos is None:
        return
      m = {pos: self.get_mark_data(pos, w)}
 
    for pos, w in sorted(m.items(), key=lambda e: e[0], reverse=True):
      self.xs.playlist_remove_entry(pos, self.view_pls, sync=False)
 
    self.unmark_all()
 
  def cmd_move(self, args):
    try:
      n = int(args)
    except ValueError:
      if not args:
        n = self.get_focus()[1]+1
      else:
        raise CommandError, "bad argument"
 
    if args and args[0] in ('+', '-'):
      if n > 0:
        self.move_down(n)
      else:
        self.move_up(-n)
    else:
      self.move_abs(n)
 
  def _get_marked_for_move(self, reverse=False):
    m = self.marked_data.items()
    if not m:
      w, pos = self.get_focus()
      if pos is None:
        return
      m = [(pos, self.get_mark_data(pos, w))]
 
    m.sort(key=lambda e: e[0], reverse=reverse)
 
    return m
 
  def move_abs(self, n, m=None):
    m = self._get_marked_for_move()
 
    if not m:
      return
 
    n -= 1
    if n > m[0][0]:
      self.move_down(n-m[0][0], reversed(m))
    else:
      self.move_up(m[0][0]-n, m)
 
  def move_up(self, n, m=None):
    if m is None:
      m = self._get_marked_for_move()
 
    top = 0
    for pos, mid in m:
      dest = pos - n
 
      if dest < top:
        dest = top
        top += 1
 
      self.xs.playlist_move(pos, dest, sync=False)
      if not self.marked_data: # moving only the focused song
        self.set_focus(dest)
      else:
        self.toggle_mark(pos, mid)
        self.toggle_mark(dest, mid)
        # TODO: scroll if moving past first row in view
 
  def move_down(self, n, m=None):
    if m is None:
      m = self._get_marked_for_move(reverse=True)
 
    bottom = len(self.body)-1
    for pos, mid in m:
      dest = pos+n
 
      if dest > bottom:
        dest = bottom
        bottom -= 1
 
      self.xs.playlist_move(pos, dest, sync=False)
      if not self.marked_data: # moving only the focused song
        self.set_focus(dest)
      else:
        self.toggle_mark(pos, mid)
        self.toggle_mark(dest, mid)
        # TODO: scroll if moving past last row in view
 
  def get_mark_data(self, pos, w):
    return w.mid
 
  def get_contexts(self):
    return [self]
 
  def _set_active_attr(self, prevpos, newpos):
    if prevpos != -1:
      self.remove_row_attr(prevpos, 'active')
 
    if newpos != -1:
      self.add_row_attr(newpos, 'active')
 
 
class PlaylistSwitcherWalker(urwid.ListWalker):
  def __init__(self):
    self.xs = xmms.get()
    self.focus = 0
    self.rows = {}
    self.playlists = []
    self.nplaylists = 0
 
    signals.connect('xmms-collection-changed', self.on_xmms_collection_changed)
    signals.connect('xmms-playlist-changed', self.on_xmms_playlist_changed)
 
    self._load()
 
  def __len__(self):
    return self.nplaylists
 
  def _load(self):
    self.playlists = [p for p in sorted(self.xs.playlist_list()) if not p.startswith('_')]
    self.nplaylists = len(self.playlists)
 
  def _reload(self):
    self.rows = {}
    self._load()
    if self.focus >= self.nplaylists:
      self.focus = self.nplaylists-1
    self._modified()
 
  def get_pos(self, pos):
    if pos < 0 or pos >= self.nplaylists:
      return None, None
 
    pls = self.playlists[pos]
 
    if pos not in self.rows:
      self.rows[pos] = widgets.PlaylistWidget(pls)
 
    return self.rows[pos], pos
 
  def get_focus(self):
    return self.get_pos(self.focus)
 
  def set_focus(self, focus):
    if focus <= 0:
      focus = 0
    elif focus >= self.nplaylists:
      focus = self.nplaylists-1
    self.focus = focus
    self._modified()
 
  def get_prev(self, pos):
    return self.get_pos(pos-1)
 
  def get_next(self, pos):
    return self.get_pos(pos+1)
 
  def on_xmms_collection_changed(self, pls, type, namespace, newname):
    if namespace == 'Playlists' and type != xmmsclient.COLLECTION_CHANGED_UPDATE:
      self._reload()
      signals.emit('need-redraw')
 
  def on_xmms_playlist_changed(self, pls, type, id, pos, newpos):
    if pos is None and \
       type in (xmmsclient.PLAYLIST_CHANGED_ADD,
                xmmsclient.PLAYLIST_CHANGED_MOVE,
                xmmsclient.PLAYLIST_CHANGED_REMOVE):
      self._reload()
      signals.emit('need-redraw')
 
 
class PlaylistSwitcher(listbox.MarkableListBox):
  context_name = 'playlist-switcher'
 
  def __init__(self, app):
    self.__super.__init__(PlaylistSwitcherWalker())
 
    self.xs = xmms.get()
    self.app = app
    self.cur_active = self.xs.playlist_current_active()
    self.active_pos = 0
    self._set_active_attr(None, self.cur_active)
 
    signals.connect('xmms-playlist-loaded', self.on_xmms_playlist_loaded)
    signals.connect('xmms-collection-changed', self.on_xmms_collection_changed)
 
  def on_xmms_playlist_loaded(self, pls):
    self._set_active_attr(self.cur_active, pls)
    self.cur_active = pls
    self._invalidate()
    signals.emit('need-redraw')
 
  def on_xmms_collection_changed(self, pls, type, namespace, newname):
    if namespace == 'Playlists' and type != xmmsclient.COLLECTION_CHANGED_UPDATE:
      if pls == self.cur_active:
        self.cur_active = newname
 
      self.clear_attrs()
      self._set_active_attr(None, self.cur_active)
 
      signals.emit('need-redraw')
 
  def _set_active_attr(self, prevpls, newpls):
    try:
      if prevpls:
        prevpos = self.body.playlists.index(prevpls)
      newpos = self.body.playlists.index(newpls)
    except ValueError:
      return # shouldn't happen
 
    if prevpls:
      self.remove_row_attr(prevpos, 'active')
 
    if newpls:
      self.add_row_attr(newpos, 'active')
 
  def cmd_activate(self, args):
    w = self.get_focus()[0]
    if w:
      self.xs.playlist_load(w.name, sync=False)
 
  # FIXME: works like crap
  def cmd_insert(self, args):
    w = self.get_focus()[0]
    if w:
      # this awfulness stems from the fact that you have to use playlist_add_collection,
      # but collections in the playlist namespace don't have order, doh
      # coll2.0 should fix this mess
      idl = coll.IDList()
      cur_active = self.xs.playlist_current_active()
      ids_from = self.xs.playlist_list_entries(w.name, 'Playlists')
      ids_to = self.xs.playlist_list_entries(cur_active, 'Playlists')
 
      for id in ids_to+ids_from:
        idl.ids.append(id)
 
      self.xs.coll_save(idl, cur_active, 'Playlists')
 
  def cmd_rm(self, args):
    w = self.get_focus()[0]
    if w:
      self.xs.playlist_remove(w.name, sync=False)
 
  def cmd_rename(self, args):
    w = self.get_focus()[0]
    if w:
      def rename(widget, new_name):
        self.xs.coll_rename(w.name, new_name, 'Playlists', sync=False)
      if args:
        rename(None, args)
      else:
        self.app.show_prompt('new name: ', rename)
 
  def cmd_new(self, args):
    def create(widget, name):
      if name:
        self.xs.playlist_create(name, sync=False)
    if args:
      create(None, args)
    else:
      self.app.show_prompt('playlist name: ', create)
 
  def get_contexts(self):
    return [self]