# -*- coding: utf-8 -*-
"Quadrature representation class for UFL"

# Copyright (C) 2009-2017 Kristian B. Oelgaard
#
# This file is part of FFC.
#
# FFC is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# FFC 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with FFC. If not, see <http://www.gnu.org/licenses/>.
#
# Modified by Anders Logg 2009, 2014
# Modified by Martin Sandve Alnæs 2013-2017

# Python modules
import collections

# UFL modules
from ufl_legacy.classes import Integral
from ufl_legacy.sorting import sorted_expr_sum
from ufl_legacy import custom_integral_types

# FFC modules
from ffc.log import ffc_assert, info, error
from ffc.utils import product
from ffc.fiatinterface import create_element

from ffc.quadrature.cpp import set_float_formatting

from ffc.representationutils import initialize_integral_ir
from ffc.quadrature.tabulate_basis import tabulate_basis
from ffc.quadrature.parameters import parse_optimise_parameters
from ffc.quadrature.deprecation import issue_deprecation_warning

from ffc.quadrature.quadraturetransformer import QuadratureTransformer
from ffc.quadrature.optimisedquadraturetransformer import QuadratureTransformerOpt


def compute_integral_ir(itg_data,
                        form_data,
                        form_id,
                        element_numbers,
                        classnames,
                        parameters):
    "Compute intermediate represention of integral."

    info("Computing quadrature representation")

    issue_deprecation_warning()

    set_float_formatting(parameters["precision"])

    # Initialise representation
    ir = initialize_integral_ir("quadrature", itg_data, form_data, form_id)

    # Create and save the optisation parameters.
    ir["optimise_parameters"] = parse_optimise_parameters(parameters, itg_data)

    # Sort integrals into a dict with quadrature degree and rule as
    # key
    sorted_integrals = sort_integrals(itg_data.integrals,
                                      itg_data.metadata["quadrature_degree"],
                                      itg_data.metadata["quadrature_rule"])

    # Tabulate quadrature points and basis function values in these
    # points
    integrals_dict, psi_tables, quadrature_rules = \
        tabulate_basis(sorted_integrals, form_data, itg_data)

    # Save tables for quadrature weights and points
    ir["quadrature_rules"] = quadrature_rules

    # Create dimensions of primary indices, needed to reset the
    # argument 'A' given to tabulate_tensor() by the assembler.
    ir["prim_idims"] = [create_element(ufl_element).space_dimension()
                        for ufl_element in form_data.argument_elements]

    # Select transformer
    if ir["optimise_parameters"]["optimisation"]:
        QuadratureTransformerClass = QuadratureTransformerOpt
    else:
        QuadratureTransformerClass = QuadratureTransformer

    # Create transformer
    transformer = QuadratureTransformerClass(psi_tables,
                                             quadrature_rules,
                                             itg_data.domain.geometric_dimension(),
                                             itg_data.domain.topological_dimension(),
                                             ir["entitytype"],
                                             form_data.function_replace_map,
                                             ir["optimise_parameters"])

    # Transform integrals.
    cell = itg_data.domain.ufl_cell()
    ir["trans_integrals"] = _transform_integrals_by_type(ir, transformer,
                                                         integrals_dict,
                                                         itg_data.integral_type,
                                                         cell)

    # Save tables populated by transformer
    ir["name_map"] = transformer.name_map
    ir["unique_tables"] = transformer.unique_tables  # Basis values?

    # Save tables map, to extract table names for optimisation option
    # -O.
    ir["psi_tables_map"] = transformer.psi_tables_map
    ir["additional_includes_set"] = transformer.additional_includes_set

    # Insert empty data which will be populated if optimization is
    # turned on
    ir["geo_consts"] = {}

    # Extract element data for psi_tables, needed for runtime
    # quadrature.  This is used by integral type custom_integral.
    ir["element_data"] = _extract_element_data(transformer.element_map, classnames)

    return ir


