/*
 * Parsing physical units.
 *
 * Author: Malte J. Ziebarth (malte.ziebarth@tum.de)
 *
 * Copyright (C) 2026 Technische Universität München
 *
 * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 *
 * SPDX-FileCopyrightText: 2026 Technical University of Munich
 * SPDX-License-Identifier: EUPL-1.2
 * SPDX-FileContributor: Malte J. Ziebarth <malte.ziebarth@tum.de>
 */

#include <funktionsklempner/quantityarray.hpp>
#include <iostream>
#include <array>
#include <boost/units/io.hpp>


using namespace funktionsklempner;
namespace si = boost::units::si;
namespace bu = boost::units;


/*
 * Compile-time checks
 * ===================
 *
 * 1. NDArray:
 */
static_assert(std::ranges::random_access_range<ndarray::NDArray<double>>);
static_assert(std::ranges::random_access_range<ndarray::NDArray<const double>>);


/*
 * 2. QuantityIterator:
 */
using TestIterator = qtyarray::QuantityIterator<si::length, double>;

constexpr TestIterator TEST;

static_assert(std::weakly_incrementable<TestIterator>);
static_assert(std::input_or_output_iterator<TestIterator>);
static_assert(std::input_iterator<TestIterator>);
static_assert(std::incrementable<TestIterator>);
static_assert(std::forward_iterator<TestIterator>);
static_assert(std::bidirectional_iterator<TestIterator>);
static_assert(std::totally_ordered<TestIterator>);
static_assert(std::sized_sentinel_for<TestIterator, TestIterator>);
static_assert(std::random_access_iterator<TestIterator>);


/*
 * 3. QuantityArray:
 */
using TestQA = qtyarray::QuantityArray<si::length, double>;
static_assert(std::ranges::range<TestQA>);
static_assert(std::ranges::sized_range<TestQA>);
static_assert(std::ranges::random_access_range<TestQA>);



/*
 * Runtime checks:
 */
template<typename F>
bool throws(F&& f)
{
    bool throws_ = false;
    try {
        f();
    } catch (...){
        throws_ = true;
    }
    return throws_;
}


template<typename T>
bool is_close(
    const T& q0,
    const T& q1
)
{
    return math::abs((q0 - q1) / std::max(math::abs(q0), math::abs(q1))) < 2e-16;
}





