#!/usr/bin/env python
import pathlib
from pathlib import Path
from urllib import parse
import subprocess
import os
import sys
import random
import re
from os.path import exists
from bisect import bisect_left, bisect_right
import sqlite3

MATCH_RATE = 0.7
songVals = {}

'''
A sorting method for songs. It works by pushing file names with more matches up higher.
'''
def getValueFunction(terms):
    def valOfResult(song):
        val = 0
        sortval = 0
        if song in songVals:
            val, sortval = songVals[song[1]][0]
        else:
            #print(song[1])
            #print('\t', end='')
            for arg in terms:
                i = 1
                for part in song[2:]:
                    part = re.sub('[^A-Za-z ]', '', part).lower()
                    if arg in part:
                        val += 1
                        # This helps make title matches more valuable
                        sortval += 3 - 1
                        #print("%d %s - %s" % (val, arg, part), end="\t")
                    i += 1
            songVals[song[1]] = (val, sortval)
            #print()
        # Double every point received by a playlist
        if song[0] == True:
            val *= 2
        return (val, sortval)
    return valOfResult

'''
Merges certain words into one. This is helpful for voice commands
'''
def mergeWords(argList):
    i = 0
    quoting = False
    while i < len(argList):
        # Merge underscore for handling later
        if quoting:
            if argList[i] != "unquote":
                argList[i - 1] = argList[i - 1] + " " + argList[i]
            else:
                quoting = False
            del argList[i]
            i -= 1
        elif argList[i] == "quote":
            quoting = True
            del argList[i]
        elif i < len(argList) - 1 and argList[i] == "under" and argList[i + 1] == "score":
            argList[i] = "underscore"
            del argList[i + 1]
        elif i < len(argList) - 1 and argList[i] == "play" and argList[i + 1] == "list":
            argList[i] = "playlist"
            del argList[i + 1]
        if argList[i] == "dot":
            argList[i - 1] = argList[i - 1] + "."
            del argList[i]
            i -= 1
        elif argList[i] == "underscore":
            argList[i - 1] = argList[i - 1] + "_"
            del argList[i]
            i -= 1
        elif i < len(argList) - 1 and argList[i] == "dash":
            argList[i] = "-" + argList[i + 1]
            del argList[i + 1]
        elif argList[i - 1].endswith(".") or argList[i - 1].endswith("_"):
            argList[i - 1] = argList[i - 1] + argList[i]
            del argList[i]
            i -= 1
        i += 1
            
    return argList

def extractOptions(args):
    # Common words that are in a lot of song titles that we should avoid
    commonWords = ["the", "off", "for", "from", "with", "and"]
    options = {}
    i = 0
    while i < len(args):
        # Some dashed fields have special meanings.
        if args[i].startswith("-") and args[i] != "-or" and args[i] != "-and" and args[i] != "--or" and args[i] != "--and":
            if args[i].startswith("--"):
                if "=" in args[i]:
                    val = args[i][args[i].index("=") + 1:]
                    if re.sub('\.', '', val).isnumeric():
                        if '.' in val:
                            val = float(val)
                        else:
                            val = int(val)
                    options[args[i][2:args[i].index("=")]] = val
                else:
                    options[args[i][2:]] = True
            # These two have special meanings
            else:
                lastChar = None
                numBuilder = ''
                for char in args[i][1:]:
                    if char.isnumeric() or char == '.':
                        numBuilder += char
                    else:
                        if numBuilder:
                            if '.' in numBuilder:
                                options[lastChar] = float(numBuilder)
                            else:
                                options[lastChar] = int(numBuilder)
                            numBuilder = ''
                        lastChar = char
                        options[char] = True
                if lastChar and numBuilder:
                    options[lastChar] = float(numBuilder)
                    numBuilder = ''
            del args[i]
        elif args[i] == "playlist" or args[i] == "m3u":
            options["playlist"] = True
            del args[i]
        elif args[i].endswith(".m3u"):
            options["playlist"] = True
            args[i] = args[i][0:args[i].rindex(".m3u")]
            i += 1
        # Remove problematic words
        elif len(args[i]) < 3 or args[i].lower() in commonWords:
            del args[i]
        else:
            i += 1
    return options

