#! /usr/bin/env 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 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'

            # Fix broken frameworks lacking platform definitions
            if fw['platforms'] is None:
                logging.warning('{} framework lacks platform definitions'.format(fw['fancyname']))
                fw['platforms'] = [dict(name=PLATFORM_UNKNOWN)]

            platform_lst = [x['name'] for x in fw['platforms'] if x['name'] not in (PLATFORM_ALL, PLATFORM_UNKNOWN)]
            available_platforms.update(set(platform_lst))

            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 open(outputfile, 'w') 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):
    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
    metainfo = yaml.load(open(yaml_file))
    tier = metainfo["tier"]
    if tier is None:
        logging.warning('Could not find tier for {}'.format(framework))
        return None
    elif tier < 1 or tier > 4:
        logging.warning('Invalid tier {} for {}'.format(tier, framework))
        return None

    metainfo.update({
        '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'],
            # 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'
                        }
                        ]
                    },
                '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'])

    tiers = {1:[],2:[],3:[],4:[]}
    for modulename in os.listdir(args.frameworksdir):
        fwinfo = create_fw_info(args.frameworksdir, modulename)
        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()

