#! /usr/bin/env python

import argparse
import json
import re
import sys
import textwrap

class Indent(str):
    '''
    An indenting string, to be used with str.format.
    See http://stackoverflow.com/questions/37501228.
    '''

    indent_format_spec = re.compile(r'i([0-9]+)')

    def __format__(self, format_spec):
        matches = self.indent_format_spec.search(format_spec)
        if matches:
            level = int(matches.group(1))
            first, sep, text = textwrap.dedent(self).strip().partition('\n')
            return first + sep + textwrap.indent(text, ' ' * level)
        else:
            return super(Indent, self).__format__(format_spec)


def vcsn_prefix(s):
    '''Convert prefix types that belong to `vcsn::`.'''
    return re.sub('(letter_class_t)', r'vcsn::\1', s)



def formal_to_arg(formal):
    '''`const std::string& algo = "default"` => `algo`, but
    `const automaton& aut = "default"` => `aut.val_`'''
    res = formal['arg']
    if formal['class'] in dyn_types:
        res += '.val_'
    return res

def formals_to_args(formals):
    return [formal_to_arg(f) for f in formals]
# A dictionary of all the classes and function,
# with the class name as key.
#
# Each class is a list of dictionaries that define function,
# providing the following keys:
#
# const: ` const` if the first argument was const, empty otherwise.
# static: `static ` if the function should be static, empty otherwise.
# doc: the documentation string.
# args: the effective arguments (e.g., "aut, i").
# formals: the formals arguments (e.g., "const automaton& aut, unsigned i").
# formals_impl: the formals arguments without the default values.
# fun: function name (e.g., multiply, is_equivalent).
# dynfun: function name in vcsn::dyn:: (e.g., multiply, are_equivalent).
# result: return type.'''
classes = {}

# For each class, a piece of code to include in the class
# definition.
classes_prologue_header = {
'automaton':
    r'''
  /// Direction for proper.
  using direction = ::vcsn::dyn::direction;

  /// Create an automaton from a file, or from a string.
  automaton(const std::string& data = "",
            const std::string& format = "default",
            const std::string& filename = "",
            bool strip = true);
''',

    'context':
    r'''
  context();

  context(const std::string& ctx);

  explicit operator bool() const;

  automaton cotrie(const std::string& data,
                   const std::string& format,
                   const std::string& filename) const;

  automaton trie(const std::string& data,
                 const std::string& format,
                 const std::string& filename) const;
''',

    'expression':
    r'''
  expression(const struct context& ctx, const std::string& r,
             identities ids = {});

  /// Convert \a this to \a ctx, using \a ids.
  expression as(const struct context& ctx = {},
                identities ids = "default") const;
''',

    'label':
    r'''
  label(const context& ctx, const std::string& s);
''',

    'polynomial':
    r'''
  polynomial(const struct context& ctx, const std::string& s);
''',

    'weight':
    r'''
  weight(const context& ctx, const std::string& s);
''',
}


# For each class, a piece of code to include in the definition
# of the class' functions.
classes_prologue_impl = {
    'automaton':
    r'''
automaton::automaton(const std::string& data, const std::string& format,
                     const std::string& filename, bool strip)
  : val_{nullptr}
{
  // It is possible to have an empty data to mean an empty automaton.
  // Check the filename instead.
  if (filename.empty())
    val_ = vcsn::dyn::make_automaton(data, format, strip);
  else
    {
      auto is = vcsn::open_input_file(filename);
      try
        {
          val_ = vcsn::dyn::read_automaton(*is, format, strip);
          vcsn::require(is->peek() == EOF, "unexpected trailing characters: ", *is);
        }
      catch (const std::runtime_error& e)
        {
          vcsn::raise(e, "  while reading automaton: ", filename);
        }
   }
}
''',

    'context':
    r'''
context::context()
{}

context::context(const std::string& s)
  : context(vcsn::dyn::make_context(s))
{}

context::operator bool() const
{
  return bool(val_);
}

automaton context::cotrie(const std::string& data,
                          const std::string& format,
                          const std::string& filename) const
{
  auto is = make_istream(data, filename);
  auto res = cotrie(*is, format);
  vcsn::require(is->peek() == EOF, "unexpected trailing characters: ", *is);
  return res;
}

automaton context::trie(const std::string& data,
                        const std::string& format,
                        const std::string& filename) const
{
  auto is = make_istream(data, filename);
  auto res = trie(*is, format);
  vcsn::require(is->peek() == EOF, "unexpected trailing characters: ", *is);
  return res;
}
''',

    'expression':
    r'''
expression::expression(const struct context& ctx, const std::string& e,
                       identities ids)
  : val_{make_expression(ctx.val_, e, ids)}
{}

expression expression::as(const struct context& ctx,
                          identities ids) const
{
  return copy((ctx ? ctx : context()), ids);
}
''',

    'label':
    r'''

label::label(const context& ctx, const std::string& s)
  : val_{make_label(ctx.val_, s)}
{}
''',

    'polynomial':
    r'''
polynomial::polynomial(const struct context& ctx, const std::string& s)
  : val_{make_polynomial(ctx.val_, s)}
{}
''',

    'weight':
    r'''
weight::weight(const context& ctx, const std::string& s)
  : val_{make_weight(ctx.val_, s)}
{}
''',
}

