# coding=utf-8
"""Help text for the dock widget."""

import copy
import logging
import re
from PyQt4 import QtCore
from os.path import exists
from inspect import getmembers

import safe.definitions as definitions
import safe.definitions.post_processors
from safe.definitions.hazard_classifications import hazard_classification_type
from developer_help import content as developer_help
from safe import messaging as m
from safe.definitions.earthquake import current_earthquake_model_name
from safe.definitions.exposure import exposure_all
from safe.definitions.field_groups import (
    population_field_groups, aggregation_field_groups)
from safe.definitions.hazard_exposure_specifications import (
    specific_notes, specific_actions)
from safe.definitions.post_processors.post_processor_inputs import (
    post_processor_input_types,
    post_processor_input_values)
from safe.gui.tools.help.batch_help import content as batch_help
from safe.gui.tools.help.dock_help import content as dock_help
from safe.gui.tools.help.extent_selector_help import content as extent_help
from safe.gui.tools.help.field_mapping_help import content as \
    field_mapping_tool_help
from safe.gui.tools.help.impact_report_help import content as report_help
from safe.gui.tools.help.multi_buffer_help import content as multi_buffer_help
from safe.gui.tools.help.needs_calculator_help import content as needs_help
from safe.gui.tools.help.needs_manager_help import content as \
    needs_manager_help
from safe.gui.tools.help.options_help import content as options_help
from safe.gui.tools.help.osm_downloader_help import content as osm_help
from safe.gui.tools.help.peta_bencana_help import content as petabencana_help
from safe.gui.tools.help.shakemap_converter_help \
    import content as shakemap_help
from safe.messaging import styles
from safe.utilities.i18n import tr
from safe.utilities.resources import resource_url, resources_path
from safe.utilities.rounding import html_scientific_notation_rate
from safe.gis import expressions
from safe.report.expressions import infographic

LOGGER = logging.getLogger('InaSAFE')
# For chapter sections
# Items marked as numbered below will show section numbering in HTML render
TITLE_STYLE = styles.TITLE_LEVEL_1_STYLE  # h1 Not numbered
SECTION_STYLE = styles.SECTION_LEVEL_2_STYLE  # h2 numbered
SUBSECTION_STYLE = styles.SUBSECTION_LEVEL_3_STYLE  # h3 numbered
BLUE_CHAPTER_STYLE = styles.BLUE_LEVEL_4_STYLE  # h4 numbered
RED_CHAPTER_STYLE = styles.RED_LEVEL_4_STYLE  # h4 numbered
DETAILS_STYLE = styles.ORANGE_LEVEL_5_STYLE  # h5 numbered
DETAILS_SUBGROUP_STYLE = styles.GREY_LEVEL_6_STYLE  # h5 numbered
# For images
SMALL_ICON_STYLE = styles.SMALL_ICON_STYLE
MEDIUM_ICON_STYLE = styles.MEDIUM_ICON_STYLE
HEADING_LOOKUPS = {
    1: SECTION_STYLE,
    2: SUBSECTION_STYLE,
    3: BLUE_CHAPTER_STYLE,
    4: DETAILS_STYLE,
    5: DETAILS_SUBGROUP_STYLE,
}
HEADING_COUNTS = {
    1: 1,
    2: 0,
    3: 0,
    4: 0,
    5: 0,
}

__copyright__ = "Copyright 2016, The InaSAFE Project"
__license__ = "GPL version 3"
__email__ = "info@inasafe.org"
__revision__ = '$Format:%H$'


def definitions_help():
    """Help message for Definitions.

    .. versionadded:: 4.0.0

    :returns: A message object containing helpful information.
    :rtype: messaging.message.Message
    """
    message = m.Message()
    message.add(m.Brand())
    message.add(heading())
    message.add(content())
    return message


def heading():
    """Helper method that returns just the header.

    This method was added so that the text could be reused in the
    other contexts.

    .. versionadded:: 4.0.0

    :returns: A heading object.
    :rtype: safe.messaging.heading.Heading
    """
    message = m.Heading(tr('InaSAFE help'), **TITLE_STYLE)
    return message


