// Copyright (c) 2022 Ultimaker B.V.
// CuraEngine is released under the terms of the AGPLv3 or higher

#include <cctype>
#include <fstream>
#include <regex> // regex parsing for temp flow graph
#include <sstream> // ostringstream
#include <stdio.h>
#include <string> //Parsing strings (stod, stoul).

#include "types/EnumSettingsT.h"
#include "utils/FMatrix4x3.h"
#include "utils/polygon.h"

#include "tools/string.h" //For Escaped.

#include "communication/slicecontext.h"

namespace cura52
{

    Settings::Settings()
    {
        parent = nullptr; // Needs to be properly initialised because we check against this if the parent is not set.
    }

    void Settings::add(const std::string& key, const std::string value)
    {
        if (settings.find(key) != settings.end()) // Already exists.
        {
            settings[key] = value;
        }
        else // New setting.
        {
            settings.emplace(key, value);
        }
    }

    template<>
    std::string Settings::get<std::string>(const std::string& key) const
    {
        // If this settings base has a setting value for it, look that up.
        if (settings.find(key) != settings.end())
        {
            return settings.at(key);
        }

        if (parent)
        {
            return parent->get<std::string>(key);
        }

        LOGE("Trying to retrieve setting with no value given: %s", key.c_str());
        return "";
    }

    template<>
    double Settings::get<double>(const std::string& key) const
    {
        return atof(get<std::string>(key).c_str());
    }

    template<>
    size_t Settings::get<size_t>(const std::string& key) const
    {
        return std::stoul(get<std::string>(key).c_str());
    }

    template<>
    int Settings::get<int>(const std::string& key) const
    {
        return atoi(get<std::string>(key).c_str());
    }