class_declare = '''struct {class};
'''

class_open = '''\
/// An object-oriented wrapper around a dyn::{class}.
struct {class}
{{
public:
  {class}(const vcsn::dyn::{class}& val)
    : val_{{val}}
  {{}}
'''

class_close = '''\
  /// The wrapped dyn::{class}.
  vcsn::dyn::{class} val_;
}};'''

memfn_declaration = '''\
  {doc:i2}
  {static}auto {fun}({formals}){const}
    -> {result};
'''

memfn_definition = '''\
auto {class}::{fun}({formals_impl}){const}
  -> {result}
{{
  return vcsn::dyn::{dynfun}({args});
}}
'''

def ignore(fun):
    "Report that we don't process this case."
    print('warning: ignoring: {fun}({formals}) -> {result}'
          .format_map(fun), file=sys.stderr)

def process_doc(doc, args):
    "Generate the documentation."
    params = [r'\param {} {}'.format(p['name'], '\n'.join(p['desc']))
            for p in doc['params'] if p['name'] in args]

    res = '\n'.join(doc['desc'] + params + doc['pre'] + doc['returns'])
    res = [('  /// ' + l).rstrip() for l in res.split('\n')]

    if res[-1] == '  ///':
        del res[-1]

    return '\n'.join(res)

def process_function(fun):
    fun['const'] = ''
    fun['static'] = ''
    fun['result'] = vcsn_prefix(fun['result'])
    for formal in fun['formals']:
        formal['type'] = vcsn_prefix(formal['type'])

    if fun['dynfun'] == 'context_of':
        fun['fun'] = 'context'
    else:
        fun['fun'] = re.sub('^are_', 'is_', fun['dynfun'])

    fs = fun['formals']

    # A dyn::word is really a dyn::label.
    fs = [{
            'class': formal['class'].replace('word', 'label'),
            'type': formal['type'].replace('word', 'label'),
            'default': formal['default'],
            'arg': formal['arg'],
            } for formal in fs]
    fun['result'] = fun['result'].replace('word', 'label')

    cls = fs[0]['class']
    args = formals_to_args(fs)

    if fun['fun'] == 'lweight':
        # In this case, the class is actually that of the second
        # argument: lweight(weight, automaton) ->
        # automaton.lweight(weight).
        cls = fs[1]['class']
        fun['const'] = ' const'
        fs = [fs[0]] + fs[2:]
        args[1] = 'val_'

    elif cls.isidentifier():
        # The first argument is ours, e.g., `automaton`.  In the
        # function body, use `val_` to denote the value stored by
        # `this`.  And keep the `const` as the function's constness.
        if 'const ' in fs[0]['type']:
            fun['const'] = ' const'
        fs = fs[1:]
        args[0] = 'val_'

    elif 'std::vector' in cls:
        # The first argument is not ours, e.g.,
        # `std::vector<automaton>`.  Make this function a static
        # function of class `automaton`.
        cls = re.sub(r'.*<(?P<class>\w+)>', r'\g<class>', cls)
        fun['static'] = 'static '
        args[0] = 'make_dyn_vector<{cls}>({arg})'.format(cls=cls,
                                                         arg=args[0])

    elif fun['result'].isidentifier() and not fun['result'] == 'void':
        # The first argument is std::ostream or std::istream.
        # Bind this to the return type.
        cls = fun['result']
    else:
        ignore(fun)
        return

    fun['class'] = cls

    fun['doc'] = process_doc(fun['doc'], args)
    fun['doc'] = Indent(fun['doc'])

    # Flatten the formal parameter list
    fs = ['{} {}{}'.format(
        f['type'],
        f['arg'],
        '' if f['default'] is None else ' = ' + f['default']
        ) for f in fs]

    # We both have both a function and a type named 'context'.  To
    # avoid clashes, we need to prefix the type by 'struct'.
    fun['result'] = re.sub(r'\b(context)\b', r'struct \1', fun['result'])
    fs = [re.sub(r'\b(context)\b', r'struct \1', f) for f in fs]
    fun['formals'] = ', '.join(fs)
    fun['formals_impl'] = ', '.join([re.sub(r'\s+=.*', '', f) for f in fs])
    fun['args'] = ', '.join(args)

    if cls not in classes:
        classes[cls] = []
    classes[cls].append(fun)