def content():
    """Helper method that returns just the content.

    This method was added so that the text could be reused in the
    dock_help module.

    .. versionadded:: 4.0.0

    :returns: A message object without brand element.
    :rtype: safe.messaging.message.Message
    """
    # We will store a contents section at the top for easy navigation
    table_of_contents = m.Message()
    # and this will be the main message that we create
    message = m.Message()

    _create_section_header(
        message,
        table_of_contents,
        'overview',
        tr('Overview'),
        heading_level=1)
    ##
    # Credits and disclaimers ...
    ##
    _create_section_header(
        message,
        table_of_contents,
        'disclaimer',
        tr('Disclaimer'),
        heading_level=2)
    message.add(m.Paragraph(definitions.messages.disclaimer()))

    _create_section_header(
        message,
        table_of_contents,
        'limitations',
        tr('Limitations and License'),
        heading_level=2)
    bullets = m.BulletedList()
    for item in definitions.limitations():
        bullets.add(item)
    message.add(bullets)

    ##
    # Basic concepts ...
    ##
    ##
    # Help dialog contents ...
    ##
    _create_section_header(
        message,
        table_of_contents,
        'glossary',
        tr('Glossary of terms'),
        heading_level=1)

    last_group = None
    table = None
    for key, value in definitions.concepts.iteritems():
        current_group = value['group']
        if current_group != last_group:
            if last_group is not None:
                message.add(table)
            _create_section_header(
                message,
                table_of_contents,
                current_group.replace(' ', '-'),
                current_group,
                heading_level=2)
            table = _start_glossary_table(current_group)
            last_group = current_group
        row = m.Row()
        term = value['key'].replace('_', ' ').title()
        description = m.Message(value['description'])
        for citation in value['citations']:
            if citation['text'] in [None, '']:
                continue
            if citation['link'] in [None, '']:
                description.add(m.Paragraph(citation['text']))
            else:
                description.add(m.Paragraph(
                    m.Link(citation['link'], citation['text'])))
        row.add(m.Cell(term))
        row.add(m.Cell(description))
        url = _definition_icon_url(value)
        if url:
            row.add(m.Cell(m.Image(url, **MEDIUM_ICON_STYLE)))
        else:
            row.add(m.Cell(''))
        table.add(row)
    # ensure the last group's table is added
    message.add(table)

    ##
    # Help dialog contents ...
    ##
    _create_section_header(
        message,
        table_of_contents,
        'core-functionality',
        tr('Core functionality and tools'),
        heading_level=1)

    _create_section_header(
        message,
        table_of_contents,
        'dock',
        tr('The InaSAFE Dock'),
        heading_level=2)
    message.add(dock_help())

    _create_section_header(
        message,
        table_of_contents,
        'reports',
        tr('InaSAFE Reports'),
        heading_level=2)
    message.add(report_help())

    _create_section_header(
        message,
        table_of_contents,
        'extents',
        tr('Managing analysis extents with the extents selector'),
        heading_level=2)
    message.add(extent_help())

    _create_section_header(
        message,
        table_of_contents,
        'options',
        tr('InaSAFE Options'),
        heading_level=2)
    message.add(options_help())

    _create_section_header(
        message,
        table_of_contents,
        'batch-runner',
        tr('The Batch Runner'),
        heading_level=2)
    message.add(batch_help())

    _create_section_header(
        message,
        table_of_contents,
        'osm-downloader',
        tr('The OpenStreetmap Downloader'),
        heading_level=2)
    message.add(osm_help())

    _create_section_header(
        message,
        table_of_contents,
        'petabencana-downloader',
        tr('The PetaBencana Downloader'),
        heading_level=2)
    message.add(petabencana_help())

    _create_section_header(
        message,
        table_of_contents,
        'shakemap-converter',
        tr('The Shakemap Converter'),
        heading_level=2)
    message.add(shakemap_help())

    _create_section_header(
        message,
        table_of_contents,
        'multi-buffer-tool',
        tr('The Multi Buffer Tool'),
        heading_level=2)
    message.add(multi_buffer_help())

    # Field mapping tool has a few added bits to enumerate the groups
    _create_section_header(
        message,
        table_of_contents,
        'field-mapping-tool',
        tr('The Field Mapping Tool'),
        heading_level=2)
    message.add(field_mapping_tool_help())

    _create_section_header(
        message,
        table_of_contents,
        'exposure-groups',
        tr('Exposure Groups'),
        heading_level=3)
    message.add(m.Paragraph(
        'The following demographic groups apply only to vector population '
        'exposure layers:'
    ))
    for group in population_field_groups:
        definition_to_message(
            group, message, table_of_contents, heading_level=4)

    _create_section_header(
        message,
        table_of_contents,
        'aggregation-groups',
        tr('Aggregation Groups'),
        heading_level=3)
    message.add(m.Paragraph(
        'The following demographic groups apply only to aggregation layers:'
    ))
    for group in aggregation_field_groups:
        definition_to_message(
            group, message, table_of_contents, heading_level=4)

    # End of field mapping tool help

    # Keep this last in the tool section please as it has subsections
    # and so uses the top level section style
    _create_section_header(
        message,
        table_of_contents,
        'minimum-needs',
        tr('Minimum Needs'),
        heading_level=2)
    _create_section_header(
        message,
        table_of_contents,
        'minimum-needs-tool',
        tr('The minimum needs tool'),
        heading_level=3)
    message.add(needs_help())
    _create_section_header(
        message,
        table_of_contents,
        'minimum-manager',
        tr('The minimum needs manager'),
        heading_level=3)
    message.add(needs_manager_help())

    ##
    #  Analysis workflow
    ##

    _create_section_header(
        message,
        table_of_contents,
        'analysis-steps',
        tr('Analysis steps'),
        heading_level=1)
    _create_section_header(
        message,
        table_of_contents,
        'analysis-internal-process',
        tr('Analysis internal process'),
        heading_level=2)
    analysis = definitions.concepts['analysis']
    message.add(analysis['description'])
    url = _definition_screenshot_url(analysis)
    if url:
        message.add(m.Paragraph(m.Image(url), style_class='text-center'))

    _create_section_header(
        message,
        table_of_contents,
        'analysis-progress-reporting',
        tr('Progress reporting steps'),
        heading_level=2)
    steps = definitions.analysis_steps.values()
    for step in steps:
        definition_to_message(
            step, message, table_of_contents, heading_level=3)

    ##
    #  Hazard definitions
    ##

    _create_section_header(
        message,
        table_of_contents,
        'hazards',
        tr('Hazard Concepts'),
        heading_level=1)

    hazard_category = definitions.hazard_category
    definition_to_message(
        hazard_category,
        message,
        table_of_contents,
        heading_level=2)

    hazards = definitions.hazards
    definition_to_message(
        hazards,
        message,
        table_of_contents,
        heading_level=2)

    ##
    #  Exposure definitions
    ##

    _create_section_header(
        message,
        table_of_contents,
        'exposures',
        tr('Exposure Concepts'),
        heading_level=1)
    exposures = definitions.exposures

    definition_to_message(
        exposures,
        message,
        table_of_contents,
        heading_level=2)

    ##
    #  Defaults
    ##

    _create_section_header(
        message,
        table_of_contents,
        'defaults',
        tr('InaSAFE Defaults'),
        heading_level=1)
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Default value'), header=True))
    row.add(m.Cell(tr('Default min'), header=True))
    row.add(m.Cell(tr('Default max'), header=True))
    row.add(m.Cell(tr('Description'), header=True))
    table.add(row)
    defaults = [
        definitions.youth_ratio_default_value,
        definitions.adult_ratio_default_value,
        definitions.elderly_ratio_default_value,
        definitions.female_ratio_default_value,
        definitions.feature_rate_default_value
    ]
    for default in defaults:
        row = m.Row()
        row.add(m.Cell(default['name']))
        row.add(m.Cell(default['default_value']))
        row.add(m.Cell(default['min_value']))
        row.add(m.Cell(default['max_value']))
        row.add(m.Cell(default['description']))
        table.add(row)
    message.add(table)

    ##
    #  All Fields
    ##

    _create_section_header(
        message,
        table_of_contents,
        'all-fields',
        tr('Fields'),
        heading_level=1)
    _create_section_header(
        message,
        table_of_contents,
        'input-fields',
        tr('Input dataset fields'),
        heading_level=2)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Exposure fields'),
        definitions.exposure_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Hazard fields'),
        definitions.hazard_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Aggregation fields'),
        definitions.aggregation_fields)
    _create_section_header(
        message,
        table_of_contents,
        'output-fields',
        tr('Output dataset fields'),
        heading_level=2)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Impact fields'),
        definitions.impact_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Aggregate hazard fields'),
        definitions.aggregate_hazard_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Aggregation summary fields'),
        definitions.aggregation_summary_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Exposure summary table fields'),
        definitions.exposure_summary_table_fields)
    _create_fields_section(
        message,
        table_of_contents,
        tr('Analysis fields'),
        definitions.analysis_fields)

    ##
    #  Geometries
    ##

    _create_section_header(
        message,
        table_of_contents,
        'geometries',
        tr('Layer Geometry Types'),
        heading_level=1)
    _create_section_header(
        message,
        table_of_contents,
        'vector-geometries',
        tr('Vector'),
        heading_level=2)
    definition_to_message(
        definitions.layer_geometry_point,
        message,
        table_of_contents,
        heading_level=3)
    definition_to_message(
        definitions.layer_geometry_line,
        message,
        table_of_contents,
        heading_level=3)
    definition_to_message(
        definitions.layer_geometry_polygon,
        message,
        table_of_contents,
        heading_level=3)
    _create_section_header(
        message,
        table_of_contents,
        'raster-geometries',
        tr('Raster'),
        heading_level=2)
    definition_to_message(
        definitions.layer_geometry_raster,
        message,
        table_of_contents,
        heading_level=3)

    ##
    #  Layer Modes
    ##

    _create_section_header(
        message,
        table_of_contents,
        'layer-modes',
        tr('Layer Modes'),
        heading_level=1)
    definition_to_message(
        definitions.layer_mode,
        message,
        table_of_contents,
        heading_level=2)

    ##
    #  Layer Purposes
    ##

    _create_section_header(
        message,
        table_of_contents,
        'layer-purposes',
        tr('Layer Purposes'),
        heading_level=1)
    definition_to_message(
        definitions.layer_purpose_hazard,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_exposure,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_aggregation,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_exposure_summary,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_aggregate_hazard_impacted,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_aggregation_summary,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_exposure_summary_table,
        message,
        table_of_contents,
        heading_level=2)
    definition_to_message(
        definitions.layer_purpose_profiling,
        message,
        table_of_contents,
        heading_level=2)

    ##
    # All units
    ##

    _create_section_header(
        message,
        table_of_contents,
        'all-units',
        tr('All Units'),
        heading_level=1)
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Plural'), header=True))
    row.add(m.Cell(tr('Abbreviation'), header=True))
    row.add(m.Cell(tr('Details'), header=True))
    table.add(row)
    for unit in definitions.units_all:
        row = m.Row()
        row.add(m.Cell(unit['name']))
        row.add(m.Cell(unit['plural_name']))
        row.add(m.Cell(unit['abbreviation']))
        row.add(m.Cell(unit['description']))
        table.add(row)
    message.add(table)

    ##
    #  Post processors
    ##

    _create_section_header(
        message,
        table_of_contents,
        'post-processors',
        tr('Post Processors'),
        heading_level=1)
    _create_section_header(
        message,
        table_of_contents,
        'post-processor-input-types',
        tr('Post Processor Input Types'),
        heading_level=2)
    table = _create_post_processor_subtable(
        post_processor_input_types
    )
    message.add(table)

    _create_section_header(
        message,
        table_of_contents,
        'post-processor-input-values',
        tr('Post Processor Input Values'),
        heading_level=2)
    table = _create_post_processor_subtable(
        post_processor_input_values
    )
    message.add(table)

    _create_section_header(
        message,
        table_of_contents,
        'post-processor-process-values',
        tr('Post Processor Process Types'),
        heading_level=2)
    table = _create_post_processor_subtable(
        definitions.post_processor_process_types
    )
    message.add(table)

    _create_section_header(
        message,
        table_of_contents,
        'post-processors',
        tr('Post Processors'),
        heading_level=2)
    post_processors = safe.definitions.post_processors
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Input Fields'), header=True))
    row.add(m.Cell(tr('Output Fields'), header=True))
    table.add(row)
    for post_processor in post_processors:
        row = m.Row()
        row.add(m.Cell(post_processor['name']))
        # Input fields
        bullets = m.BulletedList()
        for key, value in post_processor['input'].iteritems():
            bullets.add(key)
        row.add(m.Cell(bullets))
        # Output fields
        bullets = m.BulletedList()
        for key, value in post_processor['output'].iteritems():
            name = value['value']['name']
            formula_type = value['type']['key']
            if formula_type == 'formula':
                formula = value['formula']
            else:
                # We use python introspection because the processor
                # uses a python function for calculations
                formula = value['function'].__name__
                formula += ' ('
                formula += value['function'].__doc__
                formula += ')'
            bullets.add('%s  %s. : %s' % (
                name, formula_type, formula))

        row.add(m.Cell(bullets))
        table.add(row)
        # Add the descriptions
        row = m.Row()
        row.add(m.Cell(''))
        row.add(m.Cell(post_processor['description'], span=2))
        table.add(row)
    message.add(table)

    ##
    # Reporting
    ##
    _create_section_header(
        message,
        table_of_contents,
        'reporting',
        tr('Reporting'),
        heading_level=1)

    message.add(m.Paragraph(tr(
        'This section of the help documentation is intended for advanced '
        'users who want to modify reports which are produced by InaSAFE.'
    )))

    _create_section_header(
        message,
        table_of_contents,
        'reporting',
        tr('QGIS Expressions'),
        heading_level=2)

    qgis_expressions = {
        fct[0]: fct[1] for fct in getmembers(expressions)
        if fct[1].__class__.__name__ == 'QgsExpressionFunction'}
    qgis_expressions.update({
        fct[0]: fct[1] for fct in getmembers(infographic)
        if fct[1].__class__.__name__ == 'QgsExpressionFunction'})

    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Description'), header=True))
    table.add(row)
    for expression_name, expression in qgis_expressions.iteritems():
        row = m.Row()
        row.add(m.Cell(expression_name))
        help = expression.helptext()
        # This pattern comes from python/qgis/core/__init__.py ≈ L79
        pattern = r'<h3>(.*) function</h3><br>'
        help = re.sub(pattern, '', help)
        help = re.sub(r'\n', '<br>', help)
        row.add(m.Cell(help))
        table.add(row)
    message.add(table)

    ##
    # Developer documentation
    ##

    _create_section_header(
        message,
        table_of_contents,
        'developer-guide',
        tr('Developer Guide'),
        heading_level=1)
    message.add(developer_help())

    # Finally we add the table of contents at the top
    full_message = m.Message()
    # Contents is not a link so reset style
    style = SECTION_STYLE
    style['element_id'] = ''
    header = m.Heading(tr('Contents'), **style)
    full_message.add(header)
    full_message.add(table_of_contents)
    full_message.add(message)
    return full_message