    template<>
    bool Settings::get<bool>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "on" || value == "yes" || value == "true" || value == "True")
        {
            return true;
        }
        const int num = atoi(value.c_str());
        return num != 0;
    }

    template<>
    ExtruderTrain& Settings::get<ExtruderTrain&>(const std::string& key) const
    {
        int extruder_nr = std::atoi(get<std::string>(key).c_str());
        if (extruder_nr < 0)
        {
            extruder_nr = get<size_t>("extruder_nr");
        }

        if (extruder_nr < 0 || extruder_nr >= application->extruderCount())
        {
            LOGI("Settings::get<ExtruderTrain&>  [%s] [%d]", key.c_str(), extruder_nr);
            extruder_nr = 0;
        }

        return application->extruders()[extruder_nr];
    }

    template<>
    LayerIndex Settings::get<LayerIndex>(const std::string& key) const
    {
        return std::atoi(get<std::string>(key).c_str()) - 1; // For the user we display layer numbers starting from 1, but we start counting from 0. Still it may be negative for Raft layers.
    }

    template<>
    coord_t Settings::get<coord_t>(const std::string& key) const
    {
        return MM2INT(get<double>(key)); // The settings are all in millimetres, but we need to interpret them as microns.
    }

    template<>
    AngleRadians Settings::get<AngleRadians>(const std::string& key) const
    {
        return get<double>(key) * M_PI / 180; // The settings are all in degrees, but we need to interpret them as radians.
    }

    template<>
    AngleDegrees Settings::get<AngleDegrees>(const std::string& key) const
    {
        return get<double>(key);
    }

    template<>
    Temperature Settings::get<Temperature>(const std::string& key) const
    {
        return get<double>(key);
    }

    template<>
    Velocity Settings::get<Velocity>(const std::string& key) const
    {
        return get<double>(key);
    }

    template<>
    Ratio Settings::get<Ratio>(const std::string& key) const
    {
        return get<double>(key) / 100.0; // The settings are all in percentages, but we need to interpret them as radians.
    }

    template<>
    Duration Settings::get<Duration>(const std::string& key) const
    {
        return get<double>(key);
    }

    template<>
    DraftShieldHeightLimitation Settings::get<DraftShieldHeightLimitation>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "limited")
        {
            return DraftShieldHeightLimitation::LIMITED;
        }
        else // if (value == "full") or default.
        {
            return DraftShieldHeightLimitation::FULL;
        }
    }

    template<>
    FlowTempGraph Settings::get<FlowTempGraph>(const std::string& key) const
    {
        std::string value_string = get<std::string>(key);

        FlowTempGraph result;
        if (value_string.empty())
        {
            return result; // Empty at this point.
        }
        /* Match with:
         * - the last opening bracket '['
         * - then a bunch of characters until the first comma
         * - a comma
         * - a bunch of characters until the first closing bracket ']'.
         * This matches with any substring which looks like "[ 124.512 , 124.1 ]".
         */
        std::regex regex("(\\[([^,\\[]*),([^,\\]]*)\\])");

        // Default constructor = end-of-sequence:
        std::regex_token_iterator<std::string::iterator> rend;

        const int submatches[] = { 1, 2, 3 }; // Match whole pair, first number and second number of a pair.
        std::regex_token_iterator<std::string::iterator> match_iter(value_string.begin(), value_string.end(), regex, submatches);
        while (match_iter != rend)
        {
            match_iter++; // Match the whole pair.
            if (match_iter == rend)
            {
                break;
            }
            std::string first_substring = *match_iter++;
            std::string second_substring = *match_iter++;
            try
            {
                double first = std::stod(first_substring);
                double second = std::stod(second_substring);
                result.data.emplace_back(first, second);
            }
            catch (const std::invalid_argument& e)
            {
                (void)e;
                LOGE("Couldn't read 2D graph element [{%s},{%s}] in setting {%s}. Ignored.", first_substring.c_str(), second_substring.c_str(), key.c_str());
            }
        }

        return result;
    }

    template<>
    FMatrix4x3 Settings::get<FMatrix4x3>(const std::string& key) const
    {
        const std::string value_string = get<std::string>(key);

        FMatrix4x3 result;
        if (value_string.empty())
        {
            return result; // Standard matrix ([[1,0,0], [0,1,0], [0,0,1]]).
        }

        std::string num("([^,\\] ]*)"); // Match with anything but the next ',' ']' or space and capture the match.
        std::ostringstream row; // Match with "[num,num,num]" and ignore whitespace.
        row << "\\s*\\[\\s*" << num << "\\s*,\\s*" << num << "\\s*,\\s*" << num << "\\s*\\]\\s*";
        std::ostringstream matrix; // Match with "[row,row,row]" and ignore whitespace.
        matrix << "\\s*\\[\\s*" << row.str() << "\\s*,\\s*" << row.str() << "\\s*,\\s*" << row.str() << "\\]\\s*";

        std::regex point_matrix_regex(matrix.str());
        std::cmatch sub_matches; // Same as std::match_results<const char*> cm;
        std::regex_match(value_string.c_str(), sub_matches, point_matrix_regex);
        if (sub_matches.size() != 10) // One match for the whole string, nine for the cells.
        {
            LOGW("Mesh transformation matrix could not be parsed!");
            LOGD("Format should be [[f,f,f], [f,f,f], [f,f,f]] allowing whitespace anywhere in between. While what was given was {}", value_string.c_str());
            return result; // Standard matrix ([[1,0,0], [0,1,0], [0,0,1]]).
        }

        size_t sub_match_index = 1; // Skip the first because the first submatch is the whole string.
        for (size_t x = 0; x < 3; x++)
        {
            for (size_t y = 0; y < 3; y++)
            {
                std::sub_match<const char*> sub_match = sub_matches[sub_match_index];
                result.m[y][x] = strtod(std::string(sub_match.str()).c_str(), nullptr);
                sub_match_index++;
            }
        }

        return result;
    }

    template<>
    EGCodeFlavor Settings::get<EGCodeFlavor>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        // I wish that switch statements worked for std::string...
        if (value == "Griffin")
        {
            return EGCodeFlavor::GRIFFIN;
        }
        else if (value == "UltiGCode")
        {
            return EGCodeFlavor::ULTIGCODE;
        }
        else if (value == "Makerbot")
        {
            return EGCodeFlavor::MAKERBOT;
        }
        else if (value == "BFB")
        {
            return EGCodeFlavor::BFB;
        }
        else if (value == "MACH3")
        {
            return EGCodeFlavor::MACH3;
        }
        else if (value == "RepRap (Volumetric)")
        {
            return EGCodeFlavor::MARLIN_VOLUMATRIC;
        }
        else if (value == "Repetier")
        {
            return EGCodeFlavor::REPETIER;
        }
        else if (value == "RepRap (RepRap)")
        {
            return EGCodeFlavor::REPRAP;
        }
        else if (value == "Plc")
        {
            return EGCodeFlavor::PLC;
        }
        else if (value == "MACH3(Creality)")
        {
            return EGCodeFlavor::MACH3_Creality;
        }
        else if (value == "4408")
        {
            return EGCodeFlavor::Creality_OS;
        }
        // Default:
        return EGCodeFlavor::MARLIN;
    }

    template<>
    EFillMethod Settings::get<EFillMethod>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "lines")
        {
            return EFillMethod::LINES;
        }
        if (value == "aligned_rectilinear")
        {
            return EFillMethod::ALIGNLINES;
        }
        else if (value == "grid")
        {
            return EFillMethod::GRID;
        }
        else if (value == "cubic")
        {
            return EFillMethod::CUBIC;
        }
        else if (value == "cubicsubdiv")
        {
            return EFillMethod::CUBICSUBDIV;
        }
        else if (value == "tetrahedral")
        {
            return EFillMethod::TETRAHEDRAL;
        }
        else if (value == "quarter_cubic")
        {
            return EFillMethod::QUARTER_CUBIC;
        }
        else if (value == "triangles")
        {
            return EFillMethod::TRIANGLES;
        }
        else if (value == "trihexagon")
        {
            return EFillMethod::TRIHEXAGON;
        }
        else if (value == "concentric")
        {
            return EFillMethod::CONCENTRIC;
        }
        else if (value == "zigzag")
        {
            return EFillMethod::ZIG_ZAG;
        }
        else if (value == "cross")
        {
            return EFillMethod::CROSS;
        }
        else if (value == "cross_3d")
        {
            return EFillMethod::CROSS_3D;
        }
        else if (value == "gyroid")
        {
            return EFillMethod::GYROID;
        }
        else if (value == "lightning")
        {
            return EFillMethod::LIGHTNING;
        }
        else if (value == "honeycomb")
        {
            return EFillMethod::HONEYCOMB;
        }
        else // Default.
        {
            return EFillMethod::NONE;
        }
    }

    template<>
    EPlatformAdhesion Settings::get<EPlatformAdhesion>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "brim")
        {
            return EPlatformAdhesion::BRIM;
        }
        else if (value == "autobrim")
        {
            return EPlatformAdhesion::AUTOBRIM;
        }
        else if (value == "raft")
        {
            return EPlatformAdhesion::RAFT;
        }
        else if (value == "simpleraft")
        {
            return EPlatformAdhesion::SIMPLERAFT;
        }
        else if (value == "none")
        {
            return EPlatformAdhesion::NONE;
        }
        else if (value == "lace")
        {
            return EPlatformAdhesion::LACE;
        }
        else //Default.
        {
            return EPlatformAdhesion::SKIRT;
        }
    }

    template<>
    ESupportType Settings::get<ESupportType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "everywhere")
        {
            return ESupportType::EVERYWHERE;
        }
        else if (value == "buildplate")
        {
            return ESupportType::PLATFORM_ONLY;
        }
        else // Default.
        {
            return ESupportType::NONE;
        }
    }

    template<>
    PrimeTowerType Settings::get<PrimeTowerType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "normal")
        {
            return PrimeTowerType::NORMAL;
        }
        else // Default.
        {
            return PrimeTowerType::SINGLE;
        }
    }


    template<>
    RoutePlanning Settings::get<RoutePlanning>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "to_and_fro")
        {
            return RoutePlanning::TOANDFRO;
        }
        else // Default.
        {
            return RoutePlanning::NONE;
        }
    }


    template<>
    RetractionHopType Settings::get<RetractionHopType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "directlift")
        {
            return RetractionHopType::DIRECTLIFT;
        }
        else if (value == "spirallift")
        {
            return RetractionHopType::SPIRALLIFT;
        }
        else//trapezoidalleft
        {
            return RetractionHopType::TRAPEZOIDALLEFT;
        }
    }


    template<>
    ESupportStructure Settings::get<ESupportStructure>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "normal")
        {
            return ESupportStructure::NORMAL;
        }
        else if (value == "tree")
        {
            return ESupportStructure::TREE;
        }
        else if (value == "thomastree")
        {
            return ESupportStructure::THOMASTREE;
        }
        else if (value == "normal_manual")
        {
            return ESupportStructure::NORMAL_MANUAL;
        }
        else if (value == "thomastree_manual")
        {
            return ESupportStructure::THOMASTREE_MANUAL;
        }
        else // Default.
        {
            return ESupportStructure::NORMAL;
        }
    }


    template<>
    EZSeamType Settings::get<EZSeamType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "random")
        {
            return EZSeamType::RANDOM;
        }
        else if (value == "back" || value == "user_specified") // It's called 'back' internally because originally this was intended to allow the user to put the seam in the back of the object where it's less visible.
        {
            return EZSeamType::USER_SPECIFIED;
        }
        else if (value == "sharpest_corner")
        {
            return EZSeamType::SHARPEST_CORNER;
        }
        else // Default.
        {
            return EZSeamType::SHORTEST;
        }
    }

    template<>
    EZSeamCornerPrefType Settings::get<EZSeamCornerPrefType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "z_seam_corner_inner")
        {
            return EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_INNER;
        }
        else if (value == "z_seam_corner_outer")
        {
            return EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_OUTER;
        }
        else if (value == "z_seam_corner_any")
        {
            return EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_ANY;
        }
        else if (value == "z_seam_corner_weighted")
        {
            return EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_WEIGHTED;
        }
        else // Default.
        {
            return EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE;
        }
    }

    template<>
    ESurfaceMode Settings::get<ESurfaceMode>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "surface")
        {
            return ESurfaceMode::SURFACE;
        }
        else if (value == "both")
        {
            return ESurfaceMode::BOTH;
        }
        else // Default.
        {
            return ESurfaceMode::NORMAL;
        }
    }

    template<>
    FillPerimeterGapMode Settings::get<FillPerimeterGapMode>(const std::string& key) const
    {
        if (get<std::string>(key) == "everywhere")
        {
            return FillPerimeterGapMode::EVERYWHERE;
        }
        else // Default.
        {
            return FillPerimeterGapMode::NOWHERE;
        }
    }

    template<>
    BuildPlateShape Settings::get<BuildPlateShape>(const std::string& key) const
    {
        if (get<std::string>(key) == "elliptic")
        {
            return BuildPlateShape::ELLIPTIC;
        }
        else // Default.
        {
            return BuildPlateShape::RECTANGULAR;
        }
    }

    template<>
    cura54::ESupportType Settings::get<cura54::ESupportType>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "everywhere")
        {
            return cura54::ESupportType::EVERYWHERE;
        }
        else if (value == "buildplate")
        {
            return cura54::ESupportType::PLATFORM_ONLY;
        }
        else // Default.
        {
            return cura54::ESupportType::NONE;
        }
    }


    template<>
    cura54::ESurfaceMode Settings::get<cura54::ESurfaceMode>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "surface")
        {
            return cura54::ESurfaceMode::SURFACE;
        }
        else if (value == "both")
        {
            return cura54::ESurfaceMode::BOTH;
        }
        else // Default.
        {
            return cura54::ESurfaceMode::NORMAL;
        }
    }

    template<>
    cura54::SupportDistPriority Settings::get<cura54::SupportDistPriority>(const std::string& key) const
    {
        if (get<std::string>(key) == "z_overrides_xy")
        {
            return cura54::SupportDistPriority::Z_OVERRIDES_XY;
        }
        else // Default.
        {
            return cura54::SupportDistPriority::XY_OVERRIDES_Z;
        }
    }

    template<> Polygons Settings::get<Polygons>(const std::string& key) const
    {
        std::string value_string = get<std::string>(key);

        Polygons result;
        if (value_string.empty())
        {
            return result; //Empty at this point.
        }
        /* We're looking to match one or more floating point values separated by
         * commas and surrounded by square brackets. Note that because the QML
         * RegExpValidator only stops unrecognised characters being input and
         * doesn't actually barf if the trailing ']' is missing, we are lenient here
         * and make that bracket optional.
         */
        std::regex polygons_regex(R"(\[(.*)\]?)");
        std::smatch polygons_match;
        if (std::regex_search(value_string, polygons_match, polygons_regex) && polygons_match.size() > 1)
        {
            std::string polygons_string = polygons_match.str(1);

            std::regex polygon_regex(R"(\[((\[[^\[\]]*\]\s*,?\s*)*)\]\s*,?)"); // matches with a list of lists (a list of 2D vertices)
            std::smatch polygon_match;

            std::regex_token_iterator<std::string::iterator> rend; //Default constructor gets the end-of-sequence iterator.
            std::regex_token_iterator<std::string::iterator> polygon_match_iter(polygons_string.begin(), polygons_string.end(), polygon_regex, 0);
            while (polygon_match_iter != rend)
            {
                std::string polygon_str = *polygon_match_iter++;

                result.emplace_back();
                PolygonRef poly = result.back();

                std::regex point2D_regex(R"(\[([^,\[]*),([^,\]]*)\])"); // matches to a list of exactly two things

                const int submatches[] = { 1, 2 }; // Match first number and second number of a pair.
                std::regex_token_iterator<std::string::iterator> match_iter(polygon_str.begin(), polygon_str.end(), point2D_regex, submatches);
                while (match_iter != rend)
                {
                    std::string first_substring = *match_iter++;
                    std::string second_substring = *match_iter++;
                    try
                    {
                        double first = std::stod(first_substring);
                        double second = std::stod(second_substring);
                        poly.emplace_back(MM2INT(first), MM2INT(second));
                    }
                    catch (const std::invalid_argument& e)
                    {
                        (void)e;
                        LOGE("Couldn't read 2D graph element [{%s},{%s}] in setting '{%s}'. Ignored.\n", first_substring.c_str(), second_substring.c_str(), key.c_str());
                    }
                    if (match_iter == rend)
                    {
                        break;
                    }
                }
            }
        }
        return result;
    }

    template<>
    CombingMode Settings::get<CombingMode>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "off")
        {
            return CombingMode::OFF;
        }
        else if (value == "noskin")
        {
            return CombingMode::NO_SKIN;
        }
        else if (value == "no_outer_surfaces")
        {
            return CombingMode::NO_OUTER_SURFACES;
        }
        else if (value == "infill")
        {
            return CombingMode::INFILL;
        }
        else // Default.
        {
            return CombingMode::ALL;
        }
    }

    template<>
    SupportDistPriority Settings::get<SupportDistPriority>(const std::string& key) const
    {
        if (get<std::string>(key) == "z_overrides_xy")
        {
            return SupportDistPriority::Z_OVERRIDES_XY;
        }
        else // Default.
        {
            return SupportDistPriority::XY_OVERRIDES_Z;
        }
    }

    template<>
    SlicingTolerance Settings::get<SlicingTolerance>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "inclusive")
        {
            return SlicingTolerance::INCLUSIVE;
        }
        else if (value == "exclusive")
        {
            return SlicingTolerance::EXCLUSIVE;
        }
        else // Default.
        {
            return SlicingTolerance::MIDDLE;
        }
    }

    template<>
    InsetDirection Settings::get<InsetDirection>(const std::string& key) const
    {
        const std::string& value = get<std::string>(key);
        if (value == "outside_in")
        {
            return InsetDirection::OUTSIDE_IN;
        }
        if (value == "flexible_sequence")
        {
            return InsetDirection::Flexible_Sequence;
        }
        else // Default.
        {
            return InsetDirection::INSIDE_OUT;
        }
    }

    template<>
    std::vector<double> Settings::get<std::vector<double>>(const std::string& key) const
    {
        const std::string& value_string = get<std::string>(key);

        std::vector<double> result;
        if (value_string.empty())
        {
            return result;
        }

        /* We're looking to match one or more floating point values separated by
         * commas and surrounded by square brackets. Note that because the QML
         * RegExpValidator only stops unrecognised characters being input and
         * doesn't actually barf if the trailing ']' is missing, we are lenient here
         * and make that bracket optional.
         */
        std::regex list_contents_regex(R"(\[([^\]]*)\]?)");
        std::smatch list_contents_match;
        if (std::regex_search(value_string, list_contents_match, list_contents_regex) && list_contents_match.size() > 1)
        {
            std::string elements = list_contents_match.str(1);
            std::regex element_regex(R"(\s*([+-]?[0-9]*\.?[0-9]+)\s*,?)");
            std::regex_token_iterator<std::string::iterator> rend; // Default constructor gets the end-of-sequence iterator.

            std::regex_token_iterator<std::string::iterator> match_iter(elements.begin(), elements.end(), element_regex, 0);
            while (match_iter != rend)
            {
                std::string value = *match_iter++;
                try
                {
                    result.push_back(std::stod(value));
                }
                catch (const std::invalid_argument& e)
                {
                    (void)e;
                    LOGE("Couldn't read floating point value ({%s}) in setting {%s}. Ignored.", value.c_str(), key.c_str());
                }
            }
        }
        return result;
    }

    template<>
    std::vector<int> Settings::get<std::vector<int>>(const std::string& key) const
    {
        std::vector<double> values_doubles = get<std::vector<double>>(key);
        std::vector<int> values_ints;
        values_ints.reserve(values_doubles.size());
        for (double value : values_doubles)
        {
            values_ints.push_back(std::round(value)); // Round to nearest integer.
        }
        return values_ints;
    }

    template<>
    std::vector<AngleDegrees> Settings::get<std::vector<AngleDegrees>>(const std::string& key) const
    {
        std::vector<double> values_doubles = get<std::vector<double>>(key);
        return std::vector<AngleDegrees>(values_doubles.begin(), values_doubles.end()); // Cast them to AngleDegrees.
    }

    const std::string Settings::getAllSettingsString() const
    {
        std::stringstream sstream;
        for (const std::pair<std::string, std::string> pair : settings)
        {
            char buffer[4096];
            snprintf(buffer, 4096, " -s %s=\"%s\"", pair.first.c_str(), Escaped{ pair.second.c_str() }.str);
            sstream << buffer;
        }
        return sstream.str();
    }

    bool Settings::has(const std::string& key) const
    {
        if (settings.find(key) != settings.end())
            return true;

        if (parent)
            return parent->has(key);
        return false;
    }

    void Settings::setParent(Settings* new_parent)
    {
        parent = new_parent;
    }

    std::string Settings::getWithoutLimiting(const std::string& key) const
    {
        if (settings.find(key) != settings.end())
        {
            return settings.at(key);
        }
        else if (parent)
        {
            return parent->get<std::string>(key);
        }
        else
        {
            LOGE("Trying to retrieve setting with no value given: {}", key.c_str());
            std::exit(2);
        }
    }

} // namespace cura52