# The list of dyn types, loaded from JSON.
dyn_types = []

def process_json(fn):
    '''Store in `classes` the signatures from header `fn`.'''
    with open(fn, 'r') as f:
        json_ = json.load(f)
        dyn_types.extend(json_['dyn_types'])
        for fun in json_['algos']:
            process_function(fun)
    for cls in classes:
        classes[cls].sort(key=lambda f: f['fun'])


def output_memfn(format, cls):
    res = ''
    for f in cls:
        res += '\n'
        res += format.format_map(f)
    return res


def header():
    '''Generate the header of the C++ OO Bindings.'''
    head = r'''// Generated by odyn-gen, do not edit by hand.

#include <boost/optional.hpp>

#include <vcsn/algos/fwd.hh>
#include <vcsn/dyn/automaton.hh>
#include <vcsn/dyn/context.hh>
#include <vcsn/dyn/types.hh>
#include <vcsn/dyn/value.hh>
#include <vcsn/misc/export.hh>

namespace vcsn
{
  namespace odyn LIBVCSN_API
  {
    using identities = vcsn::dyn::identities;
    using location = vcsn::dyn::location;

'''
    # Forward declarations.
    body = ''
    for c, _ in sorted(classes.items()):
        body += class_declare.format_map({'class': c})
    # Class definitions.
    for c, cls in sorted(classes.items()):
        body += '\n\n'
        body += class_open.format_map({'class': c})
        if c in classes_prologue_header:
            body += classes_prologue_header[c].rstrip(' ')
        body += output_memfn(memfn_declaration, cls)
        body += '\n'
        body += class_close.format_map({'class': c})
    body = textwrap.indent(body, ' ' * 4)

    foot = '''
  }
}'''
    return head + body + foot


def output():
    '''Generate the implementation of the C++ OO Bindings.'''
    head = r'''// Generated by odyn-gen, do not edit by hand.
#include <vcsn/odyn/odyn.hh>

#include <vcsn/dyn/algos.hh>
#include <vcsn/misc/raise.hh>
#include <vcsn/misc/stream.hh>

namespace vcsn
{
  namespace odyn
  {
    /// Convert a vector of odyn values to a vector of dyn values.
    template <typename T>
    auto make_dyn_vector(const std::vector<T>& v)
    {
      auto res = std::vector<decltype(std::declval<T>().val_)>{};
      for (const auto& e: v)
        res.emplace_back(e.val_);
      return res;
    }

    /// Create an input stream from a file, or from a string.
    static
    auto make_istream(const std::string& data = "",
                      const std::string& filename = "")
      -> std::shared_ptr<std::istream>
    {
      vcsn::require(data.empty() || filename.empty(),
                    "cannot provide both data and filename");
      if (!filename.empty())
        return vcsn::open_input_file(filename);
      else
        return std::make_shared<std::istringstream>(data);
    }
'''
    body = ''
    for c, cls in sorted(classes.items()):
        if c in classes_prologue_impl:
            body += classes_prologue_impl[c].rstrip(' ')
        body += output_memfn(memfn_definition, cls)
    body = textwrap.indent(body, ' ' * 4)
    foot = '''  }
}'''
    return (head + body + foot).rstrip()



parser = argparse.ArgumentParser(description='Generate odyn.',
                                 formatter_class=argparse.RawDescriptionHelpFormatter,
                                 epilog = \
'''
Read `dyn/algos.json` and generate `odyn`, an object-oriented
version of `vcsn::dyn`.
''')

parser.add_argument('--header',
                    type=argparse.FileType('w'), default = '-',
                    help='header file to generate')
parser.add_argument('--output',
                    type=argparse.FileType('w'), default = '-',
                    help='implementation file to generate')
parser.add_argument('input', type=str, default=None,
                    help='''JSON file to process (`dyn/algos.json`).''')
args = parser.parse_args()

process_json(args.input)
print(header(), file=args.header)
print(output(), file=args.output)
