/*
    Copyright (C) 2018 Fredrik Johansson

    This file is part of Arb.

    Arb is free software: you can redistribute it and/or modify it under
    the terms of the GNU Lesser General Public License (LGPL) as published
    by the Free Software Foundation; either version 2.1 of the License, or
    (at your option) any later version.  See <http://www.gnu.org/licenses/>.
*/

#include "acb_mat.h"

/* todo: move out */
void
acb_mat_inf_norm(arb_t res, const acb_mat_t A, slong prec)
{
    slong i, j, m, n;
    arb_t s, t;

    m = acb_mat_nrows(A);
    n = acb_mat_nrows(A);

    if (m == 0 || n == 0)
    {
        arb_zero(res);
        return;
    }

    arb_init(s);
    arb_init(t);

    arb_zero(res);

    for (i = 0; i < m; i++)
    {
        acb_abs(s, acb_mat_entry(A, i, 0), prec);

        for (j = 1; j < n; j++)
        {
            acb_abs(t, acb_mat_entry(A, i, j), prec);
            arb_add(s, s, t, prec);
        }

        arb_max(res, res, s, prec);
    }

    arb_clear(s);
    arb_clear(t);
}

static void
diagonal_certify(arb_t epsilon, arb_t eta1, arb_t eta2, const acb_mat_t D, const acb_mat_t H, slong prec)
{
    arb_t mu, sigma, alpha, t, u, v;
    acb_t d;
    slong i, j, n;

    arb_init(mu);
    arb_init(sigma);
    arb_init(alpha);
    arb_init(t);
    arb_init(u);
    arb_init(v);
    acb_init(d);

    n = acb_mat_nrows(D);

    /* D = diagonal matrix; H = off diagonal matrix */

    /* mu = ||D|| */
    acb_mat_inf_norm(mu, D, prec);

    /* sigma = sigma(D) = separation number */
    arb_pos_inf(sigma);
    for (i = 0; i < n; i++)
    {
        for (j = i + 1; j < n; j++)
        {
            acb_sub(d, acb_mat_entry(D, i, i), acb_mat_entry(D, j, j), prec);
            acb_abs(t, d, prec);
            arb_min(sigma, sigma, t, prec);
        }
    }

    /* eta1 = ||Delta(H)||   Delta = diagonal projection  */
    arb_zero(eta1);

    /* eta2 = ||Omega(H)||   Omega = off-diagonal projection  */
    acb_mat_inf_norm(eta2, H, prec);

    /* alpha = min(sigma / (6 mu), 1/4) */
    arb_div(t, sigma, mu, prec);
    arb_div_ui(t, t, 6, prec);
    arb_set_d(u, 0.25);
    arb_min(alpha, t, u, prec);

    arb_add(t, eta1, eta2, prec);
    arb_mul(u, alpha, mu, prec);
    arb_mul_2exp_si(u, u, -3);

    arb_mul(v, alpha, sigma, prec);
    arb_mul_2exp_si(v, v, -3);

    if (arb_le(t, u) && arb_le(eta2, v))
    {
        arb_div(epsilon, eta2, sigma, prec);
        arb_mul_ui(epsilon, epsilon, 3, prec);
    }
    else
    {
        arb_indeterminate(epsilon);
    }

    arb_clear(mu);
    arb_clear(sigma);
    arb_clear(alpha);
    arb_clear(t);
    arb_clear(u);
    arb_clear(v);
    acb_clear(d);
}

int
acb_mat_eig_simple_vdhoeven_mourrain(acb_ptr E,
    acb_mat_t L, acb_mat_t R, const acb_mat_t A,
    acb_srcptr E_approx, const acb_mat_t R_approx, slong prec)
{
    acb_mat_t D, T, AT;
    int result;
    slong i, j, n;

    result = 0;
    n = acb_mat_nrows(A);

    if (n == 0)
        return 1;

    if (n == 1)
    {
        acb_set_round(E, acb_mat_entry(A, 0, 0), prec);
        if (L != NULL)
            acb_one(acb_mat_entry(L, 0, 0));
        if (R != NULL)
            acb_one(acb_mat_entry(R, 0, 0));
        return 1;
    }

    acb_mat_init(D, n, n);
    acb_mat_init(T, n, n);
    acb_mat_init(AT, n, n);

    /* T D = A T */
    acb_mat_get_mid(T, R_approx);
    acb_mat_mul(AT, A, T, prec);

    if (acb_mat_solve(D, T, AT, prec))
    {
        acb_mat_t DD, DH;
        arb_t epsilon, eta1, eta2;

        arb_init(epsilon);
        arb_init(eta1);
        arb_init(eta2);
        acb_mat_init(DD, n, n);
        acb_mat_init(DH, n, n);

        for (i = 0; i < n; i++)
            acb_set(acb_mat_entry(DD, i, i), acb_mat_entry(D, i, i));

        for (i = 0; i < n; i++)
            for (j = 0; j < n; j++)
                if (i != j)
                    acb_set(acb_mat_entry(DH, i, j), acb_mat_entry(D, i, j));

        diagonal_certify(epsilon, eta1, eta2, DD, DH, 2 * MAG_BITS);

        if (arb_is_finite(epsilon))
        {
            for (i = 0; i < n; i++)
            {
                /* note: paper actually uses D_c which would be better? */
                acb_set(E + i, acb_mat_entry(D, i, i));
                arb_add_error(acb_realref(E + i), eta2);
                arb_add_error(acb_imagref(E + i), eta2);
            }

            result = 1;

            /* unlikely */
            for (i = 0; i < n; i++)
                for (j = i + 1; j < n; j++)
                    if (acb_overlaps(E + i, E + j))
                        result = 0;

            if (result && (R != NULL || L != NULL))
            {
                mag_t ep, em;
                mag_init(ep);
                mag_init(em);

                arb_get_mag(ep, epsilon);
                acb_mat_zero(D);
                acb_mat_add_error_mag(D, ep);
                acb_mat_mul(D, T, D, MAG_BITS);

                for (i = 0; i < n; i++)
                {
                    for (j = 0; j < n; j++)
                    {
                        acb_get_mag(ep, acb_mat_entry(D, i, j));
                        acb_add_error_mag(acb_mat_entry(T, i, j), ep);
                    }
                }

                if (R != NULL)
                    acb_mat_set(R, T);

                if (L != NULL)
                {
                    if (!acb_mat_inv(L, T, prec))
                        acb_mat_indeterminate(L);
                }

                mag_clear(ep);
                mag_clear(em);
            }
        }

        acb_mat_clear(DD);
        acb_mat_clear(DH);
        arb_clear(epsilon);
        arb_clear(eta1);
        arb_clear(eta2);
    }

    if (!result)
    {
        for (i = 0; i < n; i++)
            acb_indeterminate(E + i);
        if (L != NULL)
            acb_mat_indeterminate(L);
        if (R != NULL)
            acb_mat_indeterminate(R);
    }

    acb_mat_clear(D);
    acb_mat_clear(T);
    acb_mat_clear(AT);

    return result;
}
