#! /usr/local/bin/python2.5
# Copyright (c) 2003,2004 Guilherme Salgado
# All rights reserved.
#
# This file is part of coverhunter.
# 
# coverhunter 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 2 of the License, or
# (at your option) any later version.
# 
# coverhunter 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 coverhunter; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Author: Guilherme Salgado <salgado@async.com.br>
#

import os, sys, tempfile, threading, traceback, urllib2
DEBUG = False
sys.path.append('/usr/local/lib/coverhunter')

import Image
import pygtk ; pygtk.require('2.0')
import gobject, gtk
from gtk import glade, gdk

from album import Album, guess_album_info
from amazon import Bag
import config
from libhunter import *
from source import AlbumSearch

COVER_DISPLAY_SIZE = (300, 300)
THREADS = 5
condition = threading.Condition()
scheduled_searches = {}
album_search = AlbumSearch()
empty_album = Album('', '', '')
_version = "0.2"


class MainWindow:
    def __init__(self):
        # start all worker threads. these threads will continuously pop an
        # item from scheduled_searches and do the search. if 
        # scheduled_searches is empty, all threads will wait() until we add
        # new items.
        for x in range(THREADS):
            w = Worker(self.append_found_cover, self.search_finished)
            w.setDaemon(True)
            w.start()
        # the TreeIter that points to the selected album.
        self.selected_album = None
        # the TreeIter that points to the selected cover.
        self.selected_cover = None

        gladefile = find_datafile('CoverHunter.glade')
        self.wtree = glade.XML(gladefile)
        widgets = ['cover_image', 'artist', 'album_name', 'hpaned',
                   'main_window', 'fetch', 'set_cover', 'fetch_for_all',
                   'image_eventbox', 'next', 'previous']
        map(lambda w: setattr(self, w, self.wtree.get_widget(w)), widgets)
        self.selection_sensitive = [self.next, self.previous, self.fetch]

        self.image_eventbox.connect('drag-data-get', self.drag_data_get)
        self.image_eventbox.drag_source_set(gdk.BUTTON1_MASK,
                                            [('text/plain', 0, 0)],
                                            gdk.ACTION_COPY)
        self.hpaned.set_position(340)
        self.wtree.signal_autoconnect(self)
        self.set_cover.set_sensitive(False)
        for w in self.selection_sensitive:
            w.set_sensitive(False)

        self.attach_lists()

    def drag_data_get(self, widget, context, selection, info, timestamp):
        cover = self.cover_list.get_cover(self.selected_cover)
        url = "file://" + cover.coverfile
        selection.set(selection.target, 8, url)

    def attach_lists(self):
        self.album_list = AlbumList()
        selection = self.album_list.get_selection()
        selection.connect('changed', self.on_album_selected)
        self.album_list.connect('row-activated', self.on_fetch_clicked)
        placeholder = self.wtree.get_widget('albums_placeholder')
        placeholder.add(self.album_list)

        self.cover_list = CoverList()
        selection = self.cover_list.get_selection()
        selection.connect('changed', self.on_cover_selected)
        placeholder = self.wtree.get_widget('covers_placeholder')
        placeholder.add(self.cover_list)

    def run(self):
        self.main_window.show_all()
        gdk.threads_init()
        gdk.threads_enter()
        gtk.main()
        gdk.threads_leave()

    def visit(self, root, dirname, names):
        while gtk.events_pending():
            gtk.main_iteration()
        path = dirname
        dirname = dirname.replace(root, "")
        dirname = dirname[1:]   # remove starting /
        artist, album_name = guess_album_info(dirname)
        for name in names:
            if os.path.isdir(os.path.join(path, name)):
                if not (artist and album_name):
                    # this is probably a dir with some albums inside it.
                    return
        self.album_list.append(Album(path, album_name, artist))

    def set_artist_and_album(self, album):
        if not album:
            self.artist.set_text('')
            self.album_name.set_text('')
        else:
            self.artist.set_text(album.artist)
            self.album_name.set_text(album.name)

    def search_finished(self, iter, album):
        # this method is a callback and will be called by workers to
        # notify us that the search for one specific album was finished.
        gdk.threads_enter()
        if album.has_cover():
            coverfile = album.get_cover()
            pixbuf = gdk.pixbuf_new_from_file(coverfile)
            pixbuf = pixbuf.scale_simple(24, 24, gdk.INTERP_BILINEAR)
        else:
            coverfile = find_datafile('picture.png')
            pixbuf = gdk.pixbuf_new_from_file(coverfile)
        self.album_list.set_row_icon(iter, pixbuf)

        # check if the selection changed.
        if self.album_list.iter_is_selected(iter):
            self.fetch.set_sensitive(True)
        
        gdk.threads_leave()

    def schedule_search(self, iter):
        album = self.album_list.get_album(iter)
        condition.acquire()
        if album in scheduled_searches:
            condition.release()
            return
        scheduled_searches.update({album: iter})
        condition.notify()
        condition.release()
        self.cover_list.clear()
        if album.has_cover():
            self.cover_list.append(album.cover)
            self.cover_list.select_path(0)
        album.clear_tmp_covers()
        pixbuf = gdk.pixbuf_new_from_file(find_datafile('searching.png'))
        self.album_list.set_row_icon(iter, pixbuf)

    def append_found_cover(self, iter, cover):
        # this method will append the found cover to self.cover_list if the
        # selected row is the row pointed by iter.
        # worker threads will call it for every cover they found. that's
        # why we need to call threads_enter()/leave().
        gdk.threads_enter()
        if self.album_list.iter_is_selected(iter):
            self.cover_list.append(cover)
        gdk.threads_leave()

    #
    # signal handler methods
    #

    def on_album_selected(self, selection):
        model, iter = selection.get_selected()
        self.cover_image.set_from_file(None)
        self.cover_list.clear()
        self.selected_album = iter
        if not iter:
            # no row is selected
            self.set_artist_and_album(empty_album)
            for w in self.selection_sensitive:
                w.set_sensitive(False)
            return

        for w in self.selection_sensitive:
            w.set_sensitive(True)
        album = self.album_list.get_album(iter)
        self.set_artist_and_album(album)
        self.cover_list.fill(album.get_all_covers())

    def on_cover_selected(self, selection):
        model, iter = selection.get_selected()
        if not iter:
            self.set_cover.set_sensitive(False)
            self.cover_image.set_from_file(None)
            self.selected_cover = None
            return
        self.set_cover.set_sensitive(True)
        cover = self.cover_list.get_cover(iter)
        self.selected_cover = iter
        pixbuf = gdk.pixbuf_new_from_file(cover.coverfile)
        size = (pixbuf.get_width(), pixbuf.get_height())
        if size > COVER_DISPLAY_SIZE:
            pixbuf = pixbuf.scale_simple(COVER_DISPLAY_SIZE[0], 
                                         COVER_DISPLAY_SIZE[1],
                                         gtk.gdk.INTERP_BILINEAR)
        self.cover_image.set_from_pixbuf(pixbuf)

    def on_artist_changed(self, widget):
        if self.selected_album:
            album = self.album_list.get_album(self.selected_album)
            album.artist = widget.get_text()

    def on_album_name_changed(self, widget):
        if self.selected_album:
            album = self.album_list.get_album(self.selected_album)
            album.name = widget.get_text()

    def on_open_activate(self, widget):
        ds = DirSelection(self.main_window)
        ds.run()
        if ds.root is not None:
            # OK clicked
            self.album_list.clear()
            os.path.walk(ds.root, self.visit, ds.root)

    def on_set_amazon_token_activate(self, widget):
        d = SetTokenDialog(self.main_window)
        if d.run() == gtk.RESPONSE_OK and d.token:
            config.save_amazon_token(d.token)
        d.destroy()

    def on_fetch_clicked(self, *args):
        assert self.selected_album
        self.fetch.set_sensitive(False)
        self.schedule_search(self.selected_album)

    def on_fetch_for_all_clicked(self, widget):
        self.album_list.model.foreach(lambda m, p, i: self.schedule_search(i))

    def on_set_cover_clicked(self, widget):
        assert self.selected_cover, self.selected_album
        album = self.album_list.get_album(self.selected_album)
        cover = self.cover_list.get_cover(self.selected_cover)
        album.set_cover(cover)
        pixbuf = gdk.pixbuf_new_from_file(cover.coverfile)
        pixbuf = pixbuf.scale_simple(24, 24, gdk.INTERP_BILINEAR)
        self.album_list.set_row_icon(self.selected_album, pixbuf)

    def on_previous_clicked(self, widget):
        # go to previous album without cover set
        (node,) = self.album_list.model.get_path(self.selected_album)
        while node:
            node -= 1
            iter = self.album_list.model.get_iter((node,))
            if not self.album_list.get_album(iter).has_cover():
                self.album_list.scroll_to_cell(node)
                self.album_list.select_iter(iter)
                return

    def on_next_clicked(self, widget):
        # go to next album without cover set
        (node,) = self.album_list.model.get_path(self.selected_album)
        node += 1
        rows = len(self.album_list.model)
        while node < rows:
            iter = self.album_list.model.get_iter(node)
            if not self.album_list.get_album(iter).has_cover():
                self.album_list.scroll_to_cell(node)
                self.album_list.select_iter(iter)
                return
            node += 1

    def on_about_activate(self, widget):
        about_dialog(self.main_window, _version)

    def quit(self, *args):
        # remove all temporary cover files
        map(lambda a: a.clear_tmp_covers(), self.album_list.dump_albums())
        gtk.main_quit()