def searchFiles(terms, options):
    musicHome = Path(Path.home(), "Music")
    if not musicHome.is_dir():
        # We are probably on Windows
        musicHome = Path(Path.home(), "My Music")
    
    findArgs = ["find", str(musicHome)]
    terms = appendSearchArguments(findArgs, sys.argv[1:], options)
    print(" ".join(findArgs))
    print()
    process = subprocess.Popen(findArgs, stdout=subprocess.PIPE)
    output, error = process.communicate()
    files = output.decode().split("\n")
    # Remove any empty things
    while "" in files:
        files.remove("")
    return files

'''
Compiles all arguments in the argument list into a SQLite search term

@return A set of search terms provided, stripped of all extra arguments.
'''
def appendDatabaseArguments(argList, args, options, playlist):
    notArgs = []
    terms = set()
    if len(args) > 0:
        for arg in args:
            if arg.startswith("!"):
                notArgs.append(arg[1:])
            elif arg == "AND" or arg == "and" or arg == "-and" or arg =="--and" or arg == "&&":
                # Override whatever AND/OR we had before
                argList.pop()
                argList.append("AND")
            elif arg == "OR" or arg == "or" or arg == "-or" or arg =="--or" or arg == "||":
                # Override whatever AND/OR we had before
                argList.pop()
                argList.append("OR")
            else:
                argList.append("(")
                if playlist:
                    argList.append("playlists.name")
                    argList.append("LIKE")
                    argList.append("'%" + arg + "%'")
                else:
                    argList.append("tracks.name")
                    argList.append("LIKE")
                    argList.append("'%" + arg + "%'")
                    argList.append("OR")
                    argList.append("albums.name")
                    argList.append("LIKE")
                    argList.append("'%" + arg + "%'")
                    argList.append("OR")
                    argList.append("artists.name")
                    argList.append("LIKE")
                    argList.append("'%" + arg + "%'")
                argList.append(")")
                terms.add(arg)
                if "a" in options:
                    argList.append("AND")
                else:
                    argList.append("OR")
        if argList[len(argList) - 1] == "AND" or argList[len(argList) - 1] == "OR":
            argList.pop()
        if len(notArgs) > 0:
            argList.append("AND")
            for arg in notArgs:
                argList.append("tracks.name")
                argList.append("NOT")
                argList.append("LIKE")
                argList.append("'%" + arg + "%'")
                argList.append("AND")
            argList.pop()
    return terms

