# Module for managing variables and expressions that can be used in a report.
import json
import typing
from collections import namedtuple
from operator import attrgetter

from qgis.core import QgsExpressionContext
from qgis.core import QgsLayout
from qgis.core import QgsRasterLayer
from qgis.PyQt.QtCore import QCoreApplication

from ..conf import Setting
from ..conf import settings_manager
from ..jobs.models import Job
from ..utils import utc_to_local

# Most (if not) all of the job variables' values will be set at runtime
JobAttrVarInfo = namedtuple(
    "JobAttrVarInfo", "job_attr var_name default_value fmt_func"
)


# Values extracted from the report settings
ReportSettingVarInfo = namedtuple(
    "ReportSettingVarInfo", "setting var_name default_value"
)


# Information about the current job layer being processed
LayerVarInfo = namedtuple("LayerVarInfo", "var_name default_value fmt_func")


def tr(message) -> str:
    return QCoreApplication.translate("report_expression", message)


def format_json(obj) -> str:
    """
    Formats a dictionary for presentation as a JSON object in a
    report variable.
    """
    return json.dumps(obj, indent=4, sort_keys=True)


def format_creation_date(creation_date) -> str:
    """
    Format job start date for presentation in a date report variable.
    """
    return str(utc_to_local(creation_date).strftime("%Y-%m-%d %H:%M"))


def _job_attr_var_mapping() -> typing.List[JobAttrVarInfo]:
    # Job attribute and corresponding variable names.
    return [
        JobAttrVarInfo("id", "te_job_id", "", str),
        JobAttrVarInfo("params", "te_job_input_params", {}, format_json),
        JobAttrVarInfo("results.uri.uri", "te_job_paths", "", str),
        JobAttrVarInfo("script.name", "te_job_alg_name", "", None),
        JobAttrVarInfo("start_date", "te_job_creation_date", "", format_creation_date),
        JobAttrVarInfo("status.value", "te_job_status", "", None),
        JobAttrVarInfo("task_name", "te_job_name", "", None),
        JobAttrVarInfo("task_notes", "te_job_comments", "", None),
    ]


def _report_settings_var_mapping() -> typing.List[ReportSettingVarInfo]:
    # Report setting variables with the corresponding values.
    org_logo_path = settings_manager.get_value(Setting.REPORT_ORG_LOGO_PATH)
    org_name = settings_manager.get_value(Setting.REPORT_ORG_NAME)
    footer = settings_manager.get_value(Setting.REPORT_FOOTER)
    disclaimer = settings_manager.get_value(Setting.REPORT_DISCLAIMER)

    return [
        ReportSettingVarInfo(
            Setting.REPORT_ORG_LOGO_PATH, "te_report_organization_logo", org_logo_path
        ),
        ReportSettingVarInfo(
            Setting.REPORT_ORG_NAME, "te_report_organization_name", org_name
        ),
        ReportSettingVarInfo(Setting.REPORT_FOOTER, "te_report_footer", footer),
        ReportSettingVarInfo(
            Setting.REPORT_DISCLAIMER, "te_report_disclaimer", disclaimer
        ),
    ]


def _current_job_layer_var_mapping() -> typing.List[LayerVarInfo]:
    # Current job map layer variables whose values will be set at runtime.
    return [LayerVarInfo("te_current_layer_name", "", lambda layer: layer.name())]


def _get_var_names(var_info_collection: typing.List) -> typing.List[str]:
    # Returns the variable names from the info objects
    return [vi.var_name for vi in var_info_collection]


