#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2010, 2011 Jack Kaliko <efrim@azylum.org>
#
#  This file is part of MPD_sima
#
#  MPD_sima 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.
#
#  MPD_sima 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 MPD_sima.  If not, see <http://www.gnu.org/licenses/>.
#
#

__version__ = '0.3'

# IMPORT#
import re

from difflib import get_close_matches
from locale import getpreferredencoding
from optparse import (OptionParser, OptionValueError, SUPPRESS_HELP)
from os import (environ, chmod, makedirs)
from os import (access, W_OK, F_OK)
from os.path import (join, isdir, isfile, expanduser)
from socket import error as SocketError
from sys import (exit, stdout, stderr)

import lib.simadb
import utils.utils

from lib.track import Track

try:
    from mpd import MPDClient
    MPD = True
except ImportError, err:
    #print 'WARNING: "%s"\npython-mpd module not available.\n' % err
    MPD = False

USAGE = u"""USAGE:  %prog [-h|--help] [options]"""
DESCRIPTION = u"""
simadb_cli helps you to edit entries in your own DB of similarity between
artists."""
DB_NAME = 'sima.db'

def is_file_rwable(option, opt_str, value, parser):# Callbacks
    # Check either file is read/write-able or not
    if isdir(value):
        raise OptionValueError('not a regular file: "%s"' % value)
    if access(value, F_OK) and not access(value, W_OK):
        raise OptionValueError('no write access to "%s"' % value)
    setattr(parser.values, option.dest, value)

def opt_req(parser):
    if parser.values.similarity: return True
    if parser.values.remove_art: return True
    if parser.values.remove_sim: return True
    return False

def check_order(option, opt_str, value, parser):
    # Check 
    opt_required = '"--remove_artist", "--remove_similarity" or "--add_similarity"'
    #print parser.values
    if not opt_req(parser):
        raise OptionValueError(
                'can\'t use %s option before or without %s' % (option,
                    opt_required))
    setattr(parser.values, option.dest, True)

def check_order_chknames(option, opt_str, value, parser):
    opt_required = '"--add_similarity"'
    if not parser.values.similarity:
        raise OptionValueError(
                'can\'t use %s option before or without %s' % (option,
                    opt_required))
    setattr(parser.values, option.dest, True)