def searchDatabase(options):
    files = []
    addedUris = []
    vals = []
    minAcceptance = MATCH_RATE
    if "match" in options:
        minAcceptance = options["match"]
    elif "m" in options:
        minAcceptance = options["m"]
    if minAcceptance > 10:
        minAcceptance /= 100
    elif minAcceptance > 1:
        minAcceptance /= 10
    
    if "directory" in options:
        dbDir = Path(options["directory"])
    else:
        dbDir = Path(Path.home(), ".local/share/lollypop")
    
    if "playlist" in options:
        db = sqlite3.connect(str(Path(dbDir, "playlists.db")))
        findArgs = "SELECT tracks.uri, playlists.name FROM tracks INNER JOIN playlists ON tracks.playlist_id = playlists.id WHERE".split(" ")
        terms = appendDatabaseArguments(findArgs, sys.argv[1:], options, True)
        findArgs = " ".join(findArgs) + ";"
        results = db.execute(findArgs);
        
        for row in results:
            matches = 0
            for term in terms:
                if term in re.sub('[^A-Za-z ]', '', row[1]).lower():
                    matches += 1
            if matches >= minAcceptance * len(terms):
                # Make playlist matches more important
                matches *= 10
                songVals[row[0]] = (matches, matches)
                point = random.randint(bisect_left(vals, matches), bisect_right(vals, matches))
                vals.insert(point, matches)
                files.insert(point, (True,) + row)
                addedUris.append(row[0])
        db.close()
    db = sqlite3.connect(str(Path(dbDir, "lollypop.db")))
    findArgs = "SELECT tracks.uri, tracks.name, albums.name, artists.name FROM tracks INNER JOIN albums ON tracks.album_id = albums.id INNER JOIN track_artists ON tracks.id = track_artists.track_id INNER JOIN artists ON track_artists.artist_id = artists.id WHERE".split(" ")
    terms = appendDatabaseArguments(findArgs, sys.argv[1:], options, False)
    findArgs.append("GROUP")
    findArgs.append("BY")
    findArgs.append("tracks.uri")
    findArgs = " ".join(findArgs) + ";"
    #print(findArgs)
    results = db.execute(findArgs);
    
    evaluate = getValueFunction(terms)
    
    for row in results:
        # Skip any songs added as part of a playlist
        if row[0] in addedUris:
            continue
        #print(row[1])
        row = (False,) + row
        val = evaluate(row)
        if val[0] >= minAcceptance * len(terms):
            # Inserts it in order, but random to matches of an equal stature
            point = random.randint(bisect_left(vals, val[1]), bisect_right(vals, val[1]))
            vals.insert(point, val[1])
            files.insert(point, row)
    db.close()
    
    return (files, terms)

if __name__ == "__main__":
    # Checks if VLC is installed
    missing = subprocess.call("command -v vlc >> /dev/null", shell=True)
    if missing:
        print("VLC Media Player is not installed", file=sys.stderr)
        exit(1)
    
    mergeWords(sys.argv)
    options = extractOptions(sys.argv)
    if "h" in options or "help" in options:
        print("""Usage: %s [OPTIONS]... [TERM]...
Searches for a song within the music collection

OPTIONS:
--directory=[dir]       Define the database directory.
-a                      The search must match every single term
-v                      Displays the media player
-l                      Loops through the found files until a new command is
                        entered.
-s                      Only the first option will play.
-h, --help              Shows this message
-m[rate], --match=<rate>All matches must have this percentage of matching
                        keywords in order to be included. Defaults to 0.65.

TERMS:
For the most part, you can just enter a single-word term, and it will be added
to the possible substrings included in the file name search. However, there are
a few additional ways to refine your search.

-and, --and, AND, and   Any of these keywords will cause the previous and next
                        terms to be linked together, so that they MUST appear
                        for the file to qualify.
-or, --or, OR, or       Any of these keywords will cause the previous and next
                        terms to be linked together, so that only one or the
                        other needs to appear for the file to qualify.
                        Overrides the -a option for those two terms.
!                       Add this to the beginning of a term to ensure that the
                        keyword does NOT appear in any matching file paths.
m3u, playlist           Normally, playlists are ignored during the file search.
                        However, this term will not only allow for their
                        search, but will also give them higher precedence.""" % (sys.argv[0]))
        exit(0)
        
    files, terms = searchDatabase(options)
    
    if len(files) == 0:
        print("No files found", file=sys.stderr)
        exit(1)
    
    files.reverse()
    
    for i in range(len(files)):
        files[i] = files[i][1]
    if "s" in options:
        files = [files[0]]
    for song in files:
        if song in songVals:
            print("%02d - %s" % (songVals[song][0], song))
        else:
            print("Pl - %s" % (song))

    if "v" in options:
        vlcArgs = ["vlc"]
    else:
        vlcArgs = ["cvlc"]
    if "l" in options:
        vlcArgs.append("--loop")
    #else:
        #files.append("vlc://quit")
    vlcArgs += files
    #print(" ".join(vlcArgs))
    print("Launching VLC: %s" % (vlcArgs))
    if not "l" in options:
        os.execvp(vlcArgs[0], vlcArgs)
    else:
        subprocess.Popen(vlcArgs, stdout=sys.stdout, stderr=sys.stderr)
        exit(0)
