# libgo - script to build library.gnome.org
# Copyright (C) 2007-2009  Frederic Peters
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301  USA

import glob
import logging
import os
import re
import shutil
import stat
import subprocess
import tarfile
import tempfile

try:
    import elementtree.ElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET

import errors
from base import DocModule

class GnomeDocbookModule(DocModule):
    '''Class for documentation shipped in a tarball and using gnome-doc-utils'''
    db2html_xsl_file = os.path.join(data_dir, 'xslt', 'db2html.xsl')
    category = None

    related_xsl_files = ['db2html.xsl', 'heading.xsl']

    def create_from_tar(cls, tar, tarinfo, makefile_am, nightly):
        self = super(GnomeDocbookModule, cls).create_from_tar(tar, tarinfo, makefile_am, nightly)
        if self.modulename == '@PACKAGE_NAME@':
            # ekiga has this, use another way, looking at omf files
            try:
                omf_file = [x.name for x in tar.getmembers() if \
                           x.name.startswith(self.dirname) and x.name.endswith('.omf.in')][0]
            except IndexError:
                logging.error('failed to get DOC_MODULE for %s' % tarinfo.name)
            self.modulename = os.path.split(omf_file)[-1][:-len('.omf.in')]
        return self
    create_from_tar = classmethod(create_from_tar)

    def setup_channel(self):
        # get category from omf file
        ext_dirname = os.path.join(app.config.private_dir, 'extracts')
        omf_file = glob.glob(os.path.join(ext_dirname, self.dirname) + '/*.omf.in')
        if not omf_file:
            omf_file = glob.glob(os.path.join(ext_dirname, self.dirname) + '/C/*.omf.in')
        if omf_file:
            try:
                self.category = ET.parse(omf_file[0]).find('resource/subject').attrib['category']
            except (IndexError, KeyError):
                pass

        channel = 'users'
        if self.category and (self.category.startswith('GNOME|Development') or
                self.category.startswith('GNOME|Applications|Programming')):
            channel = 'devel'

        self.channel = app.overlay.get_channel_overlay(self.modulename, channel)
        if self.channel == 'misc' and not self.category:
            self.category = 'GNOME|Other'

    def __str__(self):
        return 'gnome-doc-utils module at %s' % self.dirname

    def process(self):
        doc_module = self.modulename
        ext_dirname = os.path.join(app.config.private_dir, 'extracts')

        try:
            doc_linguas = re.findall(r'DOC_LINGUAS\s+=[\t ](.*)',
                    self.makefile_am)[0].split()
            if not 'en' in doc_linguas:
                doc_linguas.append('en')
            if '$(NULL)' in doc_linguas:
                doc_linguas.remove('$(NULL)')
        except IndexError:
            doc_linguas = ['en']

        try:
            doc_figures = re.findall('DOC_FIGURES\s+=\s+(.*)',
                    self.makefile_am)[0].split()
            figures_dirname = os.path.join(ext_dirname, self.dirname, 'C')
            for doc_figure in doc_figures:
                if not os.path.exists(os.path.join(figures_dirname, doc_figure)):
                    logging.warning('figure (%s) listed but not present, going to autodiscover' % \
                        doc_figure)
                    raise IndexError()
        except IndexError:
            figures_dirname = os.path.join(ext_dirname, self.dirname, 'C', 'figures')
            images_dirname = os.path.join(ext_dirname, self.dirname, 'C', 'images')
            doc_figures = []
            if os.path.exists(figures_dirname):
                doc_figures += [os.path.join('figures', x) for x in \
                        os.listdir(figures_dirname) \
                        if os.path.splitext(x)[1] in ('.png', '.jpg', '.jpeg')]
            if os.path.exists(images_dirname):
                doc_figures += [os.path.join('images', x) for x in \
                        os.listdir(images_dirname) \
                        if os.path.splitext(x)[1] in ('.png', '.jpg', '.jpeg')]

        doc_linguas.sort()
        if app.config.languages:
            for lang in doc_linguas[:]:
                if lang not in app.config.languages + ['C']:
                    doc_linguas.remove(lang)

        doc = self.get_libgo_document(doc_linguas)
        if not doc:
            return

        if self.category:
            doc.category = self.category
            doc.toc_id = app.toc_mapping.get(doc.category)

        web_output_dir = app.get_module_web_output_dir(self)

        quirks = app.overlay.get_quirks(self)
        if not 'flat-rendering' in quirks:
            doc.single_page_alternative = True

        logging.info('generating doc in %s' % web_output_dir[len(app.config.output_dir):])
        if not os.path.exists(web_output_dir):
            os.makedirs(web_output_dir)

        if app.config.create_tarballs:
            temporary_tarball_dir = tempfile.mkdtemp()
            tarball_name = '%s-html-%s.tar.gz' % (self.modulename, self.version)
            tarball_filepath = os.path.join(
                            app.get_module_web_output_dir(self, versioned=False),
                            tarball_name)

            if os.path.exists(tarball_filepath):
                # doc already exists, keep it in tarballs dictionary as it may
                # be skipped in this run, if the documentation is already
                # uptodate.
                doc.tarballs[self.one_dot_version] = tarball_name

        base_tarball_name = os.path.basename(self.filename).rsplit('-', 1)[0]

        create_tarball = app.config.create_tarballs

        for lang in doc.languages:
            if lang == 'en' and not os.path.exists(os.path.join(ext_dirname, self.dirname, 'en')):
                lang_dirname = os.path.join(ext_dirname, self.dirname, 'C')
            else:
                lang_dirname = os.path.join(ext_dirname, self.dirname, lang)

            xml_file = os.path.join(lang_dirname, doc_module + '.xml')
            if not os.path.exists(xml_file):
                # the document had a translation available in a previous
                # version, and it got removed
                continue

            xml_index_file = os.path.join(web_output_dir, 'index.xml.%s' % lang)
            skip_html_files = False
            if not app.rebuild_all and (
                    app.rebuild_language is None or
                    lang != app.rebuild_language) and os.path.exists(xml_index_file):
                mtime = os.stat(xml_index_file)[stat.ST_MTIME]
                if mtime > max(self.mtime_tarball, self.mtime_xslt_files):
                    try:
                        tree = self.process_xml_index(xml_index_file, doc, lang)
                    except errors.DepreciatedDocumentation:
                        logging.info('skipped %s (%s) (depreciated documentation)' % (
                                    self.modulename, self.one_dot_version))
                        break

                    if not create_tarball or os.path.exists(tarball_filepath):
                        logging.debug('using already generated doc in %s' % lang)
                        create_tarball = False
                        continue
                    skip_html_files = True

            if 'missing-id-on-top-book-element' in quirks:
                # Evolution documentation top element is currently <book
                # lang="en"> but the gnome-doc-utils stylesheets are
                # looking for the id # attribute to get filename.
                # -- http://bugzilla.gnome.org/show_bug.cgi?id=462811
                t = open(xml_file).read()
                open(xml_file + '.fixed', 'w').write(t.replace('\n<book ', '\n<book id="index" '))
                xml_file = xml_file + '.fixed'

            if 'correct-article-index' in quirks:
                # 2.20 release notes had <sect1 id="index"><title>Introduction...
                # instead of <sect1 id="intro"><title>Introduction... and that
                # prevented a correct id="index" on top <article>
                # older release notes had <article id="article"
                t = open(xml_file).read()
                open(xml_file + '.fixed', 'w').write(
                        re.sub('(<article.*?)(\sid=".*?")', r'\1', t # remove id on <article>
                        ).replace('<article ', '<article id="index" '
                        ).replace('<sect1 id="index"', '<sect1 id="intro"'))
                xml_file = xml_file + '.fixed'

            if not skip_html_files:
                # format docbook into html files
                cmd = ['xsltproc', '--output', web_output_dir + '/',
                        '--nonet', '--xinclude',
                        '--stringparam', 'libgo.lang', lang,
                        '--stringparam', 'libgo.channel', self.channel,
                        self.db2html_xsl_file, xml_file]

                if self.nightly:
                    cmd[5:5] = ['--param', 'libgo.nightly', 'true()']

                if app.config.symbols_dbm_filepath:
                    cmd.insert(-2, '--param')
                    cmd.insert(-2, 'libgo.dbm_support')
                    cmd.insert(-2, 'true()')

                onepage_cmd = cmd[:]

                # 1st, generate a single page with all documentation, and
                # rename it to $modulename.html.$lang (unless a single page
                # is the default rendering)
                if not 'flat-rendering' in quirks:
                    onepage_cmd.insert(5, '--stringparam')
                    onepage_cmd.insert(6, 'db.chunk.max_depth')
                    onepage_cmd.insert(7, '0')
                    logging.debug('executing %s' % ' '.join(onepage_cmd))
                    rc = subprocess.call(onepage_cmd)
                    if rc != 0:
                        logging.warn('%s failed with error %d' % (' '.join(onepage_cmd), rc))
                    index_html = os.path.join(web_output_dir, 'index.html.%s' % lang)
                    if os.path.exists(index_html):
                        os.rename(index_html,
                            os.path.join(web_output_dir, '%s.html.%s' % (doc_module, lang)))

                # 2nd, generate default rendering, to index.html.$lang
                if 'flat-rendering' in quirks:
                    cmd.insert(5, '--stringparam')
                    cmd.insert(6, 'db.chunk.max_depth')
                    cmd.insert(7, '0')
                    cmd.insert(5, '--stringparam')
                    cmd.insert(6, 'db.chunk.autotoc_depth')
                    cmd.insert(7, '1')
                if 'languages-in-sidebar' in quirks:
                    cmd.insert(5, '--stringparam')
                    cmd.insert(6, 'libgo.languages_in_sidebar')
                    cmd.insert(7, ','.join(sorted(doc_linguas)))
                if 'friends-of-gnome-ruler' in quirks:
                    cmd.insert(5, '--param')
                    cmd.insert(6, 'libgo.friends_of_gnome_ruler')
                    cmd.insert(7, 'true()')
                if 'hide-section-numbers' in quirks:
                    cmd.insert(5, '--param')
                    cmd.insert(6, 'libgo.hide_section_numbers')
                    cmd.insert(7, 'true()')
                logging.debug('executing %s' % ' '.join(cmd))
                rc = subprocess.call(cmd)
                if rc != 0:
                    logging.warn('%s failed with error %d' % (' '.join(cmd), rc))
                
                if not os.path.exists(xml_index_file):
                    logging.warn('no index file were created for %s' % doc_module)
                    continue

            if create_tarball:
                # another formatting, to ship in tarball
                cmd = ['xsltproc', '--output', os.path.join(temporary_tarball_dir, lang) + '/',
                        '--nonet', '--xinclude',
                        '--stringparam', 'libgo.lang', lang,
                        '--param', 'libgo.tarball', 'true()',
                        '--stringparam', 'db.chunk.extension', '.html',
                        '--stringparam', 'theme.icon.admon.path', '',
                        '--stringparam', 'theme.icon.nav.previous', 'nav-previous.png',
                        '--stringparam', 'theme.icon.nav.next', 'nav-next.png',
                        self.db2html_xsl_file, xml_file]
                logging.debug('executing %s' % ' '.join(cmd))
                rc = subprocess.call(cmd)
                if rc != 0:
                    logging.warn('%s failed with error %d' % (' '.join(cmd), rc))

            if doc_figures:
                # and copy images/
                logging.debug('copying figures')
                for doc_figure in doc_figures:
                    src = os.path.join(lang_dirname, doc_figure)
                    if not os.path.exists(src):
                        # fallback to image from C locale.
                        src = os.path.join(ext_dirname, self.dirname, 'C', doc_figure)
                        if not os.path.exists(src):
                            continue
                    dst = os.path.join(web_output_dir, doc_figure + '.%s' % lang)
                    if not os.path.exists(os.path.split(dst)[0]):
                        os.makedirs(os.path.split(dst)[0])
                    open(dst, 'w').write(open(src, 'r').read())

                    if create_tarball:
                        # for tarball, adds symlink, it will be followed when
                        # creating the tarball
                        dst = os.path.join(temporary_tarball_dir, lang, doc_figure)
                        if not os.path.exists(os.path.split(dst)[0]):
                            os.makedirs(os.path.split(dst)[0])
                        if not os.path.exists(dst):
                            os.symlink(os.path.abspath(src), dst)

            try:
                tree = self.process_xml_index(xml_index_file, doc, lang)
            except errors.DepreciatedDocumentation:
                logging.info('skipped %s (%s) (depreciated documentation)' % (
                            self.modulename, self.one_dot_version))
                break

            # most documentation have @id == 'index', which is perfect for web
            # publishing, for others, create a symlink from index.html.@lang.
            html_index_file = tree.getroot().attrib.get('index')
            if not html_index_file:
                logging.warn('empty html index file for module %s' % doc_module)
            elif html_index_file != 'index':
                link_html_index_file = os.path.join(
                        web_output_dir, 'index.html.%s' % lang)
                try:
                    os.symlink('%s.html.%s' % (html_index_file, lang), link_html_index_file)
                except OSError:
                    logging.warn('failed to create symlink to index file for module %s' % doc_module)

                if create_tarball:
                    dst = os.path.join(temporary_tarball_dir, lang, 'index.html')
                    os.symlink('%s.html' % html_index_file, dst)

            if create_tarball:
                # also copy some static files from data/skin/
                for filename in os.listdir(os.path.join(data_dir, 'skin')):
                    if not (filename.startswith('nav-') or filename.startswith('admon-')):
                        continue
                    src = os.path.join(data_dir, 'skin', filename)
                    dst = os.path.join(temporary_tarball_dir, lang, os.path.basename(filename))
                    os.symlink(os.path.abspath(src), dst)
        else:
            if create_tarball:
                self.create_tarball(doc, temporary_tarball_dir, tarball_filepath)
                self.create_tarball_symlink(doc)

        if app.config.create_tarballs:
            shutil.rmtree(temporary_tarball_dir)

        self.install_version_symlinks(doc)

    def process_xml_index(self, xml_index_file, doc, lang):
        tree = ET.parse(xml_index_file)

        title_node_text = None
        if tree.find('title') is not None:
            title_node_text = tree.find('title').text
        elif tree.find('{http://www.w3.org/1999/xhtml}title') is not None:
            title_node_text = tree.find('{http://www.w3.org/1999/xhtml}title').text

        if title_node_text == 'Problem showing document':
            # title used in gnome-panel for depreciated documentation (such
            # as window-list applet, moved to user guide); abort now, and
            # remove this version.  Note it would be much easier if this
            # could be detected earlier, perhaps with a marker in
            # Makefile.am or the OMF file.
            doc.versions.remove(self.one_dot_version)
            if len(doc.versions) == 0:
                # there were no other version, remove it completely
                app.documents.remove(doc)
            else:
                # there was another version, fix up path to point to that one
                previous_one_dot_version = re.match(r'\d+\.\d+',
                        doc.versions[-1]).group()
                if doc.version_keywords.get(previous_one_dot_version) != 'stable':
                    # set link to /module//previous.version/
                    doc.path = '/' + os.path.join(self.channel, self.modulename,
                            previous_one_dot_version) + '/'
                else:
                    # set link to /module/stable/
                    doc.path = '/' + os.path.join(self.channel, self.modulename,
                            'stable') + '/'
            raise errors.DepreciatedDocumentation()

        quirks = app.overlay.get_quirks(self)

        if not self.nightly or not doc.title.get(lang) and title_node_text:
            doc.title[lang] = title_node_text

        if tree.find('abstract') is not None and tree.find('abstract').text:
            doc.abstract[lang] = tree.find('abstract').text
        elif tree.find('{http://www.w3.org/1999/xhtml}abstract') is not None and \
                tree.find('{http://www.w3.org/1999/xhtml}abstract').text:
            doc.abstract[lang] = tree.find('{http://www.w3.org/1999/xhtml}abstract').text

        if 'no-title-and-abstract-from-document' in quirks:
            if doc.title.get(lang):
                del doc.title[lang]
            if doc.abstract.get(lang):
                del doc.abstract[lang]

        return tree

    def create_tarball(self, doc, temporary_dir, tarball_filepath):
        logging.info('generating doc tarball in %s' % tarball_filepath[
                len(app.config.output_dir):])
        tar = tarfile.open(tarball_filepath, 'w:gz')
        base_tarball_dir = '%s-html-%s' % (self.modulename, self.version)
        for base, dirs, files in os.walk(temporary_dir):
            base_dir = '%s/%s' % (base_tarball_dir, base[len(temporary_dir):])
            for file in files:
                orig_file = os.path.join(base, file)
                if os.path.islink(orig_file):
                    if os.readlink(orig_file)[0] == '/':
                        orig_file = os.path.realpath(orig_file)
                tarinfo = tar.gettarinfo(orig_file, '%s/%s' % (base_dir, file))
                tar.addfile(tarinfo, open(orig_file))
        tar.close()

        doc.tarballs[self.one_dot_version] = os.path.basename(tarball_filepath)


