#include <fesa/io/hdf5/writer.hpp>
#include <fesa/validation/comparison.hpp>
#include <fesa/validation/reference_csv.hpp>

#include <charconv>
#include <cmath>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace {

struct ComparisonRequest final {
    std::filesystem::path results;
    std::string instance;
    std::filesystem::path displacements;
    std::filesystem::path reactions;
    std::filesystem::path internal_forces;
    double displacement_absolute_scale;
    double reaction_absolute_scale;
    double internal_force_absolute_scale;
};

std::string_view quantity_name(const fesa::ReferenceQuantity quantity) {
    switch (quantity) {
        case fesa::ReferenceQuantity::displacement:
            return "displacement";
        case fesa::ReferenceQuantity::reaction:
            return "reaction";
        case fesa::ReferenceQuantity::internal_force:
            return "internal_force";
        case fesa::ReferenceQuantity::centroid_stress:
            return "centroid_stress";
    }
    return "unknown";
}

std::string_view component_name(
    const fesa::ReferenceQuantity quantity,
    const std::size_t component) {
    switch (quantity) {
        case fesa::ReferenceQuantity::displacement:
            return fesa::NodalFrame::displacement_components[component];
        case fesa::ReferenceQuantity::reaction:
            return fesa::NodalFrame::reaction_components[component];
        case fesa::ReferenceQuantity::internal_force:
            return fesa::BeamElementFrame::section_force_components[component];
        case fesa::ReferenceQuantity::centroid_stress:
            return fesa::BeamElementFrame::axial_stress_component;
    }
    return "unknown";
}

std::string_view stage_name(const fesa::DiagnosticStage stage) {
    switch (stage) {
        case fesa::DiagnosticStage::io:
            return "io";
        case fesa::DiagnosticStage::syntax:
            return "syntax";
        case fesa::DiagnosticStage::semantic:
            return "semantic";
        case fesa::DiagnosticStage::model:
            return "model";
        case fesa::DiagnosticStage::equation:
            return "equation";
        case fesa::DiagnosticStage::solver:
            return "solver";
        case fesa::DiagnosticStage::results:
            return "results";
        case fesa::DiagnosticStage::validation:
            return "validation";
    }
    return "unknown";
}

void print_diagnostic(const fesa::Diagnostic& diagnostic) {
    std::cerr << stage_name(diagnostic.stage) << " [" << diagnostic.code
              << "]";
    if (diagnostic.source.has_value()) {
        const fesa::SourceLocation& source = *diagnostic.source;
        std::cerr << ' ' << source.file.string() << ':' << source.line << ':'
                  << source.column;
    }
    std::cerr << ": " << diagnostic.message << '\n';
}

void print_diagnostics(const std::vector<fesa::Diagnostic>& diagnostics) {
    for (const fesa::Diagnostic& diagnostic : diagnostics) {
        print_diagnostic(diagnostic);
    }
}

void print_usage() {
    std::cerr
        << "Usage:\n"
        << "  fesa-reference-compare --results <results.h5> "
           "--instance <name> --displacements <displacements.csv> "
           "--reactions <reactions.csv> "
           "--internal-forces <internal-forces.csv> "
           "--displacement-absolute-scale <value> "
           "--reaction-absolute-scale <value> "
           "--internal-force-absolute-scale <value>\n";
}

std::optional<double> parse_number(const std::string_view text) {
    double value = 0.0;
    const auto parsed = std::from_chars(
        text.data(), text.data() + text.size(), value);
    if (
        parsed.ec != std::errc{} ||
        parsed.ptr != text.data() + text.size() ||
        !std::isfinite(value)) {
        return std::nullopt;
    }
    return value;
}

