#!/usr/bin/python
"""
resync: The ResourceSync command line client

Created by Simeon Warner on 2012-04...
"""

import logging
import logging.config
import optparse
import sys

from resync.client import Client, ClientFatalError
from resync.utils import UTCFormatter

DEFAULT_CLIENT_LOGFILE = 'resync-client.log'

def init_logging(to_file=False, logfile=None, human=True, 
                 verbose=False, eval_mode=False):
    """Initialize logging

    Use of log levels:
    DEBUG - very verbose, for evaluation of output (-e)
    INFO - verbose, only seen by users if they ask for it (-v)
    WARNING - messages output messages to console
    """
    
    fmt = '%(asctime)s | %(name)s | %(levelname)s | %(message)s'
    formatter = UTCFormatter(fmt)
    
    if human:
        # Create a special handler designed just for human readable output
        hh = logging.StreamHandler()
        hh.setLevel( logging.INFO if (verbose) else logging.WARNING )
        hh.setFormatter(logging.Formatter(fmt='%(message)s'))
    if to_file:
        if (logfile is None):
            logfile = DEFAULT_CLIENT_LOGFILE
        fh = logging.FileHandler(filename=logfile, mode='a')
        fh.setFormatter(formatter)
        fh.setLevel( logging.DEBUG if (eval_mode) else logging.INFO )

    loggers = ['client','resourcelist_builder','sitemap']
    for logger in loggers:
        log = logging.getLogger(logger)
        log.setLevel(logging.DEBUG) #control at handler instead
        if human:
            log.addHandler(hh)
        if to_file:
            log.addHandler(fh)

    log=logging.getLogger('client')
    if (to_file):
        log.info("Writing detailed log to %s" % (logfile))


def count_true_args(*args):
    """Count number of list of arguments that evaluate True"""
    count=0
    for arg in args:
        if (arg):
            count+=1
    return(count)

def parse_links(args_link):
    links={}
    if (args_link is not None):
        for link_str in args_link:
            try:
                (href, atts) = parse_link(link_str)
                links[href]=atts
            except ValueError as e:
                raise ClientFatalError("Bad --link option '%s' (%s)"%(link_str,str(e)))
    return(links)

def parse_link(link_str):
    """Parse --link option to add to capabilities

    Input string of the form: href,att1=val1,att2=val2
    """
    atts={}
    try:
        segs = link_str.split(',')
        # First segment is relation                                                             
        href = segs.pop(0)
        # Remaining segments are attributes
        for term in segs:
            (k,v)=term.split('=')
            atts[k]=v
    except ValueError as e:
        raise ClientFatalError("Bad component of --link option '%s' (%s)"%(link_str,str(e)))
    return(href,atts)