# Options list
# pop out 'sw' value before creating OptionParser object.
OPTS = list([
    {
        'sw':['-a', '--add_similarity'],
        'type': 'string',
        'dest':'similarity',
        'help': 'Similarity to add formated as follow: ' +
        ' "art_0,art_1:90,art_2:80..."'},
    {
        'sw': ['-c', '--check_names'],
        'dest': 'check_names',
        'default': False,
        'action': 'callback',
        'callback': check_order_chknames,
        'help': 'Turn on controls of artists names in MPD library.'},
    {
        'sw':['-d', '--dbfile'],
        'type': 'string',
        'dest':'dbfile',
        'action': 'callback',
        'callback': is_file_rwable,
        'help': 'File to read/write database from/to'},
    {
        'sw': ['-r', '--reciprocal'],
        'dest': 'reciprocal',
        'default': False,
        'action': 'callback',
        'callback': check_order,
        'help': 'Turn on reciprocity for similarity relation when add/remove.'},
    {
        'sw':['--remove_artist'],
        'type': 'string',
        'dest': 'remove_art',
        'metavar': '"ARTIST TO REMOVE"',
        'help': 'Remove an artist from DB (main artist entries).'},
    {
        'sw':['--remove_similarity'],
        'type': 'string',
        'dest': 'remove_sim',
        'metavar': '"MAIN ART,SIMI ART"',
        'help': 'Remove an similarity relation from DB (main artist <=> similar artist).'},
    {
        'sw':['-v', '--view_artist'],
        'type': 'string',
        'metavar': '"ARTIST NAME"',
        'dest':'view',
        'help': 'View an artist from DB.'},
    {
        'sw':['--view_all'],
        'action': 'store_true',
        'dest': 'view_all',
        'help': 'View all entries.'},
    {
        'sw': ['-S', '--host'],
        'type': 'string',
        'dest': 'mpdhost',
        'default': None,
        'help': 'MPD host, as IP or FQDN (default: localhost|MPD_HOST).'},
    {
        'sw': ['-P', '--port'],
        'type': 'int',
        'dest': 'mpdport',
        'default': None,
        'help': 'Port MPD in listening on (default: 6600|MPD_PORT).'},
    {
        'sw': ['--password'],
        'type': 'string',
        'dest': 'passwd',
        'default': None,
        'help': SUPPRESS_HELP},
    {
        'sw': ['--view_bl'],
        'action': 'store_true',
        'dest': 'view_bl',
        'help': 'View black list.'},
    {
        'sw': ['--remove_bl'],
        'type': 'int',
        'dest': 'remove_bl',
        'help': 'Suppress a black list entry, by row id. Use --view_bl to get row id.'},
    {
        'sw': ['--bl_art'],
        'type': 'string',
        'dest': 'bl_art',
        'metavar': 'ARTIST_NAME',
        'help': 'Black list artist.'},
    {
        'sw': ['--bl_curr_art'],
        'action': 'store_true',
        'dest': 'bl_curr_art',
        'help': 'Black list currently playing artist.'},
    {
        'sw': ['--bl_curr_alb'],
        'action': 'store_true',
        'dest': 'bl_curr_alb',
        'help': 'Black list currently playing album.'},
    {
        'sw': ['--bl_curr_trk'],
        'action': 'store_true',
        'dest': 'bl_curr_trk',
        'help': 'Black list currently playing track.'},
    {
        'sw':['--purge_hist'],
        'action': 'store_true',
        'dest': 'do_purge_hist',
        'help': 'Purge play history.'}])