def _start_glossary_table(group):
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    # first col for icons if present
    row.add(m.Cell(tr('Term'), header=True))
    row.add(m.Cell(tr('Description'), header=True))
    row.add(m.Cell(tr(''), header=True))
    table.add(row)
    return table


def _create_section_header(
        message,
        table_of_contents,
        element_id,
        text,
        heading_level=1):
    # Warning a side effect here is that the SECTION_STYLE is updated
    # when setting style as we modify the id so we have to make a deep copy
    style = copy.deepcopy(HEADING_LOOKUPS[heading_level])
    style['element_id'] = element_id

    HEADING_COUNTS[heading_level] += 1
    # Reset the heading counts for headings below this level
    # Also calculate the index of the TOC entry
    index_number = ''
    for key in HEADING_COUNTS.keys():
        if key > heading_level:
            HEADING_COUNTS[key] = 0
        else:
            index_number += str(HEADING_COUNTS[key]) + '.'

    header = m.Heading(text, **style)
    link = m.Link('#%s' % element_id, index_number + ' ' + text)
    # See bootstrap docs for ml-1 explanation
    # https://v4-alpha.getbootstrap.com/utilities/spacing/#examples
    paragraph = m.Paragraph(
        link,
        style_class='ml-%i' % heading_level)
    table_of_contents.add(paragraph)
    message.add(header)


