#! /usr/bin/env python
# -*- coding: utf-8 -*-

try:
    import builtins
except:
    import __builtin__ as builtins

import argparse
import re
import sys
import timeit
import warnings

import vcsn

if '-DNDEBUG' not in ' '.join([vcsn.config['cppflags'],
                               vcsn.config['cxxflags']]):
    warnings.warn('not compiled with -DNDEBUG, benches are irrelevant')
if '-O3' not in vcsn.config['cxxflags']:
    warnings.warn('not compiled with -O3, benches are irrelevant')

parser = argparse.ArgumentParser(description = 'Bench some algorithms.')
parser.add_argument('--only', metavar = 'RE',
                    type = re.compile, default = '.*',
                    help = 'run only benches whose title is matched by RE')
parser.add_argument('-v', '--verbose', action='store_true', help='Be verbose')

args = parser.parse_args()

last_setup = ['pass']

def ctx_signature(ctx):
    'A short, nice looking, description of a context.'
    ctx = re.sub(r'lal_char\((.*?)\)',
                 r'[\1]',
                 ctx)
    ctx = re.sub(r'lan_char\((.*?)\)',
                 r'[\1]?',
                 ctx)
    ctx = re.sub(r'law_char\((.*?)\)',
                 r'[\1]*',
                 ctx)
    ctx = re.sub(r'lat<(.*?)>',
                 lambda m: 'x'.join([x.strip() for x in m.group(1).split(',')]),
                 ctx)
    ctx = re.sub(r'^(.*), *(.*)$',
                 lambda m: '{} -> {}'.format(m.group(1), m.group(2).upper()),
                 ctx)
    return ctx

def bench(stmt, comment, title = None, setup = ['pass'], number = 1):
    "Report the best timing of three batches of number runs of cmd."
    if title is None:
        title = stmt

    if args.only.search(title + " # " + comment):

        if 1 < number:
            comment += ', {}x'.format(number)

        # The default context, which we use extensively.
        b = vcsn.context('lal_char(abc), b')

        try:
            # Run the set up if it is not the last one we ran.
            if not isinstance(setup, list):
                setup = [setup]
            global last_setup
            if setup != last_setup:
                for s in setup:
                    if args.verbose:
                        print("run:", s)
                    exec(s)
                last_setup = setup

            # Inject the result into `builtins`, so that the stmt run by
            # timeit finds them.  See
            # http://stackoverflow.com/a/5390326/4065671.
            builtins.__dict__.update(locals())
            builtins.__dict__.update({'vcsn': vcsn})
            if args.verbose:
                print("run:", stmt)
            t = round(min(timeit.repeat(stmt, number = number)), 2)
            t = "{:5.2f}s".format(t)
            err = None
        except RuntimeError as exc:
            t = " FAIL"
            err = exc
        print("{:7s}: {:20s} # {}".format(t, title, comment))
        if err:
            print(err, file=sys.stderr)
        sys.stdout.flush()

# Check the cost of dyn calls.
#
# We used to check a call to "b.format('text')", but that makes us too
# sensitive to the cost of the formatting itself.  Any operation can
# hardly be simpler than `automaton.is_proper`, which just returns
# "true" for an automaton on a free labelset.  So this does measure
# the pure speed of our interface with Python, and dispatch.
#
# To check the cost of the dispatch, it must be done in C++.
#
# FWIW:
#
# %timeit a.is_empty()
# 1000000 loops, best of 3: 1.03 µs per loop
#
# %timeit a.proper()
# 10000 loops, best of 3: 62.8 µs per loop
#
# %timeit a.is_proper()
# 1000000 loops, best of 3: 1 µs per loop
#
# So really, is_proper looks the right tool.
bench('a.is_proper()',
      'a = ""',
      setup = '''a = vcsn.automaton('', 'daut')''',
      number = 1000000)

# Check formatting.  was used to check dyn:: round-trip.
bench('b.format("text")',
      'b = [abc] -> B',
      number = 100000)

# I/O on expressions.
# e{N} is amazingly costly, which is bad, since we often use it to
# build large automata for benches.
e = '[ab]{20000}'
bench('b.expression(e)',
      'e = {}'.format(e),
       setup = 'e = "{}"'.format(e),
       number = 1000)