class SimaDB_CLI(object):
    """Command line management.
    """

    def __init__(self):
        self.dbfile = self._get_default_dbfile()
        self.parser = None
        self.options = dict({})
        self.localencoding = 'utf8'
        self._get_encoding()
        self._upgrade()
        self.main()
        #try:
            #self.main()
        #except Exception, err:
            #self._print_encoded('ERROR: %s' % str(err))

    def _get_encoding(self):
        """Get local encoding"""
        self.localencoding = getpreferredencoding()

    def _get_mpd_env_var(self):
        """
        MPD host/port environement variables are used if command line does not
        provide host|port|passwd.
        """
        host, port, passwd = utils.utils.get_mpd_environ()
        if self.options.passwd is None and passwd:
            self.options.passwd = passwd
        if self.options.mpdhost is None:
            if host:
                self.options.mpdhost = host
            else:
                self.options.mpdhost = 'localhost'
        if self.options.mpdport is None:
            if port:
                self.options.mpdport = port
            else:
                self.options.mpdport = 6600

    def _upgrade(self):
        """Upgrades DB if necessary, create one if not existing."""
        if not isfile(self.dbfile): # No db file
            return
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db.upgrade()

    def _declare_opts(self):
        """
        Declare options in OptionParser object.
        """
        version = "simadb_cli v%s" % __version__
        self.parser = OptionParser(version=version,
                                   usage=USAGE,
                                   prog='simadb_cli',
                                   description=DESCRIPTION)

        # Add all options declare in OPTS
        for opt in OPTS:
            opt_names = opt.pop('sw')
            self.parser.add_option(*opt_names, **opt)

    def _get_default_dbfile(self):
        """
        Use XDG directory standard if exists
        else use "HOME/.local/share/mpd_sima/"
        http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
        """
        homedir = expanduser('~')
        dirname = 'mpd_sima'
        if environ.get('XDG_DATA_HOME'):
            data_dir = join(environ.get('XDG_DATA_HOME'), dirname)
        else:
            data_dir = join(homedir, '.local', 'share', dirname)
        if not isdir(data_dir):
            makedirs(data_dir)
            chmod(data_dir, 0700)
        return join(data_dir, DB_NAME)

    def _convert(self, cli_str):
        """Convert from whatever local encoding is to unicode"""
        return unicode(cli_str, self.localencoding)

    def _get_mpd_client(self):
        """"""
        if not MPD:
            print 'python-mpd module not available, cannot retrieve current artist!'
            return False
        # TODO: encode properly host name
        host = self.options.mpdhost
        port = self.options.mpdport
        cli = MPDClient()
        try:
            cli.connect(host=host, port=port)
        except SocketError, err:
            mess = unicode('ERROR: fail to connect MPD (host: %s:%s): %s' % (
                    host, port, err))
            self._print_encoded(mess, where=2)
            exit(1)
        return cli

    def _print_encoded(self, message, where=1):
        """Encode and print message"""
        mess_u = unicode(message).encode(self.localencoding)
        if where == 1:
            print >> stdout, mess_u
        elif where == 2:
            print >> stderr, mess_u

    def _create_db(self):
        """Create database if necessary"""
        if isfile(self.dbfile):
            return
        self._print_encoded('Creating database!')
        open(self.dbfile, 'a').close()
        lib.simadb.SimaDB(db_path=self.dbfile).create_db()

    def _get_art_from_db(self, art):
        """Return (id, name, self...) from DB or None is not in DB"""
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        art_db = db.get_artist(art, add_not=True)
        if not art_db:
            self._print_encoded('ERROR: "%s" not in data base!' % art, where=2)
            return None
        return art_db

    def _control_similarity(self):
        """
         * Regex check of command line similarity
         * Controls artist presence in MPD library
        """
        usage = unicode('USAGE: "main artist,similar artist:<match score>,other' +
                'similar artist:<match score>,..."')
        cli_sim = self.options.similarity.decode(self.localencoding)
        pattern = '^([^,]+?),([^:,]+?:\d{1,2},?)+$'
        regexp = re.compile(pattern, re.U).match(cli_sim)
        if not regexp:
            mess = unicode('ERROR: similarity badly formated: "%s"' % cli_sim)
            self._print_encoded(mess, where=2)
            self._print_encoded(usage, where=2)
            exit(1)
        if self.options.check_names:
            if not self._control_artist_names():
                mess = unicode('ERROR: some artist names not found in MPD library!')
                self._print_encoded(mess, where=2)
                exit(1)

    def _control_artist_names(self):
        """Controls artist names exist in MPD library"""
        mpd_cli = self._get_mpd_client()
        artists_list_utf8 = mpd_cli.list('artist')
        sim_formated = self._parse_similarity()
        control = True
        if sim_formated[0].encode('UTF-8') not in artists_list_utf8:
            mess = unicode('WARNING: Main artist not found in MPD: %s' % sim_formated[0])
            self._print_encoded(mess, where=1)
            control = False
        for sart in sim_formated[1]:
            art = sart.get('artist')
            if art.encode('UTF-8') not in artists_list_utf8:
                mess = unicode('WARNING: Similar artist not found in MPD: %s' % art)
                self._print_encoded(mess, where=1)
                control = False
        mpd_cli.disconnect()
        return control

    def _parse_similarity(self):
        """Parse command line option similarity"""
        cli_sim = self.options.similarity.strip(',').split(',')
        sim = list([])
        main = self._convert(cli_sim[0])
        for art in cli_sim[1:]:
            artist = self._convert(art.split(':')[0])
            score = int(art.split(':')[1])
            sim.append({'artist': artist, 'score': score})
        return (main, sim)

    def _print_main_art(self, art=None):
        """Print entries, art as main artist."""
        if not art:
            art = self._convert(self.options.view)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        art_db = self._get_art_from_db(art)
        if not art_db: return
        sims = list([])
        [sims.append(a) for a in db._get_similar_artists_from_db(art_db[0])]
        if len(sims) == 0:
            return False
        self._print_encoded('"%s" similarities:' % art, where=1)
        for art in sims:
            mess = unicode('  - %2i %s' % (art.get('score'), art.get('artist')))
            self._print_encoded(mess, where=1)
        return True

    def _remove_sim(self, art1_db, art2_db):
        """Remove single similarity between two artists."""
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        similarity = db._get_artist_match(art1_db[0], art2_db[0])
        if similarity == 0:
            return False
        db._remove_relation_between_2_artist(art1_db[0], art2_db[0])
        mess = unicode('Remove: "%s" "%s:%2i"' % (art1_db[1], art2_db[1],
                similarity))
        self._print_encoded(mess, where=1)
        return True

    def _revert_similarity(self, sim_formated):
        """Revert similarity string (for reciprocal editing - add)."""
        main_art = sim_formated[0]
        similars = sim_formated[1]
        for similar in similars:
            yield (similar.get('artist'),
                [{'artist':main_art, 'score':similar.get('score')}])

    def bl_artist(self):
        """Black list artist"""
        mpd_cli = self._get_mpd_client()
        if not mpd_cli:
            return False
        artists_list_utf8 = mpd_cli.list('artist')
        # Unicode cli given artist name
        cli_artist_to_bl = self._convert(self.options.bl_art)
        if cli_artist_to_bl.encode('UTF-8') not in artists_list_utf8:
            self._print_encoded('Artist not found in MPD library.')
            match = get_close_matches(cli_artist_to_bl.encode('UTF-8'),
                    artists_list_utf8, 50, 0.78)
            if match:
                self._print_encoded(u'You may be refering to %s' %
                        u'/'.join([unicode(m_a, 'UTF-8') for m_a in match]))
            return False
        self._print_encoded(u'Black listing artist: %s' % cli_artist_to_bl)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db.get_bl_artist(cli_artist_to_bl)

    def bl_current_artist(self):
        """Black list current artist"""
        mpd_cli = self._get_mpd_client()
        if not mpd_cli:
            return False
        artist = unicode(mpd_cli.currentsong().get('artist', ''), 'UTF-8')
        if not artist:
            self._print_encoded('No artist found.')
            return False
        self._print_encoded(u'Black listing artist: %s' % artist)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db.get_bl_artist(artist)

    def bl_current_album(self):
        """Black list current artist"""
        mpd_cli = self._get_mpd_client()
        if not mpd_cli:
            return False
        track = Track(**mpd_cli.currentsong())
        if not track.album:
            self._print_encoded('No album set for this track: %s' % track)
            return False
        self._print_encoded(u'Black listing album: %s' % track.get_album())
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db.get_bl_album(track)

    def bl_current_track(self):
        """Black list current artist"""
        mpd_cli = self._get_mpd_client()
        if not mpd_cli:
            return False
        track = Track(**mpd_cli.currentsong())
        self._print_encoded(u'Black listing track: %s' % track)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db.get_bl_track(track)

    def purge_history(self):
        """Purge all entries in history"""
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        self._print_encoded(u'Purging history...')
        db.purge_history(duration=int(0))
        self._print_encoded(u'done.')
        self._print_encoded(u'Cleaning database...')
        db.clean_database()
        self._print_encoded(u'done.')

    def view(self):
        """Print out entries for an artist."""
        art = self._convert(self.options.view)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        art_db = self._get_art_from_db(art)
        if not art_db: return
        if not self._print_main_art():
            mess = unicode('"%s" present in DB but not as a main artist' % art)
            self._print_encoded(mess, where=1)
        else: self._print_encoded('', where=1)
        art_rev = list([])
        [art_rev.append(a) for a in db._get_reverse_similar_artists_from_db(art_db[0])]
        if not art_rev: return
        mess = unicode('%s" appears as similar for the following artist(s): %s' %
                (art,', '.join(art_rev)))
        self._print_encoded(mess, where=1)
        [self._print_main_art(a) for a in art_rev]

    def view_all(self):
        """Print out all entries."""
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        for art in db.get_artists():
            if not art[0]: continue
            self._print_main_art(art=art[0])

    def view_bl(self):
        """Print out black list."""
        # TODO: enhance output formating
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        for bl_e in db.get_black_list():
            self._print_encoded('\t# '.join([unicode(e) for e in bl_e]))

    def remove_similarity(self):
        """Remove entry"""
        cli_sim = self._convert(self.options.remove_sim)
        pattern = '^([^,]+?),([^,]+?,?)$'
        regexp = re.compile(pattern, re.U).match(cli_sim)
        if not regexp:
            self._print_encoded('ERROR: similarity badly formated: "%s"' % cli_sim, where=2)
            self._print_encoded('USAGE: A single relation between two artists is expected here.', where=2)
            self._print_encoded('USAGE: "main artist,similar artist"', where=2)
            exit(1)
        arts = cli_sim.split(',')
        if len(arts) != 2:
            self._print_encoded('ERROR: unknown error in similarity format', where=2)
            self._print_encoded('USAGE: "main artist,similar artist"', where=2)
            exit(1)
        art1_db = self._get_art_from_db(arts[0].strip())
        art2_db = self._get_art_from_db(arts[1].strip())
        if not art1_db or not art2_db: return
        self._remove_sim(art1_db, art2_db)
        if not self.options.reciprocal:
            return
        self._remove_sim(art2_db, art1_db)

    def remove_artist(self):
        """ Remove artist in the DB."""
        deep = False
        art = self._convert(self.options.remove_art)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        art_db = self._get_art_from_db(art)
        if not art_db: return False
        self._print_encoded('Removing "%s" from database' % art, where=1)
        if self.options.reciprocal:
            self._print_encoded('reciprocal option used, performing deep remove!')
            deep = True
        db._remove_artist(art_db[0], deep=deep)

    def remove_black_list_entry(self):
        """"""
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db._remove_bl(int(self.options.remove_bl))

    def write_simi(self):
        """Write similarity to DB.
        """
        self._create_db()
        sim_formated = self._parse_similarity()
        self._print_encoded('About to update DB with: "%s": %s' % sim_formated)
        db = lib.simadb.SimaDB(db_path=self.dbfile)
        db._update_similar_artists(*sim_formated)
        if self.options.reciprocal:
            self._print_encoded('...and with reciprocal combinations as well.')
            for sim_formed_rec in self._revert_similarity(sim_formated):
                db._update_similar_artists(*sim_formed_rec)

    def main(self):
        """
        Parse command line and run actions.
        """
        self._declare_opts()
        (self.options, args) = self.parser.parse_args()
        self._get_mpd_env_var()
        if self.options.dbfile:
            self.dbfile = self.options.dbfile
            self._print_encoded('Using db file: %s' % self.dbfile)
        if self.options.reciprocal:
            self._print_encoded('Editing reciprocal similarity')
        if self.options.bl_art:
            self.bl_artist()
            return
        if self.options.bl_curr_art:
            self.bl_current_artist()
            return
        if self.options.bl_curr_alb:
            self.bl_current_album()
            return
        if self.options.bl_curr_trk:
            self.bl_current_track()
            return
        if self.options.view_bl:
            self.view_bl()
            return
        if self.options.remove_bl:
            self.remove_black_list_entry()
            return
        if self.options.similarity:
            self._control_similarity()
            self.write_simi()
            return
        if self.options.remove_art:
            self.remove_artist()
            return
        if self.options.remove_sim:
            self.remove_similarity()
            return
        if self.options.view:
            self.view()
            return
        if self.options.view_all:
            self.view_all()
        if self.options.do_purge_hist:
            self.purge_history()
        exit(0)


def main():
    SimaDB_CLI()

# Script starts here
if __name__ == '__main__':
    main()

# VIM MODLINE
# vim: ai ts=4 sw=4 sts=4 expandtab