class Worker(threading.Thread):
    def __init__(self, callback, finish_cb):
        threading.Thread.__init__(self)
        # this callback will be called for each cover we find...
        self.callback = callback
        # ... and this one will be called when the search for an album is
        # finished.
        self.finish_cb = finish_cb

    def check_coverfile(self, coverfile):
        if not coverfile:
            return False
        try:
            # Check if image is not null.
            i = Image.open(coverfile)
            if i.size not in [(1, 1), (0, 0)]:
                return True
        except:
            pass
        return False

    def run(self):
        while True:
            condition.acquire()
            while len(scheduled_searches) < 1:
                condition.wait()
            album, iter = scheduled_searches.popitem()
            condition.release()

            try:
                # do the search
                for cover in album_search.find(album):
                    cover.get_image()
                    if self.check_coverfile(cover.coverfile):
                        album.found_covers.append(cover)
                        self.callback(iter, cover)
                    else:
                        os.unlink(cover.coverfile)
            except:
                # do not let this thread die with an uncaught exception.
                if DEBUG:
                    print "Shoot me again / I ain't dead yet"
                    traceback.print_exc()
                self.finish_cb(iter, album)

            self.finish_cb(iter, album)


class CoverList(ListView):
    COLUMN_NAME = 0
    COLUMN_COVER = 1

    def __init__(self):
        # This list stores a string with artist and album, an amazon.Bag
        # instance which has the URL of the cover image, and the filename
        # of the downloaded cover (when it was downloaded)
        ListView.__init__(self, True, str, object)
        self.nocover = Bag()
        self.nocover.ProductName = 'No Cover Found'
        self.nocover.coverfile = find_datafile('nocover.gif')

    def fill(self, covers):
        if not covers:
            self.append(self.nocover)
        else:
            map(lambda c: self.append(c), covers)
        self.scroll_to_cell(0)
        self.select_path(0)

    def append(self, cover):
        self.model.append((cover.get_full_name(), cover))

    def create_columns(self):
        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn("Found Covers", cell,
                                    text=self.COLUMN_NAME)
        column.set_sort_column_id(self.COLUMN_NAME)
        self.append_column(column)

    def get_cover(self, iter):
        return self.model.get_value(iter, self.COLUMN_COVER)