class ReportExpressionUtils:
    """
    Helper functions for expressions and variables used in a report.
    """

    VAR_NAMES_PROP = "variableNames"
    VAR_VALUES_PROP = "variableValues"

    @staticmethod
    def register_variables(layout: QgsLayout) -> None:
        """
        Registers both job-related and report setting variables in the layout
        scope.
        """
        ReportExpressionUtils.register_job_variables(layout)
        ReportExpressionUtils.register_report_settings_variables(layout)
        ReportExpressionUtils.register_current_job_layer_settings_variables(layout)

        # Need to re-evaluate expressions for layout picture items.
        layout.refresh()

    @staticmethod
    def register_job_variables(layout: QgsLayout):
        # Registers job-related variable names since the values will be set
        # at runtime.
        ReportExpressionUtils._register_variable_collection(
            layout, _job_attr_var_mapping()
        )

    @staticmethod
    def register_report_settings_variables(layout: QgsLayout):
        # Register/update report variables based on the values in the settings.
        ReportExpressionUtils._register_variable_collection(
            layout, _report_settings_var_mapping()
        )

    @staticmethod
    def register_current_job_layer_settings_variables(layout: QgsLayout):
        # Register variables for the current job layer when the report is
        # running.
        ReportExpressionUtils._register_variable_collection(
            layout, _current_job_layer_var_mapping()
        )

    @staticmethod
    def _register_variable_collection(
        layout: QgsLayout, var_info_collection: typing.List
    ):
        var_names = _get_var_names(var_info_collection)

        # Remove duplicate names and corresponding values
        var_names, var_values = ReportExpressionUtils.remove_variables(
            layout, var_names
        )

        # Now append our variable names with corresponding values
        for var_info in var_info_collection:
            var_names.append(var_info.var_name)
            var_values.append(var_info.default_value)

        layout.setCustomProperty(ReportExpressionUtils.VAR_NAMES_PROP, var_names)
        layout.setCustomProperty(ReportExpressionUtils.VAR_VALUES_PROP, var_values)

    @staticmethod
    def remove_variables(
        layout: QgsLayout, rem_var_names: typing.List[str]
    ) -> typing.Tuple[typing.List, typing.List]:
        """
        Removes variables from the layout before adding new ones to ensure
        there are no duplicates.
        """
        var_names = layout.customProperty(ReportExpressionUtils.VAR_NAMES_PROP, [])
        var_values = layout.customProperty(ReportExpressionUtils.VAR_VALUES_PROP, [])
        for rvn in rem_var_names:
            ReportExpressionUtils.remove_variable_name_value(rvn, var_names, var_values)

        return var_names, var_values

    @staticmethod
    def remove_variable_name_value(
        rem_var_name: str, var_names: typing.List[str], var_values: typing.List[str]
    ):
        # Remove the variable name and corresponding value from the collection.
        while rem_var_name in var_names:
            idx = var_names.index(rem_var_name)
            _ = var_names.pop(idx)
            _ = var_values.pop(idx)

    @staticmethod
    def update_job_layer_expression_context(
        ctx: QgsExpressionContext, layer: QgsRasterLayer
    ) -> QgsExpressionContext:
        # Update expression context with values from current job layer.
        for layer_info in _current_job_layer_var_mapping():
            if not ctx.hasVariable(layer_info.var_name):
                continue
            active_scope = ctx.activeScopeForVariable(layer_info.var_name)

            try:
                fmt_func = layer_info.fmt_func
                if fmt_func is None:
                    continue
                layer_var_value = fmt_func(layer)
                active_scope.setVariable(layer_info.var_name, layer_var_value)
            except AttributeError:
                continue

        return ctx

    @staticmethod
    def update_job_expression_context(
        ctx: QgsExpressionContext, job: Job
    ) -> QgsExpressionContext:
        # Update expression context with job attribute values.
        for jv_info in _job_attr_var_mapping():
            if not ctx.hasVariable(jv_info.var_name):
                continue
            active_scope = ctx.activeScopeForVariable(jv_info.var_name)

            # Ensure the given job attribute exists
            try:
                getattr_func = attrgetter(jv_info.job_attr)
                job_attr_value = getattr_func(job)

                # Format value if required especially those ones that the
                # QgsExpression cannot convert or does not provide a function
                # for representing the value.
                fmt_func = jv_info.fmt_func
                if fmt_func is not None:
                    job_attr_value = fmt_func(job_attr_value)

                active_scope.setVariable(jv_info.var_name, job_attr_value)
            except AttributeError:
                continue

        return ctx

    @staticmethod
    def update_expression_context(
        ctx: QgsExpressionContext, job: Job, layer: QgsRasterLayer
    ):
        # Update expression contexts for current job and job layer in the
        # report generation cycle.
        ctx = ReportExpressionUtils.update_job_expression_context(ctx, job)
        ctx = ReportExpressionUtils.update_job_layer_expression_context(ctx, layer)

        return ctx