int main(){
    /*
     * 1. Test the unit parsing at runtime:
     */
    try {
        auto res = parse_si_unit<si::force>(u8"kg m s^-2");
    } catch (std::runtime_error& e){
        std::cerr << "runtime_error: " << e.what() << "\n" << std::flush;
    } catch (...){
        std::cerr << "unknown error.\n" << std::flush;
    }

    /*
     * 2. Make sure that QuantityIterator can be default initialized.
     *    This can catch a whole lot of template problems.
     */
    qtyarray::QuantityIterator<si::length, double> qi;

    /*
     * 3. The Quantity array:
     */
    std::array<double, 6> values{1.092, 29198., 1e-3, 921., 492., -891.};
    qtyarray::QuantityArray<si::length, double> qa(
        &values[0], values.size(), "double", "cm"
    );

    /*
     * 4. Ensure that it's the same value as explicitly multiplying
     *    the double values with 0.01 * si::meters:
     */
    qi = qa.begin();
    size_t i = 0;
    for (double d : values){
        /* Check the iterator: */
        if (*qi != 0.01 * d * si::meters){
            std::cerr << "difference in element " << i << ": Δ = "
                << static_cast<double>((0.01 * d * si::meters - *qi) / si::meters)
                << " (iterator access)\n";
            return 1;
        }

        /* Check the index: */
        if (qa[i] != 0.01 * d * si::meters){
            std::cerr << "difference in element " << i << ": Δ = "
                << static_cast<double>((0.01 * d * si::meters - *qi) / si::meters)
                << " (index-based access)\n";
            return 1;
        }

        ++qi;
        ++i;
    }

    /*
     * 5. Ensure that it differs from the double values
     *    explicitly multiplied by si::meters:
     */
    qi = qa.begin();
    i = 0;
    for (double d : values){
        if (*qi == d * si::meters){
            std::cerr << "no difference in element " << i << ": Δ = "
                << static_cast<double>((0.01 * d * si::meters - *qi) / si::meters)
                << " (but should be)\n";
            return 1;
        }
        ++qi;
        ++i;
    }

    /*
     * 6. Use a range-based comparison here to ensure that
     *    the range interface of QuantityArray works as intended:
     */

    bool did_compare = false;
    size_t unequal_element = 0;
    auto compare_ranges =
    [&did_compare, &unequal_element]
    (auto&& r0, auto&& r1) -> bool
    {
        auto it0 = r0.begin();
        auto it1 = r1.begin();
        for (; (it0 != r0.end()) && (it1 != r1.end());)
        {
            did_compare = true;
            if (*it0 != *it1){
                std::cout << "Differing values " << *it0 << " vs " << *it1
                    << " in line " << __LINE__ << " (difference is "
                    << *it0 - *it1 << ")\n";
                return false;
            }
            ++it0;
            ++it1;
            ++unequal_element;
        }
        return true;
    };

    bool equal = compare_ranges(
        qa
        | std::ranges::views::transform(
            [](const bu::quantity<si::length>& l) -> double {
                return std::exp(-l / (200 * si::meters));
            }
        ),
        values
        | std::ranges::views::transform(
            [](double d) -> double {
                /* We write this in a way that precisely mimics the
                 * floating point operations performed in the above.
                 * If we were to write something like
                 *    std::exp(-d / 200e2),
                 * for instance (which is identical without rounding
                 * errors), we end up with rounding errors in the
                 * range of machine precision.
                 * That later causes the equality check to fail. */
                return std::exp(-(0.01 * d) / 200);
            }
        )
    );

    if (!did_compare){
        std::cerr << "Did not compare any elements of the two ranges in file "
            << __FILE__ << " in line " << __LINE__ << ".\n";
        return 1;
    }
    if (!equal){
        std::cerr << "The element of index " << unequal_element
            << " is not equal between the two ranges compared in file "
            << __FILE__ << " in line " << __LINE__ << "\n";
        return 1;
    }


    /*
     * Check the input and output functionality:
     */
    OutputQuantityArray<si::length, double> oqa(
        &values[0], values.size(), "double", "cm"
    );
    for (auto&& l : oqa)
        l = 1.0 * si::meter;

    InputQuantityArray<si::length, double> iqa(
        &values[0], values.size(), "double", "cm"
    );
    i = 0;
    for (auto&& l : oqa){
        if (l != 1.0 * si::meter){
            std::cout << "Element " << i
                << " has not successfully been set to 1 in line "
                << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }
        ++i;
    }
    i = 0;
    for (double d : values){
        if (d != 100.0){
            std::cout << "Element " << i
                << " has not successfully been set to 100 cm in line "
                << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }
        ++i;
    }

    /* Add 51 cm to all elements: */
    for (auto&& l : oqa)
        ((l += 0.5 * si::meter) -= 0.01 * si::meter) += 0.02 * si::meter;

    i = 0;
    for (double d : values){
        if (d != 151.0){
            std::cout << "Element " << i
                << " has not successfully been added 51 cm in line "
                << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }
        ++i;
    }

    /* Set the unit: */
    const bu::quantity<si::length> u0 =  1.0 * si::meters;
    const bu::quantity<si::length> u1 = -1.0 * si::meters;
    const bu::quantity<si::length> u2 = 0.0 * si::meters;

    if (throws([&oqa,&u0]{oqa.set_unit(u0);})){
        std::cerr << "Threw an error trying to set a positive unit in "
            "line " << __LINE__ << " in file " << __FILE__ << "\n";
        return 1;
    }
    if (!throws([&oqa,&u1]{oqa.set_unit(u1);})){
        std::cerr << "Did not throw an error trying to set a negative unit "
            "in line " << __LINE__ << " in file " << __FILE__ << "\n";
        return 1;
    }
    if (!throws([&oqa,&u2]{oqa.set_unit(u2);})){
        std::cerr << "Did not throw an error trying to set a zero unit "
            "in line " << __LINE__ << " in file " << __FILE__ << "\n";
        return 1;
    }

    /* Multiplication and division: */
    for (auto&& l : iqa){
        /* Multiplication: */
        auto v0 = l * u0;
        auto v1 = u0 * l;
        if (v0 != v1){
            std::cerr << "l * u0 != u0 * l\n"
                "   in line " << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }
        auto v2 = l * 2.0;
        auto v3 = 2.0 * l;
        if (v2 != v3){
            std::cerr << "l * u0 != u0 * l\n"
                "   in line " << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }

        /* Division: */
        auto v4 = l / u0;
        auto v5 = u0 / l;
        if (!is_close(static_cast<double>(v4 * v5), 1.0)){
            std::cerr << "(l / u0) * (u0 / l) != 1.0\n"
                "   in line " << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }
        auto v6 = l / 2.0;
        auto v7 = 2.0 / l;
        if (!is_close(static_cast<double>(v6 * v7), 1.0)){
            std::cerr << "(l / 2.0) * (2.0 / l) != 1.0\n"
                "   in line " << __LINE__ << " in file " << __FILE__ << "\n";
            return 1;
        }


    }

    /* Already in a static check as well. */
    constexpr static const char8_t* test = default_unit<si::permeability>;
    constexpr static const char8_t* compare_ptr = u8"m kg s^-2 A^-2";
    std::u8string ut(test);
    std::u8string compare(compare_ptr);

    if (ut != compare){
        std::string ts(ut.begin(), ut.end());
        std::string cs(compare.begin(), compare.end());
        std::cout << "comparison string '" << ts << "' is not the same as '"
            << cs << "'\n";
        return 1;
    }

    return 0;
}