#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2014  Alex Merry <alex.merry@kdemail.net>
# Copyright 2014  Aurélien Gâteau <agateau@kde.org>
# Copyright 2014  Alex Turbov <i.zaufi@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Python 2/3 compatibility (NB: we require at least 2.7)
from __future__ import division, absolute_import, print_function, unicode_literals

import argparse
import logging
import codecs
import os
import shutil
import subprocess
import sys
import tempfile

import jinja2
import yaml

from kapidox import argparserutils
from kapidox import utils
from kapidox.generator import *
try:
    from kapidox import depdiagram
    DEPDIAGRAM_AVAILABLE = True
except ImportError:
    DEPDIAGRAM_AVAILABLE = False

PLATFORM_ALL = "All"
PLATFORM_UNKNOWN = "UNKNOWN"


def generate_group_menu(tiers):
    """Generate a menu for the frameworks"""
    sections = []
    for t in range(1,5):
        frameworks = []
        for fw in tiers[t]:
            rellink = '../../' + fw['outputdir'] + '/html/index.html'
            frameworks.append({
                'href': rellink,
                'name': fw['fancyname']
                })
        sections.append({
            'title': 'Tier ' + str(t),
            'members': frameworks
            })
    return {'group_title': 'Frameworks', 'sections': sections}


def expand_platform_all(dct, available_platforms):
    """If one of the keys of dct is PLATFORM_ALL (or PLATFORM_UNKNOWN), remove it and add entries for all available platforms to dct"""
    add_all_platforms = False
    if PLATFORM_ALL in dct:
        add_all_platforms = True
        del dct[PLATFORM_ALL]
    if PLATFORM_UNKNOWN in dct:
        add_all_platforms = True
        del dct[PLATFORM_UNKNOWN]
    if add_all_platforms:
        for platform in available_platforms:
            if not platform in dct:
                dct[platform] = ''


def process_toplevel_html_file(outputfile, doxdatadir, tiers, title,
        api_searchbox=False):

    with open(os.path.join(doxdatadir, 'frameworks.yaml')) as f:
        tierinfo = yaml.load(f)

    # Gather a list of all frameworks and available platforms
    lst = []
    available_platforms = set()

    for t in range(1,5):
        for fw in tiers[t]:
            # Extend framework info
            fw['href'] = fw['outputdir'] + '/html/index.html'

            try:
                platform_lst = [x['name'] for x in fw['platforms'] if x['name'] not in (PLATFORM_ALL, PLATFORM_UNKNOWN)]
                available_platforms.update(set(platform_lst))
            except (KeyError, TypeError):
                logging.warning('{} framework lacks valid platform definitions'.format(fw['fancyname']))
                fw['platforms'] = [dict(name=PLATFORM_UNKNOWN)]

            lst.append(fw)

    lst.sort(key=lambda x: x['fancyname'].lower())

    # Create platform_dict, a dictionary where keys are platform name and values are platform notes
    for fw in lst:
        dct = dict((x['name'], x.get('note', '')) for x in fw['platforms'])
        expand_platform_all(dct, available_platforms)
        fw['platform_dict'] = dct

    # Separate "real" frameworks and porting aids
    framework_lst = []
    porting_aid_lst = []
    for fw in lst:
        if fw.get('portingAid'):
            porting_aid_lst.append(fw)
        else:
            framework_lst.append(fw)

    mapping = {
            'resources': '.',
            'api_searchbox': api_searchbox,
            # steal the doxygen css from one of the frameworks
            # this means that all the doxygen-provided images etc. will be found
            'doxygencss': tiers[1][0]['outputdir'] + '/html/doxygen.css',
            'title': title,
            'breadcrumbs': {
                'entries': [
                    {
                        'href': 'http://api.kde.org/',
                        'text': 'KDE API Reference'
                    }
                    ]
                },
            'framework_lst': framework_lst,
            'porting_aid_lst': porting_aid_lst,
            'tierinfo': tierinfo,
            'available_platforms': sorted(available_platforms),
        }
    tmpl = create_jinja_environment(doxdatadir).get_template('frameworks.html')
    with codecs.open(outputfile, 'w', 'utf-8') as outf:
        outf.write(tmpl.render(mapping))


def find_dot_files(dot_dir):
    """Returns a list of path to files ending with .dot in subdirs of `dot_dir`."""
    lst = []
    for (root, dirs, files) in os.walk(dot_dir):
        lst.extend([os.path.join(root, x) for x in files if x.endswith('.dot')])
    return lst


