# The Craftr build system
# Copyright (C) 2016  Niklas Rosenstein
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
:mod:`craftr.lang.cython`
=========================

This package provides target generators to compile Cython projects.
"""

from nr.types.recordclass import recordclass

import functools
import os
import re

python = load('craftr.lang.python')
cxx = load('craftr.lang.cxx')


class CythonCompiler(object):

  Project = recordclass.new('Project', 'sources main libs alias')

  def __init__(self, program=None):
    if not program:
      program = options.bin or os.getenv('CYTHON', 'cython')
    self.program = program

  @property
  @functools.lru_cache()
  def version(self):
    output = shell.pipe([self.program, '-V']).output
    match = re.match(r'cython\s+version\s+([\d\.]+)', output, re.I)
    if not match:
      raise ValueError("unable to determine Cython version")
    return match.group(1)

  def compile(self, sources, py_version=None, outputs=None, outdir='cython/src',
              cpp=False, embed=False, fast_fail=False, include=(),
              additional_flags=(), name=None):
    """
    Compile the Python/Cython source files to C or C++ sources.

    :param sources: A list of Python/Cython source files.
    :param py_version: The Python version to produce the C/C++ files for.
      Must be ``2`` or ``3``.
    :param outputs: Allows you to override the output C/C++ source output
      for every file in *sources*.
    :param outdir: Used if *outputs* is not explicitly specified.
    :param cpp: True to translate to C++ source files.
    :param embed: Pass ``--embed`` to Cython. Note that if multiple
      files are specfied in *sources*, all of them will have a ``int main()``
      function.
    :param fast_fail: True to enable the ``--fast-fail`` flag.
    :param include: Additional include directories for Cython
    :param additional_flags: Additional flags for Cython.
    :param name: The name of the target.

    Build metadata:

    :param cython_outdir: The output directory of the generate source files.
    """

    assert not isinstance(include, str)
    builder = TargetBuilder(gtn(name, "cython"), inputs=sources)

    outdir = buildlocal(outdir)
    if py_version is None:
      py_version = int(python.get_config()['VERSION'][0])
    if outputs is None:
      outputs = relocate_files(builder.inputs, outdir, '.cpp' if cpp else '.c')
    if py_version not in (2, 3):
      raise ValueError('invalid py_version: {0!r}'.format(py_version))

    command = [self.program, '$in', '-o', '$out', '-' + str(py_version)]
    command += ['-I' + x for x in include]
    command += ['--fast-fail'] if fast_fail else []
    command += ['--cplus'] if cpp else []
    command += ['--embed'] if embed else []
    command += additional_flags

    return builder.build([command], None, outputs, foreach=True,
      metadata={'cython_outdir': outdir})

  def project(self, main=None, sources=[], python_bin='python', defines=(),
      name=None, toolkit=None, in_working_tree=False, gen_output=None,
      compile_kwargs=None, link_kwargs=None, link_main_kwargs=None, **kwargs):
    """
    Compile a set of Cython source files into dynamic libraries for the
    Python version specified with "python_bin".

    :param main: Optional filename of a ``.pyx`` file that will
      be compiled with the ``--embed`` option and compiled to an
      executable file.
    :param sources: A list of the `.pyx` source files.
    :param python_bin: The name of the Python executable to compile for.
    :param defines: Additional defines for the compiler invokation.
    :param name: Target name.
    :param toolkit: The C/C++ compiler toolkit.
    :param in_working_tree: True if the build output should be inside
      the working directory, False otherwise.
    :param gen_output: Callable that takes a source input file and
      generates the output file. If this is specified, #in_working_tree
      is ignored. The correct suffix is still added to the returned file,
      thus this function should return a filename without suffix.
    :return: A :class:`ProjectResult` object
    """

    cpp = kwargs.get('cpp', False)
    name = gtn(name, 'cython_project')
    pyconf = python.get_config(python_bin)
    pyfw = python.get_framework(python_bin)
    py_version = int(pyconf['VERSION'][0])
    toolkit = toolkit or cxx.cxc

    compile_kwargs = compile_kwargs or {}
    compile_kwargs['defines'] = list(compile_kwargs.get('defines', [])) + list(defines)
    link_kwargs = link_kwargs or {}
    link_main_kwargs = link_main_kwargs or {}

    def getout(filename):
      if gen_output:
        return gen_output(filename)
      if in_working_tree:
        return path.rmvsuffix(filename)
      else:
        return path.join('cython', 'bin', path.rel(path.rmvsuffix(filename), session.module.project_dir))

    if sources:
      sources_target = self.compile(
        sources = sources,
        py_version = py_version,
        name = name + '_source',
        **kwargs
      )
    else:
      sources_target = None

    if main:
      main_source = self.compile(
        sources = [main],
        py_version = py_version,
        embed = True,
        name = name + '_main_source',
        **kwargs
      )
      main_bin = toolkit.link(
        output = getout(main),
        output_type = 'bin',
        inputs = toolkit.compile(
          language = 'c++' if cpp else 'c',
          sources = main_source.outputs,
          frameworks = [pyfw],
          pic = False,
          name = name + '_main_compile',
          **compile_kwargs
        ),
        name = name + '_main_bin',
        **link_main_kwargs
      )
    else:
      main_source = None
      main_bin = None

    # Separately compile all source files and link them to C-extensions.
    libs = []
    if sources_target:
      for pyxfile, cfile in zip(sources_target.inputs, sources_target.outputs):
        filename = path.rmvsuffix(path.basename(pyxfile))
        libs.append(toolkit.link(
          output = path.setsuffix(getout(pyxfile), pyconf['SO']),
          output_type = 'dll',
          suffix = None, # don't let link() replace the suffix
          inputs = toolkit.compile(
            language = 'c++' if cpp else 'c',
            sources = [cfile],
            frameworks = [pyfw],
            pic = True,
            name = name + '_compile_' + filename,
            **compile_kwargs
          ),
          name = name + '_lib_' + filename,
          **link_kwargs
        ))

        if main_bin:
          main_bin.implicit_deps+= libs[-1].outputs

    # TODO: Generate an alias target.
    return self.Project(sources, main_bin, libs, None)


cythonc = CythonCompiler()

def compile(*args, name=None, **kwargs):
  return cythonc.compile(*args, name=gtn(name, None), **kwargs)

def project(*args, name=None, **kwargs):
  return cythonc.project(*args, name=gtn(name, None), **kwargs)

def cython_compile(*args, name=None, **kwargs):
  logger.warn('cython_compile() is deprecated, use cython.compile() instead')
  return cythonc.compile(*args, name=gtn(name, None), **kwargs)

def cython_project(*args, name=None, **kwargs):
  logger.warn('cython_project() is deprecated, use cython.project() instead')
  return cythonc.project(*args, name=gtn(name, None), **kwargs)

__all__ = ['cython_compile', 'cython_project']
