/* ***********************************************************************
//
//   Copyright (C) 2025 -- The 4D-STAR Collaboration
//   File Author: Emily Boudreaux
//   Last Modified: March 19, 2025
//
//   4DSSE is free software; you can use it and/or modify
//   it under the terms and restrictions the GNU General Library Public
//   License version 3 (GPLv3) as published by the Free Software Foundation.
//
//   4DSSE 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 Library General Public License for more details.
//
//   You should have received a copy of the GNU Library General Public License
//   along with this software; if not, write to the Free Software
//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
#include "mfem.hpp"

#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

#include "polySolver.h"
#include "4DSTARTypes.h"
#include "config.h"
#include "integrators.h"
#include "operator.h"
#include "polyCoeff.h"
#include "probe.h"
#include "resourceManager.h"
#include "resourceManagerTypes.h"

#include "quill/LogMacros.h"


namespace laneEmden {

    double a (int k, double n) { // NOLINT(*-no-recursion)
        if ( k == 0 ) { return 1; }
        if ( k == 1 ) { return 0; }
        else { return -(c(k-2, n)/(std::pow(k, 2)+k)); }

    }

    double c(int m, double n) { // NOLINT(*-no-recursion)
        if ( m == 0 ) { return std::pow(a(0, n), n); }
        else {
            double termOne = 1.0/(m*a(0, n));
            double acc = 0;
            for (int k = 1; k <= m; k++) {
                acc += (k*n-m+k)*a(k, n)*c(m-k, n);
            }
            return termOne*acc;
        }
    }

    double thetaSeriesExpansion(double xi, double n, int order) {
        double acc = 0;
        for (int k = 0; k < order; k++) {
            acc += a(k, n) * std::pow(xi, k);
        }
        return acc;
    }
}

PolySolver::PolySolver(double n, double order) {

    // --- Check the polytropic index ---
    if (n > 4.99 || n < 0.0) {
        LOG_ERROR(m_logger, "The polytropic index n must be less than 5.0 and greater than 0.0. Currently it is {}", m_polytropicIndex);
        throw std::runtime_error("The polytropic index n must be less than 5.0 and greater than 0.0. Currently it is " + std::to_string(m_polytropicIndex));
    }

    m_polytropicIndex = n;
    m_feOrder = order;

    ResourceManager& rm = ResourceManager::getInstance();
    const Resource& resource = rm.getResource("mesh:polySphere");
    const auto &meshIO = std::get<std::unique_ptr<MeshIO>>(resource);
    meshIO->LinearRescale(polycoeff::x1(m_polytropicIndex));
    m_mesh = std::make_unique<mfem::Mesh>(meshIO->GetMesh());

    // Use feOrder - 1 for the RT space to satisfy Brezzi-Babuska condition
    // for the H1 and RT spaces
    m_fecH1 = std::make_unique<mfem::H1_FECollection>(m_feOrder, m_mesh->SpaceDimension());
    m_fecRT = std::make_unique<mfem::RT_FECollection>(m_feOrder - 1, m_mesh->SpaceDimension());

    m_feTheta = std::make_unique<mfem::FiniteElementSpace>(m_mesh.get(), m_fecH1.get());
    m_fePhi = std::make_unique<mfem::FiniteElementSpace>(m_mesh.get(), m_fecRT.get());

    m_theta = std::make_unique<mfem::GridFunction>(m_feTheta.get());
    m_phi = std::make_unique<mfem::GridFunction>(m_fePhi.get());

    assembleBlockSystem();

}

PolySolver::~PolySolver() = default;

void PolySolver::assembleBlockSystem() {

    // Start by defining the block structure of the system
    // Block 0: Theta (scalar space, uses m_feTheta)
    // Block 1: Phi (vector space, uses m_fePhi)
    mfem::Array<mfem::FiniteElementSpace*> feSpaces;
    feSpaces.Append(m_feTheta.get());
    feSpaces.Append(m_fePhi.get());

    // Create the block offsets. These define the start of each block in the combined vector.
    // Block offsets will be [0, thetaDofs, thetaDofs + phiDofs]
    mfem::Array<int> blockOffsets;
    blockOffsets.SetSize(3);
    blockOffsets[0] = 0;
    blockOffsets[1] = feSpaces[0]->GetVSize();
    blockOffsets[2] = feSpaces[1]->GetVSize();
    blockOffsets.PartialSum();

    // Add integrators to block form one by one
    // We add integrators corresponding to each term in the weak form
    // The block form of the residual matrix
    // ⎡ 0 -M ⎤ ⎡ θ ⎤ + ⎡f(θ)⎤ = ⎡ 0 ⎤ = R(X)
    // ⎣ -Q D ⎦ ⎣ Φ ⎦   ⎣  0 ⎦   ⎣ 0 ⎦
    // This then simplifies to 
    // ⎡f(θ) - MΘ ⎤ = ⎡ 0 ⎤ = R
    // ⎣ -Qɸ   Dθ ⎦   ⎣ 0 ⎦ 
    // Here M, Q, and D are
    // M = ∫∇ψᶿ·Nᵠ dV (MixedVectorWeakDivergenceIntegrator)
    // D = ∫ψᵠ·Nᵠ dV (VectorFEMassIntegrator)
    // Q = ∫ψᵠ·∇Nᶿ dV (MixedVectorGradientIntegrator)
    // f(θ) = ∫ψᶿ·θⁿ dV (NonlinearPowerIntegrator)
    // Here ψᶿ and ψᵠ are the test functions for the theta and phi spaces, respectively
    // Nᵠ and Nᶿ are the basis functions for the theta and phi spaces, respectively
    // A full derivation of the weak form can be found in the 4DSSE documentation

    // --- Assemble the MixedBilinear and Bilinear forms (M, D, and Q) ---
    auto Mform = std::make_unique<mfem::MixedBilinearForm>(m_fePhi.get(), m_feTheta.get());
    auto Qform = std::make_unique<mfem::MixedBilinearForm>(m_feTheta.get(), m_fePhi.get());
    auto Dform = std::make_unique<mfem::BilinearForm>(m_fePhi.get());
    
    // TODO: Check the sign on all of the integrators 
    Mform->AddDomainIntegrator(new mfem::MixedVectorWeakDivergenceIntegrator());
    Qform->AddDomainIntegrator(new mfem::MixedVectorGradientIntegrator());
    Dform->AddDomainIntegrator(new mfem::VectorFEMassIntegrator());

    Mform->Assemble();
    Mform->Finalize();

    Qform->Assemble();
    Qform->Finalize();

    Dform->Assemble();
    Dform->Finalize();

    // --- Assemble the NonlinearForm (f) ---
    auto fform = std::make_unique<mfem::NonlinearForm>(m_feTheta.get());
    mfem::ConstantCoefficient oneCoeff(1.0);
    fform->AddDomainIntegrator(new polyMFEMUtils::NonlinearPowerIntegrator(oneCoeff, m_polytropicIndex));
    // TODO: Add essential boundary conditions to the nonlinear form

    // -- Build the BlockOperator --
    m_polytropOperator = std::make_unique<PolytropeOperator>(
        std::move(Mform),
        std::move(Qform),
        std::move(Dform),
        std::move(fform),
        blockOffsets
    ); 

}

void PolySolver::solve() const {
    // --- Set the initial guess for the solution ---
    setInitialGuess();

    setupOperator();

    // It's safer to get the offsets directly from the operator after finalization
    const mfem::Array<int>& block_offsets = m_polytropOperator->GetBlockOffsets(); // Assuming a getter exists or accessing member if public/friend
    mfem::BlockVector state_vector(block_offsets);
    state_vector.GetBlock(0) = *m_theta;
    state_vector.GetBlock(1) = *m_phi;

    mfem::Vector zero_rhs(block_offsets.Last());
    zero_rhs = 0.0;


    mfem::NewtonSolver newtonSolver = setupNewtonSolver();

    // EMB 2025: Calling Mult gets the gradient of the operator for the NewtonSolver
    // This then is set as the operator for the solver for NewtonSolver
    // The solver (assuming it is an iterative solver) then sets the
    // operator for its preconditioner to this.
    // What this means is that there is no need to manually deal
    // with updating the preconditioner at every newton step as the
    // changes to the jacobian are automatically propagated through the
    // solving chain. This is at least true with MFEM 4.8-rc0
    newtonSolver.Mult(zero_rhs, state_vector);

    // --- Save and view the solution ---
    saveAndViewSolution(state_vector);


}

SSE::MFEMArrayPairSet PolySolver::getEssentialTrueDof() const {
    mfem::Array<int> theta_ess_tdof_list;
    mfem::Array<int> phi_ess_tdof_list;

    mfem::Array<int> thetaCenterDofs, phiCenterDofs;
    mfem::Array<double> thetaCenterVals, phiCenterVals;
    std::tie(thetaCenterDofs, phi_ess_tdof_list) = findCenterElement();
    thetaCenterVals.SetSize(thetaCenterDofs.Size());
    phiCenterVals.SetSize(phi_ess_tdof_list.Size());

    thetaCenterVals = 1.0;
    phiCenterVals = 0.0;

    mfem::Array<int> ess_brd(m_mesh->bdr_attributes.Max());
    ess_brd = 1;

    mfem::Array<double> thetaSurfaceVals;
    m_feTheta->GetEssentialTrueDofs(ess_brd, theta_ess_tdof_list);
    thetaSurfaceVals.SetSize(theta_ess_tdof_list.Size());
    thetaSurfaceVals = 0.0;
    // combine the essential dofs with the center dofs
    theta_ess_tdof_list.Append(thetaCenterDofs);
    thetaSurfaceVals.Append(thetaCenterVals);

    SSE::MFEMArrayPair thetaPair = std::make_pair(theta_ess_tdof_list, thetaSurfaceVals);
    SSE::MFEMArrayPair phiPair = std::make_pair(phi_ess_tdof_list, phiCenterVals);
    SSE::MFEMArrayPairSet pairSet = std::make_pair(thetaPair, phiPair);

    return pairSet;
}

std::pair<mfem::Array<int>, mfem::Array<int>> PolySolver::findCenterElement() const {
    mfem::Array<int> thetaCenterDofs;
    mfem::Array<int> phiCenterDofs;
    mfem::DenseMatrix centerPoint(m_mesh->SpaceDimension(), 1);
    centerPoint(0, 0) = 0.0;
    centerPoint(1, 0) = 0.0;
    centerPoint(2, 0) = 0.0;

    mfem::Array<int> elementIDs;
    mfem::Array<mfem::IntegrationPoint> ips;
    m_mesh->FindPoints(centerPoint, elementIDs, ips);
    mfem::Array<int> tempDofs;
    for (int i = 0; i < elementIDs.Size(); i++) {
        m_feTheta->GetElementDofs(elementIDs[i], tempDofs);
        thetaCenterDofs.Append(tempDofs);
        m_fePhi->GetElementDofs(elementIDs[i], tempDofs);
        phiCenterDofs.Append(tempDofs);
    }
    return std::make_pair(thetaCenterDofs, phiCenterDofs);
}

void PolySolver::setInitialGuess() const {
    // --- Set the initial guess for the solution ---
    mfem::FunctionCoefficient thetaInitGuess (
        [this](const mfem::Vector &x) {
            double r = x.Norml2();
            double radius = Probe::getMeshRadius(*m_mesh);
            double u = 1/radius;

            return -std::pow((u*r), 2)+1.0;
        }
    );
    mfem::VectorFunctionCoefficient phiInitGuess (m_mesh->SpaceDimension(),
        [this](const mfem::Vector &x, mfem::Vector &v) {
            double radius = Probe::getMeshRadius(*m_mesh);
            double u = -1/std::pow(radius,2);
            v(0) = 2*x(0)*u;
            v(1) = 2*x(1)*u;
            v(2) = 2*x(2)*u;
        }
    );
    m_theta->ProjectCoefficient(thetaInitGuess);
    m_phi->ProjectCoefficient(phiInitGuess);
    if (m_config.get<bool>("Poly:Solver:ViewInitialGuess", false)) {
        Probe::glVisView(*m_theta, *m_mesh, "θ init");
        Probe::glVisView(*m_phi, *m_mesh, "ɸ init");
    }

}

void PolySolver::saveAndViewSolution(const mfem::BlockVector& state_vector) const {
    mfem::BlockVector x_block(const_cast<mfem::BlockVector&>(state_vector), m_polytropOperator->GetBlockOffsets());
    mfem::Vector& x_theta = x_block.GetBlock(0);
    mfem::Vector& x_phi = x_block.GetBlock(1);

    if (m_config.get<bool>("Poly:Output:View", false)) {
        Probe::glVisView(x_theta, *m_feTheta, "θ Solution");
        Probe::glVisView(x_phi, *m_fePhi, "ɸ Solution");
    }

    // --- Extract the Solution ---
    if (bool write11DSolution = m_config.get<bool>("Poly:Output:1D:Save", true)) {
        auto solutionPath = m_config.get<std::string>("Poly:Output:1D:Path", "polytropeSolution_1D.csv");
        auto derivSolPath = "d" + solutionPath;

        auto rayCoLatitude = m_config.get<double>("Poly:Output:1D:RayCoLatitude", 0.0);
        auto rayLongitude = m_config.get<double>("Poly:Output:1D:RayLongitude", 0.0);
        auto raySamples = m_config.get<int>("Poly:Output:1D:RaySamples", 100);

        std::vector rayDirection = {rayCoLatitude, rayLongitude};

        Probe::getRaySolution(x_theta, *m_feTheta, rayDirection, raySamples, solutionPath);
        // Probe::getRaySolution(x_phi, *m_fePhi, rayDirection, raySamples, derivSolPath);
    }
}

void PolySolver::setupOperator() const {

    SSE::MFEMArrayPairSet ess_tdof_pair_set = getEssentialTrueDof();
    m_polytropOperator->SetEssentialTrueDofs(ess_tdof_pair_set);

    // -- Finalize the operator --
    m_polytropOperator->finalize();

    if (!m_polytropOperator->isFinalized()) {
        LOG_ERROR(m_logger, "PolytropeOperator is not finalized. Cannot solve.");
        throw std::runtime_error("PolytropeOperator is not finalized. Cannot solve.");
    }
}

void PolySolver::LoadSolverUserParams(double &newtonRelTol, double &newtonAbsTol, int &newtonMaxIter, int &newtonPrintLevel, double &gmresRelTol, double &gmresAbsTol, int &gmresMaxIter, int &gmresPrintLevel) const {
    newtonRelTol = m_config.get<double>("Poly:Solver:Newton:RelTol", 1e-7);
    newtonAbsTol = m_config.get<double>("Poly:Solver:Newton:AbsTol", 1e-7);
    newtonMaxIter = m_config.get<int>("Poly:Solver:Newton:MaxIter", 200);
    newtonPrintLevel = m_config.get<int>("Poly:Solver:Newton:PrintLevel", 1);

    gmresRelTol = m_config.get<double>("Poly:Solver:GMRES:RelTol", 1e-10);
    gmresAbsTol = m_config.get<double>("Poly:Solver:GMRES:AbsTol", 1e-12);
    gmresMaxIter = m_config.get<int>("Poly:Solver:GMRES:MaxIter", 2000);
    gmresPrintLevel = m_config.get<int>("Poly:Solver:GMRES:PrintLevel", 0);

    LOG_DEBUG(m_logger, "Newton Solver (relTol: {:0.2E}, absTol: {:0.2E}, maxIter: {}, printLevel: {})", newtonRelTol, newtonAbsTol, newtonMaxIter, newtonPrintLevel);
    LOG_DEBUG(m_logger, "GMRES Solver (relTol: {:0.2E}, absTol: {:0.2E}, maxIter: {}, printLevel: {})", gmresRelTol, gmresAbsTol, gmresMaxIter, gmresPrintLevel);
}

mfem::BlockDiagonalPreconditioner PolySolver::build_preconditioner() const {
    // --- Set up the preconditioners. The non-linear form will use a Chebyshev Preconditioner while the linear terms will use a simpler Jacobi preconditioner ---
    mfem::BlockDiagonalPreconditioner prec(m_polytropOperator->GetBlockOffsets());
    const mfem::BlockOperator &jacobian = m_polytropOperator->GetJacobianOperator();

    // Get all the blocks. J00 -> Non-linear form (df(θ)/dθ), J01-> -M, J10 -> -Q, J11 -> D
    const mfem::Operator& J00 = jacobian.GetBlock(0, 0);
    mfem::Vector J00diag;

    J00.AssembleDiagonal(J00diag);

    SSE::MFEMArrayPairSet ess_tdof_pair_set = m_polytropOperator->GetEssentialTrueDofs();

    // TODO: This order may need to be tuned (EMB)
    // --- ess_tdof_pair_set.first -> (theta dof ids, theta dof vals).first -> theta dof ids
    // --- ess_tdof_pair_set.second -> (phi dof ids, phi dof vals).first -> phi dof ids
    mfem::OperatorChebyshevSmoother J00Prec(J00, J00diag, ess_tdof_pair_set.first.first, 2);
    mfem::OperatorJacobiSmoother J11Prec(m_polytropOperator->GetJ11diag(), ess_tdof_pair_set.second.first);

    prec.SetDiagonalBlock(0, &J00Prec);
    prec.SetDiagonalBlock(1, &J11Prec);

    return prec;
}

mfem::NewtonSolver PolySolver::setupNewtonSolver() const {
    // --- Load configuration parameters ---
    double newtonRelTol, newtonAbsTol, gmresRelTol, gmresAbsTol;
    int newtonMaxIter, newtonPrintLevel, gmresMaxIter, gmresPrintLevel;
    LoadSolverUserParams(newtonRelTol, newtonAbsTol, newtonMaxIter, newtonPrintLevel, gmresRelTol, gmresAbsTol,
                         gmresMaxIter, gmresPrintLevel);

    // --- Set up the Newton solver ---
    mfem::NewtonSolver newtonSolver;
    newtonSolver.SetRelTol(newtonRelTol);
    newtonSolver.SetAbsTol(newtonAbsTol);
    newtonSolver.SetMaxIter(newtonMaxIter);
    newtonSolver.SetPrintLevel(newtonPrintLevel);
    newtonSolver.SetOperator(*m_polytropOperator);

    // --- Created the linear solver which is used to invert the jacobian ---
    mfem::GMRESSolver gmresSolver;
    gmresSolver.SetRelTol(gmresRelTol);
    gmresSolver.SetAbsTol(gmresAbsTol);
    gmresSolver.SetMaxIter(gmresMaxIter);
    gmresSolver.SetPrintLevel(gmresPrintLevel);

    // build_preconditioner();


    newtonSolver.SetSolver(gmresSolver);

    return newtonSolver;
}