def generate_diagram(png_path, fancyname, tier, dot_files, tmp_dir):
    """Generate a dependency diagram for a framework.
    """
    def run_cmd(cmd, **kwargs):
        try:
            subprocess.check_call(cmd, **kwargs)
        except subprocess.CalledProcessError as exc:
            logging.error(
                    'Command {exc.cmd} failed with error code {exc.returncode}.'.format(exc=exc))
            return False
        return True

    logging.info('Generating dependency diagram')
    dot_path = os.path.join(tmp_dir, fancyname + '.dot')

    with open(dot_path, 'w') as f:
        with_qt = tier <= 2
        ok = depdiagram.generate(f, dot_files, framework=fancyname, with_qt=with_qt)
        if not ok:
            logging.error('Generating diagram failed')
            return False

    logging.info('- Simplifying diagram')
    simplified_dot_path = os.path.join(tmp_dir, fancyname + '-simplified.dot')
    with open(simplified_dot_path, 'w') as f:
        if not run_cmd(['tred', dot_path], stdout=f):
            return False

    logging.info('- Generating diagram png')
    if not run_cmd(['dot', '-Tpng', '-o' + png_path, simplified_dot_path]):
        return False

    # These os.unlink() calls are not in a 'finally' block on purpose.
    # Keeping the dot files around makes it possible to inspect their content
    # when running with the --keep-temp-dirs option. If generation fails and
    # --keep-temp-dirs is not set, the files will be removed when the program
    # ends because they were created in `tmp_dir`.
    os.unlink(dot_path)
    os.unlink(simplified_dot_path)
    return True


def create_fw_info(frameworksdir, modulename, maintainers):
    fwdir = os.path.join(frameworksdir, modulename)
    if not os.path.isdir(fwdir):
        return None

    yaml_file = os.path.join(fwdir, 'metainfo.yaml')
    if not os.path.isfile(yaml_file):
        logging.warning('{} does not contain a framework (metainfo.yaml missing)'.format(fwdir))
        return None

    fancyname = utils.parse_fancyname(fwdir)
    if not fancyname:
        logging.warning('Could not find fancy name for {}, skipping it'.format(fwdir))
        return None

    outputdir = modulename

    # FIXME: option in yaml file to disable docs
    try:
        metainfo = yaml.load(open(yaml_file))
    except:
        logging.warning('Could not load metainfo.yaml for {}, skipping it'.format(modulename))
        return None

    if metainfo is None:
        logging.warning('Empty metainfo.yaml for {}, skipping it'.format(modulename))
        return None

    tier = metainfo.get("tier")
    if tier is None:
        logging.warning('Could not find tier for {}, skipping it'.format(modulename))
        return None
    elif tier < 1 or tier > 4:
        logging.warning('Invalid tier {} for {}, skipping it'.format(tier, modulename))
        return None

    if 'maintainer' not in metainfo:
        fw_maintainers = []
    elif isinstance(metainfo['maintainer'],list):
        fw_maintainers = map(lambda x: maintainers.get(x, None),
                             metainfo['maintainer'])
    else:
        fw_maintainers = [maintainers.get(metainfo['maintainer'], None)]

    metainfo.update({
        'maintainers': list(filter(lambda x: x is not None, fw_maintainers)),
        'modulename': modulename,
        'fancyname': fancyname,
        'srcdir': fwdir,
        'outputdir': outputdir,
        'dependency_diagram': None,
        })
    return metainfo


def create_fw_context(args, fwinfo, tagfiles):
    return Context(args,
            # Names
            modulename=fwinfo['modulename'],
            fancyname=fwinfo['fancyname'],
            fwinfo=fwinfo,
            # KApidox files
            resourcedir='../..',
            # Input
            srcdir=fwinfo['srcdir'],
            tagfiles=tagfiles,
            dependency_diagram=fwinfo['dependency_diagram'],
            # Output
            outputdir=fwinfo['outputdir'],
            )

def gen_fw_apidocs(ctx, tmp_base_dir):
    create_dirs(ctx)
    # tmp_dir is deleted when tmp_base_dir is
    tmp_dir = tempfile.mkdtemp(prefix=ctx.modulename + '-', dir=tmp_base_dir)
    generate_apidocs(ctx, tmp_dir,
            doxyfile_entries=dict(WARN_IF_UNDOCUMENTED=True)
            )

