#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2014 Cédric Picard
#
# LICENSE
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
# END_OF_LICENSE
#
"""
Simple command line browser independant bookmark utility.

Usage: bm [options] [-r] URL TAG...
       bm [options]  -d  URL
       bm [options]  -l  [TAG]...
       bm [options]  -L  TAG...
       bm [options]  URL
       bm [options]  -i  SOURCE...
       bm [options]  -t

Arguments:
    URL     The url to bookmark
            If alone, print the tags associated with URL
            If the url corresponds to an existing file,
            the absolute path is substituted to URL
            If URL is '-', then the program looks for a list of URL
            comming from the standard input.
    TAG     The tags to use with the url.
    SOURCE  When uniting, the paths to the source files.

Options:
    -h, --help          Print this help and exit
    -r, --remove        Remove TAG from URL
    -d, --delete        Delete an url from the database
    -l, --list-every    List the urls with every of TAG
                        If no tag is given, list all urls
    -L, --list-any      List the urls with any of TAG
    -f, --file FILE     Use FILE as the database, can be an url
                        Default is ~/.bookmarks
    -t, --tags          List every tag present in the database
                        with how many times it is used.
                        Output is sorted from the least to the most used
    -i, --import        Import bookmarks from sources into the database.
    -n, --no-path-subs  Disable file path substitution
    -v, --verbose       Displays the list of tags of each url when listing
    -w, --web           Open the results in a web browser
    --version           Print current version number
"""
VERSION = "1.5.4"

import os
from docopt              import docopt
from msgpack             import dump, load
from msgpack.exceptions  import UnpackValueError, ExtraData

## Atomic database mutations

def add(database, url, tags):
    """ Add `tags' to `url' in `database' """
    if url in database:
        for tag in tags:
            if tag not in database[url]:
                database[url].append(tag)
    else:
        database[url] = list(tags)


def remove(database, url, tags):
    """ Removes `tags' from `url' in `database' """
    try:
        for tag in tags:
            database[url].remove(tag)

        if database[url] == []:
            database.pop(url)
    except ValueError:
        pass


def delete(database, url):
    """ Removes `url' from `database' """
    try:
        database.pop(url)
    except KeyError:
        pass

## Database listing and query functions

def list_any(database, tags, *, verbose=False):
    """
    Yields any url in `database' with one of `tags'
    If `verbose' is set yields tupples (url, tags)
    """
    for url in database:
        if set(tags).intersection(set(database[url]))!=set() or tags==[]:
            if not verbose:
                yield url
            else:
                yield url, database[url]


def list_every(database, tags, *, verbose=False):
    """
    Yields any url in `database' with each of `tags'
    If `verbose' is set yields tupples (url, tags)
    """
    for url in database:
        if set(tags).issubset(set(database[url])):
            if not verbose:
                yield url
            else:
                yield url, database[url]


def list_every_tags(database):
    """ Returnrs the list of every tags present in `database' """
    from collections import Counter

    counter = Counter()
    for url in database:
        for tag in database[url]:
            counter[tag] += 1
    return [(t, x) for x,t in counter.items()]


def _list(database, urls, tags, method, verbose):
    """ Internal dispatcher to listing functions """
    result = []
    for url in urls:
        result += list(method(database, tags, verbose=verbose))

    if not verbose:
        result.sort()
    else:
        result.sort(key=lambda x:x[0])

    return result

## Database file Input/Output

def import_db(database, d_paths):
    """ Merges each database in `d_paths' into `database' """
    paths = expand_paths(d_paths)

    for path in paths:
        db = open_db(path)
        for url in db:
            add(database, url, db[url])


def dump_db(database, d_file):
    """ Writes `database' into `d_file' """
    try:
        dump(database, open(d_file, 'wb'))

    except FileNotFoundError:
        print("File not found: " + d_file, file=os.sys.stderr)


def expand_paths(paths):
    """ Yields the paths in `paths' expanded to absolute paths if possible """
    for path in paths:
        if path is not None and os.path.exists(path):
            yield os.path.abspath(path)
        else:
            yield path

## Open in browser feature