bench('b.expression(e)',
      'e = "(\e+a)" * 500',
      setup = '''e = '(\e+a)' * 500''',
      number = 100)
bench('r.format("text")',
      'r = b.expression("(\e+a)" * 500)',
      setup = ['''e = '(\e+a)' * 500''',
               '''r = b.expression(e) '''],
      number = 1000)

# Output should be fast.
r = 'a?{500}'
for fmt in ['dot', 'efsm', 'fado', 'grail', 'tikz']:
    bench('a.format("{}")'.format(fmt),
          'a = std({})'.format(r),
          setup = ['r = "{}"'.format(r),
                   'a = b.expression(r).standard()'],
          number = 5)

# Input should be too.
r = 'a?{500}'
for fmt in ['dot', 'efsm', 'fado']:
    bench('vcsn.automaton(a, "{}")'.format(fmt),
          's = {}(std({}))'.format(fmt, r),
          'read(s)',
          setup = ['r = "{}"'.format(r),
                   'a = b.expression(r).standard().format("{}")'.format(fmt)])

## -------------- ##
## derived_term.  ##
## -------------- ##
def bench_derived_term(alphabet, exp, algo, number = 10):
    bench('r.derived_term("{}")'.format(algo),
          'r = {}, c = [{}] -> Z'.format(exp, alphabet),
          setup = ['e = "{}"'.format(exp),
                   'c = vcsn.context("lal_char({}), z")'.format(alphabet),
                   'r = c.expression(e)'],
          number = number)

def bench_dt(alphabet, size, algo, number = 10):
    bench_derived_term(alphabet,
                       "(a+b)*b(<2>a+<2>b){{{}}}".format(size),
                       algo,
                       number = number)

bench_dt('ab',  150, 'derivation')
bench_dt('a-z', 150, 'derivation')
bench_dt('a-z', 150, 'expansion')
bench_dt('ab',  300, 'derivation')
bench_dt('a-z', 300, 'derivation')
bench_dt('a-z', 300, 'expansion')

bench_derived_term('a', 'a?{150}', 'derivation', number = 1)
bench_derived_term('a', 'a?{150}', 'expansion', number = 1)

# standard
e = "(a+b)*b(<2>a+<2>b){20000}"
bench('r.standard()',
      'r = {}, c = [a-z] -> Z'.format(e),
      setup = ['e = "{}"'.format(e),
               '''r = vcsn.context('lal_char(a-z), z').expression(e)'''],
      number = 10)

# thompson
bench('r.thompson()',
      'r = {}, c = [a-z]? -> Z'.format(e),
      setup = ['e = "{}"'.format(e),
               '''r = vcsn.context('lan_char(a-z), z').expression(e)'''],
      number = 10)


## ------------- ##
## Determinize.  ##
## ------------- ##

# These are the well known worst cases.  21 is too long, slowly moving
# to use 18 as reference.
for n in [18, 21]:
    bench('a.determinize()',
          'a = ladybird({})'.format(n),
          setup = ['n = {}'.format(n),
                   '''a = vcsn.context('lal_char(abc), b').ladybird(n)'''])

# In the case of 18, check that we scale well with the size of the
# (context's) alphabet.
n = 18
ctx = 'lal_char(a-zA-Z0-9), b'
bench('a.determinize()',
      'a = ladybird({}), c = {}'.format(n, ctx_signature(ctx)),
      setup = ['n = {}'.format(n),
               'c = vcsn.context("{}")'.format(ctx),
               'a = c.ladybird(n)'])

# See how boolean vs. weighted determinization goes.
for n, number in [(13, 10), (14, 10), (16, 2)]:
    for algo in ['', '"weighted"']:
        for ws in ['b', 'f2']:
            ctx = 'lal_char(abc), ' + ws
            bench('a.determinize({})'.format(algo),
                  'a = de_bruijn({}), c = {}'.format(n, ctx_signature(ctx)),
                  setup = ['c = vcsn.context("{}")'.format(ctx),
                           'n = {}'.format(n),
                           'a = c.de_bruijn(n)'],
                  number = number)