def _create_post_processor_subtable(item_list):
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Description'), header=True))
    table.add(row)
    for item in item_list:
        row = m.Row()
        row.add(m.Cell(item['key']))
        row.add(m.Cell(item['description']))
        table.add(row)
    return table


def _create_fields_section(message, table_of_contents, title, fields):
    _create_section_header(
        message,
        table_of_contents,
        title.replace(' ', '-'),
        title,
        heading_level=3)
    table = _create_fields_table()
    for field in fields:
        _add_field_to_table(field, table)
    message.add(table)


def definition_to_message(
        definition, message=None, table_of_contents=None, heading_level=None):
    """Helper function to render a definition to a message.

    :param definition: A definition dictionary (see definitions package).
    :type definition: dict

    :param message: The message that the definition should be appended to.
    :type message: parameters.message.Message

    :param table_of_contents: Table of contents that the headings should be
        included in.
    :type message: parameters.message.Message

    :param heading_level: Optional style to apply to the definition
        heading. See HEADING_LOOKUPS
    :type heading_level: int

    :returns: Message
    :rtype: str
    """

    if message is None:
        message = m.Message()

    if table_of_contents is None:
        table_of_contents = m.Message()

    if heading_level:
        _create_section_header(
            message,
            table_of_contents,
            definition['name'].replace(' ', '-'),
            definition['name'],
            heading_level=heading_level)
    else:
        header = m.Paragraph(m.ImportantText(definition['name']))
        message.add(header)

    # If the definition has an icon, we put the icon and description side by
    # side in a table otherwise just show the description as a paragraph
    url = _definition_icon_url(definition)
    if url is None:
        message.add(m.Paragraph(definition['description']))
        if 'citations' in definition:
            _citations_to_message(message, definition)
    else:
        LOGGER.info('Creating mini table for definition description: ' + url)
        table = m.Table(style_class='table table-condensed')
        row = m.Row()
        row.add(m.Cell(m.Image(url, **MEDIUM_ICON_STYLE)))
        row.add(m.Cell(definition['description']))
        table.add(row)
        for citation in definition['citations']:
            if citation['text'] in [None, '']:
                continue
            row = m.Row()
            row.add(m.Cell(''))
            if citation['link'] in [None, '']:
                row.add(m.Cell(citation['text']))
            else:
                row.add(m.Cell(m.Link(citation['link'], citation['text'])))
            table.add(row)
        message.add(table)

    url = _definition_screenshot_url(definition)
    if url:
        message.add(m.Paragraph(m.Image(url), style_class='text-center'))

    # types contains e.g. hazard_all
    if 'types' in definition:
        for sub_definition in definition['types']:
            definition_to_message(
                sub_definition,
                message,
                table_of_contents,
                heading_level=3)

    #
    # Notes section if available
    #

    if 'notes' in definition:
        # Start a notes details group too since we have an exposure
        message.add(m.Heading(
            tr('Notes:'), **DETAILS_STYLE))
        message.add(m.Heading(
            tr('General notes:'), **DETAILS_SUBGROUP_STYLE))
        bullets = m.BulletedList()
        for note in definition['notes']:
            if type(note) is dict:
                bullets = _add_dict_to_bullets(bullets, note)
            elif note:
                bullets.add(m.Text(note))
        message.add(bullets)
        if 'citations' in definition:
            _citations_to_message(message, definition)

    # This only for EQ
    if 'earthquake_fatality_models' in definition:
        current_function = current_earthquake_model_name()
        paragraph = m.Paragraph(
            'The following earthquake fatality models are available in '
            'InaSAFE. Note that you need to set one of these as the '
            'active model in InaSAFE Options. The currently active model is: ',
            m.ImportantText(current_function)
        )
        message.add(paragraph)

        models_definition = definition['earthquake_fatality_models']
        for model in models_definition:
            message.add(m.Heading(model['name'], **DETAILS_SUBGROUP_STYLE))
            if 'description' in model:
                paragraph = m.Paragraph(model['description'])
                message.add(paragraph)
            for note in model['notes']:
                paragraph = m.Paragraph(note)
                message.add(paragraph)
            _citations_to_message(message, model)

    for exposure in exposure_all:
        extra_exposure_notes = specific_notes(definition, exposure)
        if extra_exposure_notes:
            title = tr(u'Notes for exposure : {exposure_name}').format(
                exposure_name=exposure['name'])
            message.add(m.Heading(title, **DETAILS_SUBGROUP_STYLE))
            bullets = m.BulletedList()
            for note in extra_exposure_notes:
                if type(note) is dict:
                    bullets = _add_dict_to_bullets(bullets, note)
                elif note:
                    bullets.add(m.Text(note))
            message.add(bullets)

    if 'continuous_notes' in definition:
        message.add(m.Heading(
            tr('Notes for continuous datasets:'),
            **DETAILS_SUBGROUP_STYLE))
        bullets = m.BulletedList()
        for note in definition['continuous_notes']:
            bullets.add(m.Text(note))
        message.add(bullets)

    if 'classified_notes' in definition:
        message.add(m.Heading(
            tr('Notes for classified datasets:'),
            **DETAILS_SUBGROUP_STYLE))
        bullets = m.BulletedList()
        for note in definition['classified_notes']:
            bullets.add(m.Text(note))
        message.add(bullets)

    if 'single_event_notes' in definition:
        message.add(
            m.Heading(tr('Notes for single events'), **DETAILS_STYLE))
        if len(definition['single_event_notes']) < 1:
            message.add(m.Paragraph(tr('No single event notes defined.')))
        else:
            bullets = m.BulletedList()
            for note in definition['single_event_notes']:
                bullets.add(m.Text(note))
            message.add(bullets)

    if 'multi_event_notes' in definition:
        message.add(
            m.Heading(
                tr('Notes for multi events / scenarios:'),
                **DETAILS_STYLE))
        if len(definition['multi_event_notes']) < 1:
            message.add(m.Paragraph(tr('No multi-event notes defined.')))
        else:
            bullets = m.BulletedList()
            for note in definition['multi_event_notes']:
                bullets.add(m.Text(note))
            message.add(bullets)

    if 'actions' in definition:
        message.add(m.Paragraph(m.ImportantText(tr('Actions:'))))
        bullets = m.BulletedList()
        for note in definition['actions']:
            if type(note) is dict:
                bullets = _add_dict_to_bullets(bullets, note)
            elif note:
                bullets.add(m.Text(note))
        message.add(bullets)

    for exposure in exposure_all:
        extra_exposure_actions = specific_actions(definition, exposure)
        if extra_exposure_actions:
            title = tr(u'Actions for exposure : {exposure_name}').format(
                exposure_name=exposure['name'])
            message.add(m.Heading(title, **DETAILS_SUBGROUP_STYLE))
            bullets = m.BulletedList()
            for note in extra_exposure_actions:
                if type(note) is dict:
                    bullets = _add_dict_to_bullets(bullets, note)
                elif note:
                    bullets.add(m.Text(note))
            message.add(bullets)

    if 'continuous_hazard_units' in definition:
        message.add(m.Paragraph(m.ImportantText(tr('Units:'))))
        table = m.Table(style_class='table table-condensed table-striped')
        row = m.Row()
        row.add(m.Cell(tr('Name'), header=True))
        row.add(m.Cell(tr('Plural'), header=True))
        row.add(m.Cell(tr('Abbreviation'), header=True))
        row.add(m.Cell(tr('Details'), header=True))
        table.add(row)
        for unit in definition['continuous_hazard_units']:
            row = m.Row()
            row.add(m.Cell(unit['name']))
            row.add(m.Cell(unit['plural_name']))
            row.add(m.Cell(unit['abbreviation']))
            row.add(m.Cell(unit['description']))
            table.add(row)
        message.add(table)

    if 'fields' in definition:
        message.add(m.Paragraph(m.ImportantText(tr('Fields:'))))
        table = _create_fields_table()

        if 'extra_fields' in definition:
            all_fields = definition['fields'] + definition['extra_fields']
        else:
            all_fields = definition['fields']

        for field in all_fields:
            _add_field_to_table(field, table)
        message.add(table)

    if 'classifications' in definition:
        message.add(m.Heading(
            tr('Hazard classifications'),
            **DETAILS_STYLE))
        message.add(m.Paragraph(
            definitions.hazard_classification['description']))
        for inasafe_class in definition['classifications']:
            definition_to_message(
                inasafe_class,
                message,
                table_of_contents,
                heading_level=3)

    if 'classes' in definition:
        message.add(m.Paragraph(m.ImportantText(tr('Classes:'))))
        is_hazard = definition['type'] == hazard_classification_type
        if is_hazard:
            table = _make_defaults_hazard_table()
        else:
            table = _make_defaults_exposure_table()

        for inasafe_class in definition['classes']:
            row = m.Row()
            if is_hazard:
                # name() on QColor returns its hex code
                if 'color' in inasafe_class:
                    colour = inasafe_class['color'].name()
                    row.add(m.Cell(
                        u'', attributes='style="background: %s;"' % colour))
                else:
                    row.add(m.Cell(u' '))

            row.add(m.Cell(inasafe_class['name']))

            if is_hazard:
                if 'affected' in inasafe_class:
                    row.add(m.Cell(tr(inasafe_class['affected'])))
                else:
                    row.add(m.Cell(tr('unspecified')))

            if is_hazard:
                if inasafe_class.get('fatality_rate') > 0:
                    # we want to show the rate as a scientific notation
                    rate = html_scientific_notation_rate(
                        inasafe_class['fatality_rate'])
                    rate = u'%s%%' % rate
                    row.add(m.Cell(rate))
                elif inasafe_class.get('fatality_rate') == 0:
                    row.add(m.Cell('0%'))
                else:
                    row.add(m.Cell(tr('unspecified')))

            if is_hazard:
                if 'displacement_rate' in inasafe_class:
                    rate = inasafe_class['displacement_rate'] * 100
                    rate = u'%.0f%%' % rate
                    row.add(m.Cell(rate))
                else:
                    row.add(m.Cell(tr('unspecified')))

            if 'string_defaults' in inasafe_class:
                defaults = None
                for default in inasafe_class['string_defaults']:
                    if defaults:
                        defaults += ',%s' % default
                    else:
                        defaults = default
                row.add(m.Cell(defaults))
            else:
                row.add(m.Cell(tr('unspecified')))

            if is_hazard:
                # Min may be a single value or a dict of values so we need
                # to check type and deal with it accordingly
                if 'numeric_default_min' in inasafe_class:
                    if isinstance(inasafe_class['numeric_default_min'], dict):
                        bullets = m.BulletedList()
                        minima = inasafe_class['numeric_default_min']
                        for key, value in minima.iteritems():
                            bullets.add(u'%s : %s' % (key, value))
                        row.add(m.Cell(bullets))
                    else:
                        row.add(m.Cell(inasafe_class['numeric_default_min']))
                else:
                    row.add(m.Cell(tr('unspecified')))

            if is_hazard:
                # Max may be a single value or a dict of values so we need
                # to check type and deal with it accordingly
                if 'numeric_default_max' in inasafe_class:
                    if isinstance(inasafe_class['numeric_default_max'], dict):
                        bullets = m.BulletedList()
                        maxima = inasafe_class['numeric_default_max']
                        for key, value in maxima.iteritems():
                            bullets.add(u'%s : %s' % (key, value))
                        row.add(m.Cell(bullets))
                    else:
                        row.add(m.Cell(inasafe_class['numeric_default_max']))
                else:
                    row.add(m.Cell(tr('unspecified')))

            table.add(row)
            # Description goes in its own row with spanning
            row = m.Row()
            row.add(m.Cell(''))
            row.add(m.Cell(inasafe_class['description'], span=7))
            table.add(row)

        # For hazard classes we also add the 'not affected' class manually:
        if definition['type'] == definitions.hazard_classification_type:
            row = m.Row()
            colour = definitions.not_exposed_class['color'].name()
            row.add(m.Cell(
                u'', attributes='style="background: %s;"' % colour))
            description = definitions.not_exposed_class['description']
            row.add(m.Cell(description, span=7))
            table.add(row)

        message.add(table)

    if 'affected' in definition:
        if definition['affected']:
            message.add(m.Paragraph(tr(
                'Exposure entities in this class ARE considered affected')))
        else:
            message.add(m.Paragraph(tr(
                'Exposure entities in this class are NOT considered '
                'affected')))

    if 'optional' in definition:
        if definition['optional']:
            message.add(m.Paragraph(tr(
                'This class is NOT required in the hazard keywords.')))
        else:
            message.add(m.Paragraph(tr(
                'This class IS required in the hazard keywords.')))

    return message


