#!/usr/bin/env python3
# ********************************************************************
# Copyright 2010-2020 Robert A. Beezer
#
# This file is part of PreTeXt.
#
# PreTeXt 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 or version 3 of the
# License (at your option).
#
# PreTeXt 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 PreTeXt.  If not, see <http://www.gnu.org/licenses/>.
# *********************************************************************

# 2021-05-21: this script expects Python 3.6 or newer
#     argparse is backported to 3.1 (?)
# 2020-05-20: this script expects Python 3.4 or newer

import pretext as ptx

###################
# Utility Functions
###################

# Check and expand directories, files provided
# by author/publisher on the command line
# Use pretext module console messaging routines

def verify_input_file(f, file_type):
    """Verify file exists, or raise error.  Return absolute path"""
    # file_type is 'source' or 'publisher'
    #    the source gets fed into the lxml routines and so we
    #    let Python deal with separators, etc.  However the
    #    publisher file is passed around until an XSL stylesheet
    #    actually opens it for reading, so we need to make
    #    separators all of a style the XSL command expects
    #    Bonus, we can customize logging messages
    import os # .sep
    import os.path # isfile(), abspath()

    # print representation AND error-check
    if file_type == 'source':
        file_descriptor = 'XML source'
    elif file_type == 'publisher':
        file_descriptor = 'publisher'
    else:
        msg = "input file verification should receive 'source' or 'publisher' parameter, not '{}'"
        raise ValueError(msg.format(file_type))

    ptx._verbose('verifying and expanding {} file: {}'.format(file_descriptor, f))
    if not(os.path.isfile(f)):
        raise ValueError('file {} does not exist'.format(f))
    absf = os.path.abspath(f)
    if file_type == 'publisher':
        absf = absf.replace(os.sep, '/')
    ptx._verbose('input {} file expanded to absolute path: {}'.format(file_descriptor, absf))
    return absf

def get_output_file(file_path):
    """Expand provided filename, or return None"""
    import os # getcwd

    # if specified, return absolute path
    # does not need to exist
    if file_path:
        return os.path.abspath(file_path)
    # else leave unspecified
    return None

def get_destination_directory(directory):
    """Check and expand provided directory, or return current working directory"""
    import os # getcwd

    # if specified, check, sanitize, return absolute path
    if directory:
        return ptx.verify_input_directory(directory)
    # else default to current directory
    return os.getcwd()