# Something more realistic: the previous automata explode in an
# exponential number of states, and half of them end up being final.
# This exagerates the importance of the handling of the final states.
#
# The following bench tries to be more realistic (i.e., more NLP
# like): the automata are almost deterministic (and easy to
# determinize), are "wide", and have few final states.
n = 100
ctx = 'lal_char(a-zA-Z0-9), b'
r = '([^]+a){{{}}}'.format(n)
for algo in ['', '"weighted"']:
    bench('a.determinize({})'.format(algo),
          'a = std({}), c = {}'.format(r, ctx_signature(ctx)),
          setup= ['r = "{}"'.format(r),
                  'c = vcsn.context("{}")'.format(ctx),
                  'a = c.expression(r).standard()'],
          number = 2)


## ------ ##
## eval.  ##
## ------ ##

n = 150
# Many many states active concurrently.
# too slow: a = b.expression('(a+b)*a(a+b){{{n}}}).derived_term()
bench('a.eval("a"*{})'.format(n+1),
      'a = de_bruijn({})'.format(n),
      setup = ['n = {}'.format(n),
               'a = b.de_bruijn(n)'],
      number = 1000)
# Something less wide.
ctx = 'lal_char(a-z), z'
n = 25
r = '[a-z]*'
bench('a.eval("abcxyz"*{})'.format(n),
      'a = std({}), c = {}'.format(r, ctx_signature(ctx)),
      setup = ['c = vcsn.context("{}")'.format(ctx),
               'r = c.expression("{}")'.format(r),
               'a = r.standard()'],
      number = 1000)



## ---------- ##
## shortest.  ##
## ---------- ##

n = 9
# too slow: a = b.expression('(a+b)*a(a+b){{{n}}}).derived_term()
bench('a.shortest(5)',
      'a = de_bruijn({})'.format(n),
      setup = ['n = {}'.format(n),
               'a = b.de_bruijn(n)'],
      number = 10)

bench('a.shortest(5000)',
      'a = de_bruijn({})'.format(n),
      setup = ['n = {}'.format(n),
               'a = b.de_bruijn(n)'],
      number = 10)

ctx = 'lal_char(a-e), z'
r = "[a-e]?{600}"
bench('a.shortest(5)',
      'a = std({}), c = {}'.format(r, ctx_signature(ctx)),
      setup = ['c = vcsn.context(ctx)',
               'a = c.expression(r).standard()'])

ctx = 'lat<lan_char(a), lan_char(x)>, q'
r = "(\e|x + a|\e)*"
bench('a.shortest(5000)',
      'a = derived_term({}), c = {}'.format(r, ctx_signature(ctx)),
      setup = ['c = vcsn.context(ctx)',
               'a = c.expression(r).derived_term().strip()'],
      number = 10)

# sort.
r = "[a-e]?{700}"
bench('a.sort()',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-e), z").expression(r).standard()'])

# proper.
r = "a?{1200}"
bench('a.proper()',
      'a = thompson({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lan_char(a), b").expression(r).thompson()'])

# to-expression.
bench('a.expression("associative", "delgado")',
      'a = ladybird(2000)',
      setup = ['a = vcsn.context("lal_char(a-c), b").ladybird(2000)'])

r = '[a-d]?{100}'
bench('a.expression("associative", "naive")',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-d), b").expression(r).standard()'])

bench('a.expression("associative", "naive")',
      'a = ladybird(8000)',
      setup = ['a = vcsn.context("lal_char(a-c), b").ladybird(8000)'])

r = '[a-d]?{15}'
bench('a.expression("linear", "delgado")',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-d), b").expression(r).standard()'])

bench('a.expression("linear", "delgado")',
      'a = ladybird(2000)',
      setup = ['a = vcsn.context("lal_char(a-c), b").ladybird(2000)'])

r = '[a-d]?{9}'
bench('a.expression("linear", "naive")',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-d), b").expression(r).standard()'])


bench('a.expression("linear", "naive")',
      'a = ladybird(4000)',
      setup = ['a = vcsn.context("lal_char(a-c), b").ladybird(4000)'])

# Other conjunction and power testcases, with more outgoing transitions
# per state.  This stresses much better the new conjunction algorithm.
r = "[a-e]?{50}"
bench('a.conjunction(a)',
      'a = std({})'.format(r),
      'a.conjunction(a)',
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-e), z").expression(r).standard()'])

bench('a.shuffle(a)',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-e), z").expression(r).standard()'])

# infiltration.
r = "[a-e]?{30}"
bench('a.infiltration(a)',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-e), z").expression(r).standard()'])