def _citations_to_message(message, definition):
    citations = m.Message()
    bullets = m.BulletedList()
    for citation in definition['citations']:
        if citation['text'] in [None, '']:
            continue
        if citation['link'] in [None, '']:
            bullets.add(m.Paragraph(citation['text']))
        else:
            bullets.add(m.Paragraph(
                m.Link(citation['link'], citation['text'])))
    if not bullets.is_empty():
        citations.add(m.Paragraph(m.ImportantText(
            tr('Citations:')
        )))
        citations.add(bullets)
        message.add(citations)


def _definition_icon_url(definition):
    if 'key' not in definition:
        return None
    svg_image_path = resources_path(
        'img', 'definitions', definition['key'] + '.svg')
    png_image_path = resources_path(
        'img', 'definitions', definition['key'] + '.png')
    if exists(svg_image_path):
        url = resource_url(svg_image_path)
    elif exists(png_image_path):
        url = resource_url(png_image_path)
    else:
        url = None
    return url


def _definition_screenshot_url(definition):
    jpg_image_path = resources_path(
        'img', 'definitions', definition['key'] + '_screenshot.jpg')
    png_image_path = resources_path(
        'img', 'definitions', definition['key'] + '_screenshot.png')
    if exists(jpg_image_path):
        url = resource_url(jpg_image_path)
    elif exists(png_image_path):
        url = resource_url(png_image_path)
    else:
        url = None
    return url