# If this grows, we'll have to put it in another file
def html_generator(tags, sites):
    """ Returns the html template of a page presenting tags and sites """
    li_html  = '<li><a href="{u}">{u}</a><p>{t}</p></li>'
    template = """
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>bookmark</title>
      </head>
      <body>
        <h1>
          {tags}
        </h1>
        <ol>
          {sites}
        </ol>
      </body>
    </html>
    """
    template = template.format(
        tags  = ', '.join(tags),
        sites = ('\n'+' '*10).join(li_html.format(u = x.split()[0],
                                                  t = ','.join(x.split()[2:]))
                                  for x in sites))
    return template


def web_open(browser, html_src):
    """ Creates and opens a page with `html_src' into the `browser' """
    from subprocess import Popen
    from time import sleep

    path = "/tmp/bm-tmp.html"
    try:
        f = open(path, "w")
        f.write(html_src)
    finally:
        f.close()
    Popen([browser, "file://" + path])

## Main routines

def open_db(path, *, encoding="utf-8"):
    """ Opens a database at `path' and return its data """
    if "://" in path:
        import tempfile
        import requests
        from   requests.exceptions import ConnectionError

        try:
            request  = requests.get(path)
            database = request.content
            encoding = request.encoding or "utf-8"

            tmp_file = True
            path = tempfile.mktemp()

            open(path, "wb").write(database)

        except ConnectionError:
            print("Connection impossible: " + path, file=os.sys.stderr)
            data = []
            return data

    else:
        tmp_file = False
        encoding = "utf-8"


    try:
        database = open(path, "rb")
        data = load(database, encoding=encoding)

    except FileNotFoundError:
        print("File not found: " + path, file=os.sys.stderr)
        data = []

    except ExtraData:
        print("Page not found: " + request.url, file=os.sys.stderr)
        data = []

    except ConnectionError:
        print("Connection impossible: " + request.url, file=os.sys.stderr)
        data = []

    finally:
        if tmp_file:
            os.remove(path)

    return data


def manage_urls(urls, tags, d_file, database, args):
    """
    Manages action-oriented arguments
        urls:     user provived urls
        tags:     user provived tags
        d_file:   database file for storage
        database: reference to the current database
        args:     command-line arguments dictionnary
    """
    verbose = args["--verbose"]

    if args["--delete"]:
        for url in urls:
            delete(database, url)
        dump_db(database, d_file)

    elif args["--list-any"] or args["--list-every"]:
        if args["--list-any"]:
            sites = _list(database, urls, tags, list_any, verbose)
        else:
            sites = _list(database, urls, tags, list_every, verbose)

        if verbose:
            sites = [ x[0] + ' : ' + ' '.join(x[1]) for x in sites ]

        if args["--web"]:
            web_open(os.environ["BROWSER"], html_generator(tags, sites))
        else:
            for each in sites:
                print(each)

    elif args["--remove"]:
        for url in urls:
            remove(database, url, tags)
        dump_db(database, d_file)

    elif args["--tags"]:
        result = list_every_tags(database)
        result.sort()
        for num, tag in result:
            print("%s %s" % (tag, num))

    elif args["--import"]:
        for url in urls:
            import_db(database, tags)
        dump_db(database, d_file)

    elif args["TAG"]:
        for url in urls:
            add(database, url, tags)
        dump_db(database, d_file)

    else:
        for url in urls:
            for tag in database[url]:
                print(tag)


def main():
    """ Reception and preprocessing of command-line arguments """
    args = docopt(__doc__, version=VERSION)

    tags = args["TAG"] or args["SOURCE"]

    if args["URL"] == '-':
        urls = os.sys.stdin.read().splitlines()
    else:
        # Only one url...but we still need an iterable
        urls = [ args["URL"] ]

    d_file = args["--file"] or os.environ["HOME"] + "/.bookmarks"

    try:
        if not os.path.exists(d_file) and "://" not in d_file:
            print('The file "' + d_file + '" does not exist: creating it.',
                    file=os.sys.stderr)
            dump_db({}, d_file)

        database = open_db(d_file)

    except UnpackValueError:
        database = {}

    except PermissionError as e:
        os.sys.exit(e)


    if not args["--no-path-subs"]:
        urls = expand_paths(urls)

    manage_urls(urls, tags, d_file, database, args)


if __name__ == "__main__":
    # This is a little rude but, hey, better late than never!
    try:
        main()
    except KeyError as e:
        print("No such bookmark:", e, file=os.sys.stderr)
        os.sys.exit(1)
    except BrokenPipeError as e:
        pass