# Conjunction with spontaneous transitions.
r = "[a-e]?{80}"
bench('a.conjunction(a)',
      'a = thompson({})'.format(r),
      'a.conjunction(a)',
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lan_char(a-e), z").expression(r).thompson()'])

# power.
n = 12
r = "[a-e]*b(<2>[a-e])*"
bench('a & {}'.format(n),
     'a = std({})'.format(r),
     setup = ['r = "{}"'.format(r),
              'a = vcsn.context("lal_char(a-e), z").expression(r).standard()'])

# compose.
r = "['(a,a)'-'(i,z)']{4}"
bench('a.compose(a)',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'c = vcsn.context("lat<lal_char(a-z), lal_char(a-z)>, b")',
               'a = c.expression(r).standard()'])

bench('a.compose(a)',
      'a = thompson({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'c = vcsn.context("lat<lan_char(a-z), lan_char(a-z)>, b")',
               'a = c.expression(r).thompson()'])

# has_bounded_lag
r = "(['(a,x)'-'(b,y)']{1000})*"
bench('a.has_bounded_lag()',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'c = vcsn.context("lat<lan_char(a-c),lan_char(x-z)>, b")',
               'a = c.expression(r).standard()'])
r = "(['(a,x)'-'(b,y)']*){600}"
bench('a.has_bounded_lag()',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'c = vcsn.context("lat<lan_char(a-c),lan_char(x-z)>, b")',
               'a = c.expression(r).standard()'])


def bench_minimize(ctx, r, algo):
    bench('a.minimize("{}")'.format(algo),
          'a = std({}), c = {}'.format(r, ctx_signature(ctx)),
          setup = ['ctx = vcsn.context("{}")'.format(ctx),
                   'r = ctx.expression("{}")'.format(r),
                   'a = r.standard()'])
bench_minimize('lal_char(a-k), b', '[a-g]{800}',  'signature')
bench_minimize('lal_char(a-k), b', '[a-k]{2000}', 'moore')
bench_minimize('lal_char(a-k), b', '[a-g]{300}',  'weighted')
bench_minimize('lal_char(a-k), z', '[a-g]{300}',  'weighted')


bench('a.synchronizing_word()',
      'a = de_bruijn(6)',
      setup = 'a = vcsn.context("lal_char(a-c), b").de_bruijn(6).determinize()')

# reduce.
r = "[a-g]{300}"
bench('a.reduce()',
      'a = std({}), c = [a-k] -> Z'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-k), z").expression(r).standard()'])
bench('a.reduce()',
      'a = std({}), c = [a-k] -> Q'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-k), q").expression(r).standard()'])

# twins property.
r = "[a-c]{200}*+[a-c]{200}*"
bench('a.has_twins_property()',
      'a = std({}, "associative"), c = [abc] -> Q'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(a-c), zmin").expression(r, "associative").standard()'],
      number = 20)

# cycle ambiguous.
r = "[a-c]{2000}(<2>ab+a<3>b)"
bench('a.is_cycle_ambiguous()',
      'a = std({}), c = [abc] -> B'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lal_char(abc), z").expression(r).standard()'],
      number = 20)

# is_functional.
r = "'(a, x)'{2000}'(b, y)'"
bench('a.is_functional()',
      'a = std({})'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lat<lal_char(ab), lal_char(xy)>, b").expression(r).standard()'],
      number = 100)

# accessible.
r = "[a-m]{20000}"
bench('a.accessible()',
      'a = thompson({}).proper(False)'.format(r),
      setup = ['r = "{}"'.format(r),
               'a = vcsn.context("lan_char(a-z), b").expression(r).thompson().proper(False)'])

# scc.
r = '(abc)*{1000}'
for algo in ['dijkstra', 'kosaraju', 'tarjan_iterative', 'tarjan_recursive']:
    bench('a.scc("{}")'.format(algo),
          'a = std({})'.format(r),
          setup = ['r = "{}"'.format(r),
                   'c = vcsn.context("lal_char(abc), b")',
                   'a = c.expression(r).standard()'],
          number = 20)

# polynomial.trie.
r = '[a-j]{6}'
bench('p.trie()',
      'p = [a-j]{6}',
      setup = ['c = vcsn.context("law_char(a-j), b")',
               'm = c.polynomial("[a-j]")',
               'p = m ** 6'])