def string_parameters_as_dict(pairs):
    """Convert flat list of consecutive keys and values into a dictionary"""
    # Expects a list, and argparse below defaults to that
    # But still includes reasonable behavior for input of None
    if not(pairs):
        return {}
    if (len(pairs) % 2) == 1:
        msg = 'There are an odd number of items in option/value list ({})'.format(pairs)
        raise ValueError(msg)
    # safe to assume even length, so floor division
    params = {}
    for i in range(len(pairs)//2):
        params[pairs[2*i]] = pairs[2*i+1]
    return params

####################
# Configuration File
####################

def get_executables():
    """Query the configuration file, return dictionary of executables/commands"""

    import configparser # ConfigParser()
    import os.path # join()

    # parse user configuration(s), contains locations of executables
    # in the "executables" section of the INI-style file

    ptx_dir = ptx.get_ptx_path()
    config_filename = 'pretext.cfg'
    default_config_file = os.path.join(ptx_dir, 'pretext', config_filename)
    user_config_file = os.path.join(ptx_dir, 'user', config_filename)
    # 2020-05-21: obsolete'd mbx script and associated config filenames
    # Try to read old version, but prefer new version
    stale_user_config_file = os.path.join(ptx_dir, 'user', 'mbx.cfg')
    config_file_list = [default_config_file, stale_user_config_file, user_config_file]

    # report on which configuration files are being examined
    ptx._verbose("parsing possible configuration files: {}".format(config_file_list))

    config = configparser.ConfigParser()
    files_read = config.read(config_file_list)
    # report on which configuration files were used
    ptx._debug("configuration files actually used/read: {}".format(files_read))
    if not(user_config_file in files_read):
        msg = "using default configuration only, custom configuration file not used at {}"
        ptx._verbose(msg.format(user_config_file))
    executable_dict = dict(config['executables'])
    # report the dictionary of keys
    ptx._debug("dictionary of executables/commands: {}".format(executable_dict))

    return executable_dict

#####################
# Command-Line Parser
#####################

def get_cli_arguments():
    """Return the CLI arguments in parser object"""
    import argparse
    parser = argparse.ArgumentParser(description='PreTeXt utility script (requires Python 3.6)',
                                     formatter_class=argparse.RawTextHelpFormatter)

    verbose_help = '\n'.join(["verbosity of information on progress of the program",
                              "  -v  is actions being performed",
                              "  -vv is some additional raw debugging information"])
    parser.add_argument('-v', '--verbose', help=verbose_help, action="count")

    component_info = [
        ('asy', 'Asymptote diagrams'),
        ('sageplot', 'Sage graphics'),
        ('latex-image', 'LaTeX pictures'),
        ('webwork', 'WeBWorK problems in authored, PG, url, and static representations'),
        ('pg-macros', 'Server-side macros for WeBWorK problem generation'),
        ('youtube', 'Thumbnails for YouTube videos (JPEG only)'),
        ('preview', 'Static preview images for interactives'),
        ('mom', 'MyOpenMath problem files, static versions'),
        ('math', 'Math elements for MathJax conversion (only)'),
        ('all', 'Complete document (in various formats)'),
        ('tikz', 'tikz pictures (removed, use latex-image)'),
    ]
    component_help = 'Possible components are:\n' + '\n'.join(['  {} - {}'.format(info[0], info[1]) for info in component_info])
    parser.add_argument('-c', '--component', help=component_help, action="store", dest="component")

    format_info = [
        ('svg', 'Scalable Vector Graphics file(s)'),
        ('pdf', 'Portable Document Format file(s)'),
        ('png', 'Portable Network Graphics file(s)'),
        ('eps', 'Encapsulated Post Script file(s)'),
        ('mml', 'MathML (math elements)'),
        ('braille', 'Nemeth Braille (math elements only)'),
        ('braille-emboss', 'UEB + Nemeth braille, for print'),
        ('braille-electronic', 'UEB + Nemeth Braille, for one-line display'),
        ('tactile', 'Images with braille labels, etc. (latex-image graphics only)'),
        ('speech', 'Speech (math elements)'),
        ('source', 'Standalone source files'),
        ('latex', 'LaTeX source file'),
        ('html', 'HyperText Markup Language (online/web pages)'),
        ('epub-svg', 'EPUB container, math as SVG'),
        ('epub-mml', 'EPUB container, math as MathML'),
        ('epub-speech', 'EPUB container, math as speech'),
        ('epub-kindle', 'EPUB container, math as MathML, PNG images'),
        ('sagenb', 'Sage worksheet conversion (removed)'),
        ('all', 'All available output formats'),
    ]
    format_help = 'Output formats are:\n' + '\n'.join(['  {} - {}'.format(info[0], info[1]) for info in format_info])
    parser.add_argument('-f', '--format', help=format_help, action="store", dest='format')

    parser.add_argument('-p', '--publisher', help='filename for publisher file', action="store", dest='publisher_file')
    # "nargs" allows multiple options following the flag
    # separate by spaces, can't use "-stringparam"
    # stringparams is a list of strings on return, defaults to empty list
    parser.add_argument('-x', '--parameters', nargs='+', help='extra stringparam options to pass to XSLT extraction stylesheet (multiple option/value pairs, not as last argument)',
                         action="store", dest='stringparams', default=[])
    # default to an empty string, which signals root to XSL stylesheet
    parser.add_argument('-r', '--restrict', help='restrict to subtree rooted at element with specified xml:id',
                         action="store", dest='xmlid', default='')

    parser.add_argument('-s', '--server', help='base URL for server (webwork only)', action="store", dest='server')

    parser.add_argument('-i', '--include', help='external data directory, relative to source, latex-image only', action="store", dest='data_dir')
    parser.add_argument('-o', '--output', help='file for output (supersedes -d)', action="store", dest='out')
    parser.add_argument('-d', '--directory', help='directory for output, defaults to current directory', action="store", dest='dir')
    parser.add_argument('-a', '--abort', help='abort script upon recoverable errors', action="store_true", dest='abort')

    parser.add_argument('xml_file', help='PreTeXt source file with content', action="store")

    return parser.parse_args()

############################################
# Interface Command-Line to Module Functions
############################################

def main():
    """React to command-line switches in order to perform basic tasks"""

    # always check version, raises fatal error for Python 2 or less
    ptx.check_python_version()

    # grab command line arguments
    args = get_cli_arguments()

    # set verbosity of supplied console messages
    # based on CLI argument, if supplied (0,1,2)
    # Parser can return None, this is level 0
    if not(args.verbose):
        ptx.set_verbosity(0)
    else:
        ptx.set_verbosity(args.verbose)

    # Now, and only now, we can report some setup,
    # since we had to wait for the vebosity to be reported and set

    # Report the command-line arguments just obtained
    ptx._debug("Parsed CLI args {}".format(vars(args)))

    # Report Python version in debugging output
    ptx._debug("Python version: {} (expecting 3.6 or newer)".format(ptx.python_version()))

    # Check discovering directory locations as
    # realized by PreTeXt installation
    # Necessary for locating configuration files (next)
    ptx._debug("discovered distribution and xsl directories: {}, {}".format(ptx.get_ptx_path(), ptx.get_ptx_xsl_path()))

    # The "get" will report 'executables' in configuration file
    ptx.set_executables(get_executables())

    # directory/file locations provided on command-line by user
    # Expanded here to be complete paths, or set to defaults
    # output file expanded; if not specified, then no
    # natural default, so could be left undefined (None)
    # We don't assume it exists, it is being created
    out_file = get_output_file(args.out)
    # output directory: CLI switch or default to cwd
    # must exist first if given, return value always exists
    dest_dir = get_destination_directory(args.dir)
    # check and sanitize XML source, presumed to exiast
    xml_source = verify_input_file(args.xml_file, 'source')
    # data directory is optional, so allow for None here
    if args.data_dir:
        data_dir = ptx.verify_input_directory(args.data_dir)
    else:
        data_dir = None
    # publisher file is optional, so allow for None here
    if args.publisher_file:
        publisher_file = verify_input_file(args.publisher_file, 'publisher')
    else:
        publisher_file = None

    # convert list of string parameters into a dictionary
    # routines in module expect a dictionary, so convert here and now
    stringparams = string_parameters_as_dict(args.stringparams)
    # if 'publisher' is passed as stringparam, pull it out
    # as 'publisher_file' and ensure correct slashes
    # but do not stomp on a file passed with dedicated switch
    if ('publisher' in stringparams) and not(publisher_file):
        publisher_file = verify_input_file(stringparams['publisher'], 'publisher')
        del stringparams['publisher']

    ptx._verbose('Done examining environment and initializing setup info')

    # The big switch
    if args.component == 'asy':
        if args.format in ['html', 'pdf', 'svg', 'png', 'eps', 'source']:
            ptx.asymptote_conversion(xml_source, publisher_file, stringparams, args.xmlid, dest_dir, args.format)
        else:
            raise NotImplementedError('cannot make Asymptote diagrams in "{}" format'.format(args.format))
    elif  args.component == 'sageplot':
        if args.format in ['pdf', 'svg', 'source']:
            ptx.sage_conversion(xml_source, publisher_file, stringparams, args.xmlid, dest_dir, args.format)
        else:
            raise NotImplementedError('cannot make Sage graphics in "{}" format'.format(args.format))
    elif args.component == 'latex-image':
        if args.format in ['pdf', 'svg', 'png', 'eps', 'source', 'all']:
            ptx.latex_image_conversion(xml_source, publisher_file, stringparams, args.xmlid, data_dir, dest_dir, args.format)
        elif args.format in ['tactile']:
            ptx.latex_tactile_image_conversion(xml_source, publisher_file, stringparams, data_dir, dest_dir, args.format)
        else:
            raise NotImplementedError('cannot make LaTeX pictures in "{}" format'.format(args.format))
    elif args.component == 'webwork-tex':
        wtdep = ('the "webwork-tex" component has been replaced by the "webwork" component, '
                 'and behaves very differently')
        raise NotImplementedError(wtdep)
    elif args.component == 'webwork':
        ptx.webwork_to_xml(xml_source, publisher_file, stringparams, args.abort, args.server, dest_dir)
    elif args.component == 'pg-macros':
        ptx.pg_macros(xml_source, dest_dir)
    elif args.component == 'youtube':
        ptx.youtube_thumbnail(xml_source, publisher_file, stringparams, args.xmlid, dest_dir)
    elif args.component == 'preview':
        ptx.preview_images(xml_source, publisher_file, stringparams, args.xmlid, dest_dir)
    elif args.component == 'mom':
        ptx.mom_static_problems(xml_source, publisher_file, stringparams, args.xmlid, dest_dir)
    elif args.component == 'math':
        if args.format in ['svg', 'mml', 'nemeth', 'speech']:
            ptx.mathjax_latex(xml_source, publisher_file, out_file, dest_dir, args.format)
        else:
            raise NotImplementedError('cannot convert math elements to "{}" format'.format(args.format))
    elif args.component == 'all':
        if args.format == 'html':
            ptx.html(xml_source, publisher_file, stringparams, dest_dir)
        elif args.format == 'pdf':
            ptx.pdf(xml_source, publisher_file, stringparams, out_file, dest_dir)
        elif args.format == 'latex':
            ptx.latex(xml_source, publisher_file, stringparams, out_file, dest_dir)
        elif args.format == 'braille-emboss':
            ptx.braille(xml_source, publisher_file, stringparams, out_file, dest_dir, 'emboss')
        elif args.format == 'braille-electronic':
            ptx.braille(xml_source, publisher_file, stringparams, out_file, dest_dir, 'electronic')
        elif args.format == 'epub-svg':
            ptx.epub(xml_source, publisher_file, out_file, dest_dir, 'svg')
        elif args.format == 'epub-mml':
            ptx.epub(xml_source, publisher_file, out_file, dest_dir, 'mml')
        elif args.format == 'epub-speech':
            ptx.epub(xml_source, publisher_file, out_file, dest_dir, 'speech')
        elif args.format == 'epub-kindle':
            ptx.epub(xml_source, publisher_file, out_file, dest_dir, 'kindle')
        # 2020-05-19 MathBookXMLtoSWS class removed
        elif args.format == 'sagenb':
            raise NotImplementedError("conversion to Sage notebook format removed (2020-05-18)")
        #2020-08-19 Deprecated in favor of -emboss, -electronic
        elif args.format == 'braille':
            raise NotImplementedError("'braille' format deprecated (2020-08-19), 'braille-emboss' is replacement")
        else:
            raise NotImplementedError('cannot make entire document in "{}" format'.format(args.format))
    elif args.component == 'images':
        ptx.all_images(xml_source, publisher_file, stringparams, args.xmlid)
    # 2020-05-19 tikz_conversion() function removed
    elif args.component == 'tikz':
        raise NotImplementedError('conversion of TikZ pictures has been subsumed into the "latex-image" component (2020-05-19)')
    else:
        raise ValueError('the "{}" component is not a conversion option'.format(args.component))

    # Cleanup, if execution does not raise errors
    ptx.release_temporary_directories()


# Do it - lone top-level command
main()