def _create_fields_table():
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Field Name'), header=True))
    row.add(m.Cell(tr('Type'), header=True))
    row.add(m.Cell(tr('Length'), header=True))
    row.add(m.Cell(tr('Precision'), header=True))
    table.add(row)
    return table


def _add_field_to_table(field, table):
    row = m.Row()
    row.add(m.Cell(field['name']))
    row.add(m.Cell(field['field_name']))
    field_types = None
    if not isinstance(field['type'], list):
        field_types = '%s' % _type_to_string(field['type'])
    else:
        # List of field types are supported but the user will only care
        # about the simple types so we turn them into simple names (whole
        # number, decimal number etc. and then strip out the duplicates
        unique_list = []
        # First iterate the types found in the definition to get english names
        for field_type in field['type']:
            field_type_string = _type_to_string(field_type)
            if field_type_string not in unique_list:
                unique_list.append(field_type_string)
        # now iterate the unque list and write to a sentence
        for field_type in unique_list:
            if field_types:
                field_types += ', %s' % unicode(field_type)
            else:
                field_types = unicode(field_type)
    row.add(m.Cell(field_types))
    row.add(m.Cell(field['precision']))
    table.add(row)
    # Description goes in its own row with spanning
    row = m.Row()
    row.add(m.Cell(field['description'] + ' ' + field['help_text'], span=6))
    table.add(row)