std::optional<ComparisonRequest> parse_request(
    const int argc,
    char* argv[]) {
    if (argc != 17) {
        return std::nullopt;
    }

    std::optional<std::filesystem::path> results;
    std::optional<std::string> instance;
    std::optional<std::filesystem::path> displacements;
    std::optional<std::filesystem::path> reactions;
    std::optional<std::filesystem::path> internal_forces;
    std::optional<double> displacement_absolute_scale;
    std::optional<double> reaction_absolute_scale;
    std::optional<double> internal_force_absolute_scale;

    for (int index = 1; index < argc; index += 2) {
        const std::string_view option{argv[index]};
        const std::string_view value{argv[index + 1]};
        if (option == "--results" && !results.has_value()) {
            results = std::filesystem::path{value};
        } else if (option == "--instance" && !instance.has_value()) {
            instance = value;
        } else if (
            option == "--displacements" &&
            !displacements.has_value()) {
            displacements = std::filesystem::path{value};
        } else if (option == "--reactions" && !reactions.has_value()) {
            reactions = std::filesystem::path{value};
        } else if (
            option == "--internal-forces" &&
            !internal_forces.has_value()) {
            internal_forces = std::filesystem::path{value};
        } else if (
            option == "--displacement-absolute-scale" &&
            !displacement_absolute_scale.has_value()) {
            displacement_absolute_scale = parse_number(value);
        } else if (
            option == "--reaction-absolute-scale" &&
            !reaction_absolute_scale.has_value()) {
            reaction_absolute_scale = parse_number(value);
        } else if (
            option == "--internal-force-absolute-scale" &&
            !internal_force_absolute_scale.has_value()) {
            internal_force_absolute_scale = parse_number(value);
        } else {
            return std::nullopt;
        }
    }

    if (
        !results.has_value() || results->empty() ||
        !instance.has_value() || instance->empty() ||
        !displacements.has_value() || displacements->empty() ||
        !reactions.has_value() || reactions->empty() ||
        !internal_forces.has_value() || internal_forces->empty() ||
        !displacement_absolute_scale.has_value() ||
        !reaction_absolute_scale.has_value() ||
        !internal_force_absolute_scale.has_value() ||
        *displacement_absolute_scale < 0.0 ||
        *reaction_absolute_scale < 0.0 ||
        *internal_force_absolute_scale < 0.0) {
        return std::nullopt;
    }

    return ComparisonRequest{
        std::move(*results),
        std::move(*instance),
        std::move(*displacements),
        std::move(*reactions),
        std::move(*internal_forces),
        *displacement_absolute_scale,
        *reaction_absolute_scale,
        *internal_force_absolute_scale,
    };
}

bool append_samples(
    const fesa::ResultFrame& frame,
    const std::vector<fesa::ReferenceRow>& rows,
    const fesa::Tolerance tolerance,
    std::vector<fesa::ComparisonSample>& samples) {
    bool matched = true;
    for (const fesa::ReferenceRow& row : rows) {
        fesa::ComparisonSampleMatch match = fesa::make_comparison_sample(
            frame,
            row.quantity,
            row.position,
            row.values,
            tolerance);
        if (!match.sample.has_value()) {
            print_diagnostics(match.failures);
            matched = false;
            continue;
        }
        samples.push_back(std::move(*match.sample));
    }
    return matched;
}

}  // namespace

int main(int argc, char* argv[]) {
    const std::optional<ComparisonRequest> request =
        parse_request(argc, argv);
    if (!request.has_value()) {
        print_usage();
        return 1;
    }

    const fesa::Hdf5ReadResult results =
        fesa::read_hdf5_results(request->results);
    if (!results.database.has_value()) {
        print_diagnostics(results.diagnostics);
        return 1;
    }

    const fesa::ReferenceCsvReadResult displacements =
        fesa::read_reference_csv(
            fesa::ReferenceQuantity::displacement,
            request->displacements,
            request->instance);
    const fesa::ReferenceCsvReadResult reactions =
        fesa::read_reference_csv(
            fesa::ReferenceQuantity::reaction,
            request->reactions,
            request->instance);
    const fesa::ReferenceCsvReadResult internal_forces =
        fesa::read_reference_csv(
            fesa::ReferenceQuantity::internal_force,
            request->internal_forces,
            request->instance);
    if (
        !displacements.diagnostics.empty() ||
        !reactions.diagnostics.empty() ||
        !internal_forces.diagnostics.empty()) {
        print_diagnostics(displacements.diagnostics);
        print_diagnostics(reactions.diagnostics);
        print_diagnostics(internal_forces.diagnostics);
        return 1;
    }

    const fesa::ResultFrame& frame =
        results.database->steps.front().frames.front();
    std::vector<fesa::ComparisonSample> samples;
    samples.reserve(
        displacements.rows.size() + reactions.rows.size() +
        internal_forces.rows.size());
    const bool displacements_matched = append_samples(
        frame,
        displacements.rows,
        {0.0, request->displacement_absolute_scale},
        samples);
    const bool reactions_matched = append_samples(
        frame,
        reactions.rows,
        {0.0, request->reaction_absolute_scale},
        samples);
    const bool internal_forces_matched = append_samples(
        frame,
        internal_forces.rows,
        {0.0, request->internal_force_absolute_scale},
        samples);
    if (
        !displacements_matched || !reactions_matched ||
        !internal_forces_matched) {
        return 1;
    }

    const fesa::CorrelationReport report = fesa::correlate_samples(samples);
    std::cout << std::setprecision(std::numeric_limits<double>::max_digits10);
    for (const fesa::ComponentCorrelationMetric& metric : report.metrics) {
        std::cout << "metric quantity=" << quantity_name(metric.quantity)
                  << " component="
                  << component_name(metric.quantity, metric.component_index)
                  << " count=" << metric.value_count
                  << " rmse=" << metric.root_mean_square_error
                  << " relative_l2=" << metric.relative_l2_error << '\n';
    }
    print_diagnostics(report.failures);
    return report.evaluable ? 0 : 1;
}
