#!/usr/bin/env python3

import scipy.optimize as optimize
from timfuz import Benchmark, load_sub, A_ub_np2d, acorner2csv, corner_s2i
import numpy as np
import glob
import json
import math
import sys
import os
import time
import timfuz_solve


def run_corner(
        Anp, b, names, corner, verbose=False, opts={}, meta={}, outfn=None):
    if len(Anp) == 0:
        print('WARNING: zero equations')
        if outfn:
            timfuz_solve.solve_save(outfn, [], [], corner)
        return
    maxcorner = {
        'slow_max': True,
        'slow_min': False,
        'fast_max': True,
        'fast_min': False,
    }[corner]

    # Given timing scores for above delays (-ps)
    assert type(Anp[0]) is np.ndarray, type(Anp[0])
    assert type(b) is np.ndarray, type(b)

    #check_feasible(Anp, b)
    '''
    Be mindful of signs
    t1, t2: total delay contants
    d1, d2..: variables to solve for

    Max corner intuitive form:
    d1 + d2 +     d4 >= t1
         d2 + d3     >= t2

    But need it in compliant form:
    -d1 + -d2 +      -d4 <= -t1
          -d2 + -d3      <= -t2
    Minimize delay elements


    Min corner intuitive form:
    d1 + d2 +     d4 <= t1
         d2 + d3     <= t2
    Maximize delay elements
    '''

    rows = len(Anp)
    cols = len(Anp[0])
    if maxcorner:
        print('maxcorner => scaling to solution form...')
        b_ub = -1.0 * b
        #A_ub = -1.0 * Anp
        A_ub = [-1.0 * x for x in Anp]
    else:
        print('mincorner => no scaling required')
        b_ub = b
        A_ub = Anp

    print('Creating misc constants...')
    # Minimization function scalars
    # Treat all logic elements as equally important
    if maxcorner:
        # Best result are min delays
        c = [1 for _i in range(len(names))]
    else:
        # Best result are max delays
        c = [-1 for _i in range(len(names))]
    # Delays cannot be negative
    # (this is also the default constraint)
    #bounds =  [(0, None) for _i in range(len(names))]
    # Also you can provide one to apply to all
    bounds = (0, None)

    # Seems to take about rows + 3 iterations
    # Give some margin
    #maxiter = int(1.1 * rows + 100)
    #maxiter = max(1000, int(1000 * rows + 1000))
    # Most of the time I want it to just keep going unless I ^C it
    maxiter = 1000000

    if verbose >= 2:
        print('b_ub', b)
    print('Unique delay elements: %d' % len(names))
    print('  # delay minimization weights: %d' % len(c))
    print('  # delay constraints: %d' % len(bounds))
    print('Input paths')
    print('  # timing scores: %d' % len(b))
    print('  Rows: %d' % rows)

    tlast = [time.time()]
    iters = [0]
    printn = [0]

    def callback(xk, **kwargs):
        iters[0] = kwargs['nit']
        if time.time() - tlast[0] > 1.0:
            sys.stdout.write('I:%d ' % kwargs['nit'])
            tlast[0] = time.time()
            printn[0] += 1
            if printn[0] % 10 == 0:
                sys.stdout.write('\n')
            sys.stdout.flush()

    print('')
    # Now find smallest values for delay constants
    # Due to input bounds (ex: column limit), some delay elements may get eliminated entirely
    # https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.optimize.linprog.html
    print('Running linprog w/ %d r, %d c (%d name)' % (rows, cols, len(names)))
    res = optimize.linprog(
        c,
        A_ub=A_ub,
        b_ub=b_ub,
        bounds=bounds,
        callback=callback,
        options={
            "disp": True,
            'maxiter': maxiter,
            'bland': True,
            'tol': 1e-6,
        })
    nonzeros = 0
    print('Ran %d iters' % iters[0])
    if res.success:
        print('Result sample (%d elements)' % (len(res.x)))
        plim = 3
        for xi, (name, x) in enumerate(zip(names, res.x)):
            nonzero = x >= 0.001
            if nonzero:
                nonzeros += 1
            #if nonzero and (verbose >= 1 or xi > 30):
            if nonzero and (verbose or (
                (nonzeros < 100 or nonzeros % 20 == 0) and nonzeros <= plim)):
                print('  % 4u % -80s % 10.1f' % (xi, name, x))
        print('Delay on %d / %d' % (nonzeros, len(res.x)))

        if outfn:
            timfuz_solve.solve_save(
                outfn, res.x, names, corner, verbose=verbose)


def main():
    import argparse

    parser = argparse.ArgumentParser(
        description=
        'Solve timing solution using linear programming inequalities')

    parser.add_argument('--verbose', action='store_true', help='')
    parser.add_argument('--massage', action='store_true', help='')
    parser.add_argument(
        '--bounds-csv', help='Previous solve result starting point')
    parser.add_argument(
        '--sub-json', help='Group substitutions to make fully ranked')
    parser.add_argument('--corner', required=True, default="slow_max", help='')
    parser.add_argument(
        '--out', default=None, help='output timing delay .json')
    parser.add_argument('fns_in', nargs='+', help='timing4i.csv input files')
    args = parser.parse_args()
    # Store options in dict to ease passing through functions
    bench = Benchmark()

    fns_in = args.fns_in
    if not fns_in:
        fns_in = glob.glob('specimen_*/timing4i.csv')

    sub_json = None
    if args.sub_json:
        sub_json = load_sub(args.sub_json)

    try:
        timfuz_solve.run(
            run_corner=run_corner,
            sub_json=sub_json,
            bounds_csv=args.bounds_csv,
            fns_in=fns_in,
            corner=args.corner,
            massage=args.massage,
            outfn=args.out,
            verbose=args.verbose)
    finally:
        print('Exiting after %s' % bench)


if __name__ == '__main__':
    main()
