#============================================================================ 
#Name        : sphinx_ext.py 
#Part of     : Helium 
#
#Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
#All rights reserved.
#This component and the accompanying materials are made available
#under the terms of the License "Eclipse Public License v1.0"
#which accompanies this distribution, and is available
#at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
#Initial Contributors:
#Nokia Corporation - initial contribution.
#
#Contributors:
#
#Description:
#===============================================================================
""" Custom Sphinx operations to help with Helium doc linking. """

import os
import re
import atexit

from docutils import nodes, utils
from docutils.parsers.rst import directives

import amara

tree = None
treecache = None
database_path = os.path.abspath(os.path.join(os.getcwd() + '/build', 'public_database.xml'))

# Error count for custom sphinx operations
exit_with_failure = 0 

def check_cached_database():
    """ Check the Ant database XML data is cached as needed. """
    global tree
    global treecache    
    
    if tree == None or treecache == None:
        f = open(database_path)
        tree = amara.parse(f)
    
        treecache = {}
        for project in tree.antDatabase.project:
            for x in project.xml_children:
                if hasattr(x, 'name'):
                    treecache[str(x.name)] = [str(project.name),'project']
        if hasattr(tree.antDatabase, "antlib"):
            for antlib in tree.antDatabase.antlib:
                for x in antlib.xml_children:
                    if hasattr(x, 'name'):
                        treecache[str(x.name)] = [str(antlib.name),'antlib']
        
def handle_hlm_role(role, _, text, lineno, inliner, options=None, content=None): # pylint: disable=W0613
    """ Process a custom Helium ReStructuredText role to link to a target, property or macro. """
    if options == None:
        options = {}
    if content == None:
        content = []
        
    # See if the role is used to embed a API element field
    if '[' in text:
        role_data = _embed_role_field(role, text, lineno, inliner)
    else:
        role_data = _build_link(text, lineno, inliner, options)
        
    return role_data
    
def _embed_role_field(role, text, lineno, inliner):
    """ Insert the contents of an element field. 
    
    These take the form of e.g. hlm-p:`build.drive[summary]`
    """
    messages = []
    node = nodes.Text('', '')
    
    field_match = re.search("(.*?)\[(.*?)\]", text)
    if field_match != None:
        element_name = field_match.group(1)
        field_name = field_match.group(2)
        if field_name != None and len(field_name) > 0:
            field_value = find_field_value(role, element_name, field_name)
            if field_value != None and len(field_value) > 0:
                node = nodes.Text(field_value, utils.unescape(field_value))
            else:
                messages.append(inliner.reporter.error(('Field value cannot be found for API field: "%s".' % text), line=lineno))
        else:
            messages.append(inliner.reporter.error(('Invalid field name for API value replacement: "%s".' % text), line=lineno))
        return [node], messages

def find_field_value(role, element_name, field_name):
    """ Gets the value of a field from an API element. """
    check_cached_database()
    
    field_value = None
    element = tree.xml_xpath('//' + roles[role] + "[name='" + element_name + "']")
    
    if element != None and len(element) == 1:
        field_value_list = element[0].xml_xpath(field_name)
        if field_value_list != None and len(field_value_list) == 1:
            field_value = str(field_value_list[0])
    return field_value
    

def _build_link(text, lineno, inliner, options):
    """ Build an HTML link to the API doc location for API element. """
    global exit_with_failure
    full_path_match = re.search(r"<document source=\"(.*?)\"", str(inliner.document))
    full_path = full_path_match.group(1)
    path_segment = full_path[full_path.index('\\doc\\') + 5:]
    dir_levels = path_segment.count('\\')
    (parent_type, parent_name) = get_root_element_name(text)
    messages = []
    
    # See if link can be built
    if parent_type != None and parent_name != None:
        href_text = text.replace('.', '-').lower()
        api_path_segment = 'api/helium/' + parent_type + '-' + parent_name  + '.html#' + href_text
        relative_path = ('../' * dir_levels) + api_path_segment
        api_doc_path = os.path.abspath(os.path.join(os.getcwd() + '/build/doc', api_path_segment))
        node = nodes.reference(text, utils.unescape(text), refuri=relative_path, **options)
        node = nodes.literal(text, '', node, **options)
    # Or just insert the basic property text
    else:
        messages.append(inliner.reporter.error(('Missing API doc for "%s".' % text), line=lineno))
        node = nodes.literal(text, utils.unescape(text))
        # Error occurred so record this in order to return the total number as a failure when exiting the program
        exit_with_failure += 1 
    return [node], messages

def get_root_element_name(text):
    check_cached_database()
    
    if text in treecache:
        return (treecache[text][1], treecache[text][0])
    return (None, None)

roles = {'hlm-t': 'target',
         'hlm-p': 'property',
         'hlm-m': 'macro',}


def setup(app):
    """ Register custom RST roles for linking Helium targets, properties and macros
    to the API documentation. """
    for role in roles.keys():
        app.add_role(role, handle_hlm_role)
    app.add_description_unit('property', 'ant-prop', 'pair: %s; property')
    app.add_description_unit('target', 'ant-target', 'pair: %s; target')

    
def check_for_failure():
    """ Check whether we need to exit the program with a failure due to one or more errors in a custom Sphinx operation. """
    if exit_with_failure:
        raise SystemExit("EXCEPTION: Found %d error(s) of type '(ERROR/3) Missing API doc for <property>'" % (exit_with_failure) )

# Register a cleanup routine to handle exit with failure
atexit.register(check_for_failure)