def main():

    if (sys.version_info < (2,6)):
        sys.exit("This program requires python version 2.6 or later")
    
    # Options and arguments
    p = optparse.OptionParser(description='ResourceSync sync script',
                              usage='usage: %prog [options] uri_path local_path  (-h for help)',
                              add_help_option=False)

    # Modes
    # a. modes using remote sitemap/resources
    rem = p.add_option_group('REMOTE MODES',
        'These modes use a remote source that is specified in a set of uri=path mappings '
        'and potentially also using an explicit --sitemap location. The default mode is '
        '--baseline')
    rem.add_option('--baseline', '-b', action='store_true',
                   help='baseline sync of resources from remote source (src) to local filesystem (dst)')
    rem.add_option('--inc', '-i', action='store_true',
                   help='incremental sync of resources from remote source (src) to local filesystem (dst)')
    rem.add_option('--audit', '-a', action='store_true',
                   help="audit sync state of destination wrt source")
    rem.add_option('--parse', '-p', action='store_true',
                   help="just parse the remote sitemap (from mapping or explicit --sitemap)")
    rem.add_option('--explore', action='store_true',
                   help="just explore links between sitemap and changelists starting from "
                        "mapping or explicit --sitemap)")
    # b. modes based solely on files on local disk
    loc = p.add_option_group('LOCAL MODES',
        'These modes act on files on the local disk')
    loc.add_option('--write', '-w', action='store_true',
                   help="write a sitemap based on files on disk using uri=path mappings "
                        "in reverse to calculate URIs from the local paths. To STDOUT by "
                        "default, override with --outfile")
    loc.add_option('--changelist', '-c', action='store_true',
                   help="write a changelist sitemap by comparison of a reference sitemap "
                        "(specify file with --reference) and either files on disk (using "
                        "the map provided) or a second sitemap (specify file with "
                        "--newreference). Otherwise follows --write options. Also accepts "
                        "the --empty option (with no mapping) to write and empy changelist.")

    # Specification of map between remote URI and local file paths, and remote sitemap
    nam = p.add_option_group('FILE/URI NAMING OPTIONS')
    nam.add_option('--outfile', type=str, action='store',
                   help='write sitemap to specified file rather than STDOUT')
    nam.add_option('--sitemap', type=str, action='store',
                   help='explicitly set sitemap name, overriding default sitemap.xml '
                        'appended to first source URI specified in the mappings')
    nam.add_option('--reference', type=str, action='store',
                   help='reference sitemap name for --changelist calculation')
    nam.add_option('--newreference', type=str, action='store',
                   help='updated reference sitemap name for --changelist calculation')
    nam.add_option('--dump', metavar="DUMPFILE", type=str, action='store',
                   help='write dump to specified file for --write or --changelist')
    nam.add_option('--changelist-uri', type=str, action='store',
                   help='explicitly set the changelist URI that will be use in --inc mode, '
                        'overrides process of getting this from the sitemap')

    # Options that apply to multiple modes
    opt = p.add_option_group('OPTIONS')
    opt.add_option('--checksum', action='store_true',
                   help="use checksum (md5) in addition to last modification time and size")
    opt.add_option('--delete', action='store_true',
                   help="allow files on destination to be deleted")
    opt.add_option('--exclude', type=str, action='append',
                   help="exclude resources with URI or filename matching pattern "
                        "(repeat option for multiple excludes)")
    opt.add_option('--empty', action='store_true',
                   help="combine with --changelist to write and empty changelist, perhaps with links")
    opt.add_option('--link', type=str, action='append',
                   help="add discovery links to the output sitemap, "
                        "format: href,att1=val1,att2=val2 "
                        "(repeat option for multiple links)")
    opt.add_option('--prev', type=str, action='store',
                   help="add prev discovery link to this URI to the output sitemap")
    opt.add_option('--next', type=str, action='store',
                   help="add next discovery link to this URI to the output sitemap")
    opt.add_option('--current', type=str, action='store',
                   help="add current discovery link to this URI to the output sitemap")
    opt.add_option('--multifile', '-m', action='store_true',
                   help="disable reading and output of sitemapindex for multifile sitemap")
    opt.add_option('--noauth', action='store_true',
                   help="disable checking of URL paths to ensure that the sitemaps refer "
                        "only to resources on the same server/sub-path etc. Use with care.")
    opt.add_option('--warc', action='store_true',
                   help="write dumps in WARC format (instead of ZIP+Sitemap default)")
    opt.add_option('--dryrun', '-n', action='store_true',
                   help="don't update local resources, say what would be done")
    opt.add_option('--ignore-failures', action='store_true',
                   help="continue past download failures")
    # These likely only useful for experimentation
    opt.add_option('--max-sitemap-entries', type=int, action='store',
                   help="override default size limits")
    # Want these to show at the end
    opt.add_option('--verbose', '-v', action='store_true',
                   help="verbose")
    opt.add_option('--logger', '-l', action='store_true',
                   help="create detailed log of client actions (will write "
                        "to %s unless specified with --logfile" %
                        DEFAULT_CLIENT_LOGFILE)
    opt.add_option('--logfile', type='str', action='store',
                   help="create detailed log of client actions")
    opt.add_option('--eval', '-e', action='store_true',
                   help="output evaluation of source/client synchronization performance... "
                        "be warned, this is very verbose")
    opt.add_option('--help', '-h', action='help',
                   help="this help")

    (args, map) = p.parse_args()

    # Implement exclusive arguments and default --baseline (support for exclusive
    # groups in argparse is incomplete is python2.6)
    if (not args.baseline and not args.inc and not args.audit and 
        not args.parse and not args.explore and not args.write and 
        not args.changelist):
        args.baseline=True
    elif (count_true_args(args.baseline,args.inc,args.audit,args.parse,
                          args.explore,args.write,args.changelist)>1):
        p.error("Only one of --baseline, --inc, --audit, --parse, --explore, --write, --changelist modes allowed")

    # Configure logging module and create logger instance
    init_logging( to_file=args.logger, logfile=args.logfile, 
                  verbose=args.verbose, eval_mode=args.eval )

    c = Client( checksum=args.checksum,
                verbose=args.verbose,
                dryrun=args.dryrun )

    try:
        if (map):
            # Mappings apply to (almost) everything
            c.set_mappings(map)
        if (args.sitemap):
            c.sitemap_name=args.sitemap
        if (args.warc):
            c.dump_format='warc'
        if (args.exclude):
            c.exclude_patterns=args.exclude
        if (args.multifile):
            c.allow_multifile=not args.multifile
        if (args.noauth):
            c.noauth=args.noauth
        if (args.max_sitemap_entries):
            c.max_sitemap_entries=args.max_sitemap_entries
        if (args.ignore_failures):
            c.ignore_failures=args.ignore_failures

        # Links apply to anything that writes sitemaps
        links = parse_links(args.link)
        if (args.prev):
            links[args.prev] = { 'rel': 'prev http://www.openarchives.org/rs/changelist' }
        if (args.next):
            links[args.next] = { 'rel': 'next http://www.openarchives.org/rs/changelist' }
        if (args.current):
            links[args.current] = { 'rel': 'current http://www.openarchives.org/rs/changelist' }

        # Finally, do something...
        if (args.baseline or args.audit):
            c.baseline_or_audit(allow_deletion=args.delete,
                            audit_only=args.audit)
        elif (args.inc):
            c.incremental(allow_deletion=args.delete,
                          changelist_uri=args.changelist_uri)
        elif (args.parse):
            c.parse_sitemap()
        elif (args.explore):
            c.explore_links()
        elif (args.write):
            c.write_sitemap(outfile=args.outfile,
                            capabilities=links,
                            dump=args.dump)
        elif (args.changelist):
            if (not args.reference and not args.empty):
                p.error("Must supply --reference sitemap for --changelist, or --empty")
            c.changelist_sitemap(ref_sitemap=args.reference,
                            newref_sitemap=( args.newreference if (args.newreference) else None ),
                            empty=args.empty,
                            outfile=args.outfile,
                            capabilities=links,
                            dump=args.dump)
        else:
            p.error("Unknown mode requested")
    # Any problem we expect will come as a ClientFatalError, anything else 
    # is... an exception ;-)
    except ClientFatalError as e:
        sys.stderr.write("\nFatalError: " + str(e) + "\n")

if __name__ == '__main__':
    main()
