# -*- coding: utf-8 -*-
"This module provides functionality for compilation of strings as dolfin SubDomains."

# Copyright (C) 2008-2016 Martin Sandve Alnæs
#
# This file is part of DOLFIN.
#
# DOLFIN 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.
#
# DOLFIN 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 DOLFIN. If not, see <http://www.gnu.org/licenses/>.

from six import string_types
import re
import hashlib

# Import local compile_extension_module
from dolfin.compilemodules.compilemodule import (compile_extension_module,
                                                 expression_to_code_fragments,
                                                 math_header)

import dolfin.cpp as cpp

__all__ = ["CompiledSubDomain"]

_map_args = ["x", "y"]

_subdomain_template = """
class %(classname)s: public SubDomain
{
public:
%(members)s

  %(classname)s()
  {
%(constructor)s
  }

  /// Return true for points inside the sub domain
  bool inside(const Array<double>& x, bool on_boundary) const
  {
    %(inside)s
  }

};
"""

# TODO: Support implementation of map as well
"""
  /// Map coordinate x in domain H to coordinate y in domain G (used for periodic boundary conditions)
  void map(const Array<double>& x, Array<double>& y) const
  {
    %(map)s
  }
"""

def expression_to_subdomain(cpparg, classname):
    """
    Generate code for a :py:class:`SubDomain <dolfin.cpp.SubDomain>`
    subclass for a single expression.
    """

    # Assure we have a simple string expression
    assert isinstance(cpparg, string_types)

    # Extract code fragments from the expr and defaults
    fragments, members = expression_to_code_fragments(\
        [cpparg], ["x", "on_boundary", "DOLFIN_EPS"])

    # Generate code for inside()
    insidecode = "  return %s;" % cpparg

    # Generate code for map()
    #mapcode = "..."

    # Connect the code fragments using the function template code
    fragments["inside"]    = insidecode
    fragments["classname"] = classname
    #fragments["map"]       = mapcode
    code = _subdomain_template % fragments
    return code, members

def compile_subdomain_code(code, classname, mpi_comm=None):

    # Complete the code
    code = math_header + \
"""
namespace dolfin
{
%s
}
""" % code

    # Compile the extension module
    compiled_module = compile_extension_module(code, mpi_comm=mpi_comm)

    # Get compiled class
    return getattr(compiled_module, classname)

def CompiledSubDomain(cppcode, mpi_comm=None, **kwargs):
    """
    Compile a C++ string expression into a
    :py:class:`SubDomain <dolfin.cpp.SubDomain>` instance.

    *Arguments*
        cppcode
            a string containing an expression in C++ syntax.

    If the string contains a name, it is assumed to be a scalar
    variable name, and is added as a public member of the generated
    subdomain. All such members need a default initial value.

    If the string contains a class name it is interpreted as a
    complete implementations of subclasses of :py:class:`SubDomain
    <dolfin.cpp.SubDomain>`.

    *Examples of usage*

        .. code-block:: python

            left  = CompiledSubDomain("near(x[0], 0) && on_boundary")
            right = CompiledSubDomain("near(x[1], 1) && on_boundary")
            center = CompiledSubDomain("near(x[1], c)", c = 0.5)

    """

    if not isinstance(cppcode, string_types):
        raise TypeError("expected a 'str'")

    if isinstance(cppcode, string_types) and "class" in cppcode and \
           "SubDomain" in cppcode:
        classname = re.findall(r"class[ ]+([\w]+).*", cppcode)[0]
        code, members = cppcode, []

    else:
        classname = "CompiledSubDomain" + hashlib.sha1(cppcode.encode(\
            "utf-8")).hexdigest()
        code, members = expression_to_subdomain(cppcode, classname)

    SubDomainClass = compile_subdomain_code(code, classname, mpi_comm=mpi_comm)

    # Check passed default arguments
    not_allowed = [n for n in dir(cpp.SubDomain) if n[0] !="_"]
    not_allowed += ["cppcode"]

    if not all(member in kwargs for member in members):
        missing = []
        for member in members:
            if member not in kwargs:
                missing.append(member)
        missing = ", ".join("'%s'" % miss for miss in missing)
        cpp.dolfin_error("subdomains.py",
                         "compile C++ subdomain",
                         "Expected a default value to all member "
                         "variables in the SubDomain. Missing: %s." % missing)

    for name in list(kwargs.keys()):
        if name in not_allowed:
            cpp.dolfin_error("subdomains.py",
                             "compile C++ subdomain",
                             "Parameter name: '%s' is not allowed. It is "
                             "part of the interface of SubDomain" % name)

        if not (all(isinstance(value, (int, float)) \
                    for value in list(kwargs.values()))):
            raise TypeError("expected default arguments for member variables "\
                            "to be scalars.")

    # Store compile arguments for possible later use
    SubDomainClass.cppcode = cppcode

    # Instantiate CompiledSubDomain
    subdomain = SubDomainClass()

    # Set default variables
    for member, value in list(kwargs.items()):
        setattr(subdomain, member, value)

    return subdomain