def finish_fw_apidocs(ctx, group_menu):
    classmap = build_classmap(ctx.tagfile)
    write_mapping_to_php(classmap, os.path.join(ctx.outputdir, 'classmap.inc'))
    postprocess(ctx, classmap,
            template_mapping={
                'breadcrumbs': {
                    'entries': [
                        {
                            'href': 'http://api.kde.org/',
                            'text': 'KDE API Reference'
                        },
                        {
                            'href': '../../index.html',
                            'text': 'Frameworks'
                        },
                        {
                            'href': 'index.html',
                            'text': ctx.fancyname
                        }
                        ]
                    },
                'group_menu': group_menu
                },
            )


def create_fw_tagfile_tuple(fwinfo):
    tagfile = os.path.abspath(
                os.path.join(
                    fwinfo['outputdir'],
                    'html',
                    fwinfo['modulename']+'.tags'))
    return (tagfile, '../../' + fwinfo['outputdir'] + '/html/')


def parse_args():
    parser = argparse.ArgumentParser(description='Generate API documentation for the KDE Frameworks')
    group = argparserutils.add_sources_group(parser)
    group.add_argument('frameworksdir',
            help='Location of the frameworks modules.')
    group.add_argument('--depdiagram-dot-dir',
            help='Generate dependency diagrams, using the .dot files from DIR.',
            metavar="DIR")
    argparserutils.add_output_group(parser)
    argparserutils.add_qt_doc_group(parser)
    argparserutils.add_paths_group(parser)
    argparserutils.add_misc_group(parser)
    args = parser.parse_args()
    argparserutils.check_common_args(args)

    if args.depdiagram_dot_dir and not DEPDIAGRAM_AVAILABLE:
        logging.error('You need to install the Graphviz Python bindings to generate dependency diagrams.\nSee <http://www.graphviz.org/Download.php>.')
        exit(1)

    if not os.path.isdir(args.frameworksdir):
        logging.error(args.frameworksdir + " is not a directory")
        exit(2)

    return args


def main():
    utils.setup_logging()
    args = parse_args()

    tagfiles = search_for_tagfiles(
            suggestion = args.qtdoc_dir,
            doclink = args.qtdoc_link,
            flattenlinks = args.qtdoc_flatten_links,
            searchpaths = ['/usr/share/doc/qt5', '/usr/share/doc/qt'])
    maintainers = download_kde_identities()

    tiers = {1:[],2:[],3:[],4:[]}
    for modulename in os.listdir(args.frameworksdir):
        fwinfo = create_fw_info(args.frameworksdir, modulename, maintainers)
        if fwinfo:
            tiers[fwinfo["tier"]].append(fwinfo)

    for t in range(1,5):
        tiers[t] = sorted(tiers[t], key=lambda f: f['fancyname'].lower())

    copy_dir_contents(os.path.join(args.doxdatadir,'htmlresource'),'.')

    group_menu = generate_group_menu(tiers)

    process_toplevel_html_file('index.html', args.doxdatadir,
            title=args.title, tiers=tiers, api_searchbox=args.api_searchbox)

    tmp_dir = tempfile.mkdtemp(prefix='kgenframeworksapidox-')
    try:
        if args.depdiagram_dot_dir:
            dot_files = find_dot_files(args.depdiagram_dot_dir)
            assert(dot_files)

        for t in range(1,5):
            for fwinfo in tiers[t]:
                logging.info('# Generating doc for {}'.format(fwinfo['fancyname']))
                if args.depdiagram_dot_dir:
                    png_path = os.path.join(tmp_dir, fwinfo['modulename']) + '.png'
                    ok = generate_diagram(png_path, fwinfo['fancyname'], t, dot_files, tmp_dir)
                    if ok:
                        fwinfo['dependency_diagram'] = png_path
                ctx = create_fw_context(args, fwinfo, tagfiles)
                gen_fw_apidocs(ctx, tmp_dir)
                tagfiles.append(create_fw_tagfile_tuple(fwinfo))
                if t < 3:
                    finish_fw_apidocs(ctx, group_menu)

            if t >= 3:
                # Rebuild for interdependencies
                # FIXME: can we be cleverer about deps?
                for fwinfo in tiers[t]:
                    logging.info('# Rebuilding {} for interdependencies'.format(fwinfo['fancyname']))
                    shutil.rmtree(fwinfo['outputdir'])
                    ctx = create_fw_context(args, fwinfo, tagfiles)
                    gen_fw_apidocs(ctx, tmp_dir)
                    finish_fw_apidocs(ctx, group_menu)
        logging.info('# Done')
    finally:
        if args.keep_temp_dirs:
            logging.info('Kept temp dir at {}'.format(tmp_dir))
        else:
            shutil.rmtree(tmp_dir)


if __name__ == "__main__":
    main()