def sort_integrals(integrals, default_scheme, default_degree):
    """Sort and accumulate integrals according to the number of quadrature
    points needed per axis.

    All integrals should be over the same (sub)domain.
    """

    if not integrals:
        return {}

    # Get domain properties from first integral, assuming all are the
    # same
    integral_type = integrals[0].integral_type()
    subdomain_id = integrals[0].subdomain_id()
    domain = integrals[0].ufl_domain()
    ffc_assert(all(integral_type == itg.integral_type() for itg in integrals),
               "Expecting only integrals of the same type.")
    ffc_assert(all(domain == itg.ufl_domain() for itg in integrals),
               "Expecting only integrals on the same domain.")
    ffc_assert(all(subdomain_id == itg.subdomain_id() for itg in integrals),
               "Expecting only integrals on the same subdomain.")

    sorted_integrands = collections.defaultdict(list)
    for integral in integrals:
        # Override default degree and rule if specified in integral
        # metadata
        integral_metadata = integral.metadata() or {}
        degree = integral_metadata.get("quadrature_degree", default_degree)
        scheme = integral_metadata.get("quadrature_rule", default_scheme)
        assert isinstance(degree, int)
        # Add integrand to dictionary according to degree and scheme.
        rule = (scheme, degree)
        sorted_integrands[rule].append(integral.integrand())

    # Create integrals from accumulated integrands.
    sorted_integrals = {}
    for rule, integrands in list(sorted_integrands.items()):
        # Summing integrands in a canonical ordering defined by UFL
        integrand = sorted_expr_sum(integrands)
        sorted_integrals[rule] = Integral(integrand, integral_type, domain,
                                          subdomain_id, {}, None)
    return sorted_integrals


def _transform_integrals_by_type(ir, transformer, integrals_dict,
                                 integral_type, cell):
    num_facets = cell.num_facets()
    num_vertices = cell.num_vertices()

    if integral_type == "cell":
        # Compute transformed integrals.
        info("Transforming cell integral")
        transformer.update_cell()
        terms = _transform_integrals(transformer, integrals_dict, integral_type)

    elif integral_type == "exterior_facet":
        # Compute transformed integrals.
        terms = [None]*num_facets
        for i in range(num_facets):
            info("Transforming exterior facet integral %d" % i)
            transformer.update_facets(i, None)
            terms[i] = _transform_integrals(transformer, integrals_dict,
                                            integral_type)

    elif integral_type == "interior_facet":
        # Compute transformed integrals.
        terms = [[None]*num_facets for i in range(num_facets)]
        for i in range(num_facets):
            for j in range(num_facets):
                info("Transforming interior facet integral (%d, %d)" % (i, j))
                transformer.update_facets(i, j)
                terms[i][j] = _transform_integrals(transformer, integrals_dict,
                                                   integral_type)

    elif integral_type == "vertex":
        # Compute transformed integrals.
        terms = [None]*num_vertices
        for i in range(num_vertices):
            info("Transforming vertex integral (%d)" % i)
            transformer.update_vertex(i)
            terms[i] = _transform_integrals(transformer, integrals_dict,
                                            integral_type)

    elif integral_type in custom_integral_types:

        # Compute transformed integrals: same as for cell integrals
        info("Transforming custom integral")
        transformer.update_cell()
        terms = _transform_integrals(transformer, integrals_dict, integral_type)

    else:
        error("Unhandled domain type: " + str(integral_type))
    return terms


def _transform_integrals(transformer, integrals, integral_type):
    "Transform integrals from UFL expression to quadrature representation."
    transformed_integrals = []
    for point, integral in sorted(integrals.items()):
        transformer.update_points(point)
        terms = transformer.generate_terms(integral.integrand(), integral_type)
        transformed_integrals.append((point, terms, transformer.function_data,
                                      {}, transformer.coordinate,
                                      transformer.conditionals))
    return transformed_integrals


def _extract_element_data(element_map, classnames):
    "Extract element data for psi_tables"

    # Iterate over map
    element_data = {}
    for elements in element_map.values():
        for ufl_element, counter in elements.items():

            # Create corresponding FIAT element
            fiat_element = create_element(ufl_element)

            # Compute value size
            value_size = product(ufl_element.value_shape())

            # Get element classname
            element_classname = classnames["finite_element"][ufl_element]

            # Store data
            element_data[counter] = {"physical_value_size": value_size,
                                     "num_element_dofs": fiat_element.space_dimension(),
                                     "classname": element_classname}

    return element_data