class AlbumList(ListView):
    COLUMN_ICON = 0
    COLUMN_NAME = 1
    COLUMN_ALBUM = 2

    def __init__(self):
        ListView.__init__(self, True, gdk.Pixbuf, str, object)
        self.enable_model_drag_dest([('text/plain', 0, 0),
                                     ('TEXT', 0, 1),
                                     ('STRING', 0, 2),
                                     ('text/uri-list', 0, 3)],
                                    gtk.gdk.ACTION_COPY)
        self.connect("drag-data-received", self.drag_data_received)

    def append(self, album):
        icon = None
        if album.has_cover():
            icon = gdk.pixbuf_new_from_file(album.get_cover())
            icon = icon.scale_simple(24, 24, gtk.gdk.INTERP_BILINEAR)
        self.model.append((icon, album.get_full_str(), album))

    def create_columns(self):
        cell = gtk.CellRendererPixbuf()
        column = gtk.TreeViewColumn("", cell, pixbuf=self.COLUMN_ICON)
        self.append_column(column)
        cell = gtk.CellRendererText()
        column = gtk.TreeViewColumn("Album", cell, text=self.COLUMN_NAME)
        column.set_sort_column_id(self.COLUMN_NAME)
        self.append_column(column)

    def drag_data_received(self, treeview, context, x, y, selection, 
                           info, timestamp):
        dummy, path = tempfile.mkstemp()
        img = open(path, 'wb')
        sock = urllib2.urlopen(selection.data)
        img.write(sock.read())
        sock.close()
        img.close()
        row, pos = self.get_dest_row_at_pos(x, y)
        iter = self.model.get_iter(row)
        album = self.get_album(iter)
        bag = Bag()
        bag.ProductName = album.get_full_str()
        bag.coverfile = path
        album.found_covers.append(bag)
        if self.path_is_selected(row):
            # By emiting the 'changed' signal, we make the cover list be
            # updated, and then show the dropped image.
            self.get_selection().emit('changed')

    def dump_albums(self):
        return self.dump_column(self.COLUMN_ALBUM)

    def get_album(self, iter):
        assert iter
        return self.model.get_value(iter, self.COLUMN_ALBUM)

    def set_row_icon(self, iter, pixbuf):
        self.model.set_value(iter, self.COLUMN_ICON, pixbuf)


def find_datafile(filename):
    return os.path.join('/usr/local/share/coverhunter', filename)


if __name__ == "__main__":
    MainWindow().run()