def _type_to_string(value):
    type_map = {
        QtCore.QVariant.Double: tr('Decimal number'),
        QtCore.QVariant.String: tr('Text'),
        QtCore.QVariant.Int: tr('Whole number'),
        QtCore.QVariant.UInt: tr('Whole number'),
        QtCore.QVariant.LongLong: tr('Whole number'),
        QtCore.QVariant.ULongLong: tr('Whole number'),
        6: tr('Decimal number'),
        10: tr('Text'),
        2: tr('Whole number')
    }
    return type_map[value]


def _make_defaults_hazard_table():
    """Build headers for a table related to hazard classes.

    :return: A table with headers.
    :rtype: m.Table
    """
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    # first row is for colour - we dont use a header here as some tables
    # do not have colour...
    row.add(m.Cell(tr(''), header=True))
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Affected'), header=True))
    row.add(m.Cell(tr('Fatality rate'), header=True))
    row.add(m.Cell(tr('Displacement rate'), header=True))
    row.add(m.Cell(tr('Default values'), header=True))
    row.add(m.Cell(tr('Default min'), header=True))
    row.add(m.Cell(tr('Default max'), header=True))
    table.add(row)
    return table


def _make_defaults_exposure_table():
    """Build headers for a table related to exposure classes.

    :return: A table with headers.
    :rtype: m.Table
    """
    table = m.Table(style_class='table table-condensed table-striped')
    row = m.Row()
    row.add(m.Cell(tr('Name'), header=True))
    row.add(m.Cell(tr('Default values'), header=True))
    table.add(row)
    return table


def _add_dict_to_bullets(target, value):
    """Add notes and actions in dictionary to the bullets

    :param target: Target bullets
    :type target: Bullets

    :param value: Dictionary that contains actions or notes
    :type value: dict

    :return: Updated bullets
    :rtype: Bullets
    """
    actions = value.get('action_list')
    notes = value.get('item_list')
    items_tobe_added = []
    if actions:
        items_tobe_added = actions
    elif notes:
        items_tobe_added = notes

    if items_tobe_added:
        for item in items_tobe_added:
            target.add(m.Text(item))
    return target
