#include <fesa/io/abaqus/parser.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <fstream>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace fesa {
namespace {

enum class Scope { global, part, assembly, instance };

struct KeywordLine final {
    std::string keyword;
    std::map<std::string, std::string, std::less<>> parameters;
    SourceLocation source;
};

std::string_view trim(const std::string_view value) {
    constexpr std::string_view whitespace{" \t\f\v\r\n"};
    const std::size_t first = value.find_first_not_of(whitespace);
    if (first == std::string_view::npos) {
        return {};
    }
    const std::size_t last = value.find_last_not_of(whitespace);
    return value.substr(first, last - first + 1);
}

std::string uppercase_ascii(std::string value) {
    std::ranges::transform(value, value.begin(), [](const char character) {
        if (character >= 'a' && character <= 'z') {
            return static_cast<char>(character - 'a' + 'A');
        }
        return character;
    });
    return value;
}

std::vector<std::string> split_fields(const std::string_view value) {
    std::vector<std::string> fields;
    std::size_t first = 0;
    while (true) {
        const std::size_t comma = value.find(',', first);
        const std::string_view field = comma == std::string_view::npos
                                           ? value.substr(first)
                                           : value.substr(first, comma - first);
        fields.emplace_back(trim(field));
        if (comma == std::string_view::npos) {
            break;
        }
        first = comma + 1;
    }
    return fields;
}

ParseDeckResult failure(
    const DiagnosticStage stage,
    std::string code,
    std::string message,
    std::optional<SourceLocation> source) {
    std::vector<Diagnostic> diagnostics;
    diagnostics.push_back({
        stage,
        Severity::error,
        std::move(code),
        std::move(message),
        std::move(source),
    });
    return {std::nullopt, std::move(diagnostics)};
}

ParseDeckResult syntax_failure(
    std::string code,
    std::string message,
    SourceLocation source) {
    return failure(
        DiagnosticStage::syntax,
        std::move(code),
        std::move(message),
        std::move(source));
}

bool is_supported_record(const std::string_view keyword) {
    constexpr std::array supported{
        std::string_view{"BEAM GENERAL SECTION"},
        std::string_view{"BOUNDARY"},
        std::string_view{"CLOAD"},
        std::string_view{"ELASTIC"},
        std::string_view{"ELEMENT"},
        std::string_view{"ELSET"},
        std::string_view{"END STEP"},
        std::string_view{"MATERIAL"},
        std::string_view{"NODE"},
        std::string_view{"NSET"},
        std::string_view{"STATIC"},
        std::string_view{"STEP"},
        std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
    };
    return std::ranges::find(supported, keyword) != supported.end();
}

std::optional<KeywordLine> parse_keyword_line(
    const std::string_view line,
    const SourceLocation& source,
    ParseDeckResult& error) {
    const std::vector<std::string> fields = split_fields(line.substr(1));
    if (fields.empty() || fields[0].empty()) {
        error = syntax_failure(
            "abaqus.syntax.empty_keyword",
            "Abaqus keyword name is empty.",
            source);
        return std::nullopt;
    }

    KeywordLine parsed{
        uppercase_ascii(fields[0]),
        {},
        source,
    };
    for (std::size_t index = 1; index < fields.size(); ++index) {
        const std::string_view field = fields[index];
        if (field.empty()) {
            continue;
        }

        const std::size_t equals = field.find('=');
        const std::string_view key_text =
            trim(field.substr(0, equals));
        const std::string_view value_text =
            equals == std::string_view::npos
                ? std::string_view{}
                : trim(field.substr(equals + 1));
        if (key_text.empty()) {
            error = syntax_failure(
                "abaqus.syntax.invalid_parameter",
                "Abaqus keyword parameter name is empty.",
                source);
            return std::nullopt;
        }

        const std::string key = uppercase_ascii(std::string{key_text});
        if (!parsed.parameters.emplace(key, value_text).second) {
            error = syntax_failure(
                "abaqus.syntax.duplicate_parameter",
                "Abaqus keyword parameter '" + key +
                    "' is specified more than once.",
                source);
            return std::nullopt;
        }
    }
    return parsed;
}

const std::string* parameter(
    const KeywordLine& keyword,
    const std::string_view name) {
    const auto found = keyword.parameters.find(name);
    return found == keyword.parameters.end() ? nullptr : &found->second;
}

ParseDeckResult missing_parameter(
    const KeywordLine& keyword,
    const std::string_view parameter_name) {
    return syntax_failure(
        "abaqus.syntax.missing_parameter",
        "Abaqus *" + keyword.keyword + " requires parameter " +
            std::string{parameter_name} + ".",
        keyword.source);
}

}  // namespace

ParseDeckResult parse_deck(const std::filesystem::path& path) {
    std::ifstream input{path, std::ios::binary};
    if (!input) {
        return failure(
            DiagnosticStage::io,
            "abaqus.io.open_failed",
            "Unable to open Abaqus input file.",
            SourceLocation{path, 0U, 0U});
    }

    ParsedDeck deck;
    Scope scope = Scope::global;
    std::optional<ParsedPart> current_part;
    std::optional<ParsedAssembly> current_assembly;
    std::optional<ParsedInstance> current_instance;
    DeckRecord* current_record = nullptr;

    std::string line;
    std::size_t line_number = 0;
    while (std::getline(input, line)) {
        ++line_number;
        if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
            line.erase(0, 3);
        }

        const std::size_t first_nonspace =
            line.find_first_not_of(" \t\f\v\r");
        if (first_nonspace == std::string::npos) {
            continue;
        }
        const std::string_view content = trim(line);
        if (content.starts_with("**")) {
            continue;
        }

        if (!content.starts_with('*')) {
            const std::vector<std::string> fields = split_fields(content);
            if (scope == Scope::instance) {
                current_instance->transform_data.push_back(fields);
            } else if (current_record != nullptr) {
                current_record->data.push_back(fields);
            } else {
                return syntax_failure(
                    "abaqus.syntax.data_without_keyword",
                    "Abaqus data line has no preceding keyword record.",
                    SourceLocation{path, line_number, first_nonspace + 1U});
            }
            continue;
        }

        const SourceLocation source{
            path,
            line_number,
            first_nonspace + 1U,
        };
        ParseDeckResult keyword_error;
        std::optional<KeywordLine> parsed =
            parse_keyword_line(content, source, keyword_error);
        if (!parsed.has_value()) {
            return keyword_error;
        }
        KeywordLine& keyword = *parsed;
        current_record = nullptr;

        if (keyword.keyword == "PART") {
            if (scope != Scope::global) {
                return syntax_failure(
                    "abaqus.syntax.invalid_part_scope",
                    "*PART is only valid in global input scope.",
                    source);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            current_part = ParsedPart{*name, {}, source};
            scope = Scope::part;
            continue;
        }

        if (keyword.keyword == "END PART") {
            if (scope != Scope::part || !current_part.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_part",
                    "*END PART does not match an open *PART.",
                    source);
            }
            deck.parts.push_back(std::move(*current_part));
            current_part.reset();
            scope = Scope::global;
            continue;
        }

        if (keyword.keyword == "ASSEMBLY") {
            if (scope != Scope::global) {
                return syntax_failure(
                    "abaqus.syntax.invalid_assembly_scope",
                    "*ASSEMBLY is only valid in global input scope.",
                    source);
            }
            if (deck.assembly.has_value() ||
                current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.multiple_assemblies",
                    "ParsedDeck can preserve only one *ASSEMBLY.",
                    source);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            current_assembly = ParsedAssembly{*name, {}, {}, source};
            scope = Scope::assembly;
            continue;
        }

        if (keyword.keyword == "END ASSEMBLY") {
            if (scope != Scope::assembly ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_assembly",
                    "*END ASSEMBLY does not match an open *ASSEMBLY.",
                    source);
            }
            deck.assembly = std::move(*current_assembly);
            current_assembly.reset();
            scope = Scope::global;
            continue;
        }

        if (keyword.keyword == "INSTANCE") {
            if (scope != Scope::assembly ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.invalid_instance_scope",
                    "*INSTANCE is only valid in an open *ASSEMBLY.",
                    source);
            }
            const std::string* name = parameter(keyword, "NAME");
            if (name == nullptr || name->empty()) {
                return missing_parameter(keyword, "NAME");
            }
            const std::string* part_name = parameter(keyword, "PART");
            if (part_name == nullptr || part_name->empty()) {
                return missing_parameter(keyword, "PART");
            }
            current_instance =
                ParsedInstance{*name, *part_name, {}, source};
            scope = Scope::instance;
            continue;
        }

        if (keyword.keyword == "END INSTANCE") {
            if (scope != Scope::instance ||
                !current_instance.has_value() ||
                !current_assembly.has_value()) {
                return syntax_failure(
                    "abaqus.syntax.unexpected_end_instance",
                    "*END INSTANCE does not match an open *INSTANCE.",
                    source);
            }
            current_assembly->instances.push_back(
                std::move(*current_instance));
            current_instance.reset();
            scope = Scope::assembly;
            continue;
        }

        if (!is_supported_record(keyword.keyword)) {
            return syntax_failure(
                "abaqus.unsupported_keyword",
                "Unsupported Abaqus keyword *" + keyword.keyword + ".",
                source);
        }

        DeckRecord next_record{
            std::move(keyword.keyword),
            std::move(keyword.parameters),
            {},
            source,
        };
        switch (scope) {
            case Scope::global:
                deck.global_records.push_back(std::move(next_record));
                current_record = &deck.global_records.back();
                break;
            case Scope::part:
                current_part->records.push_back(std::move(next_record));
                current_record = &current_part->records.back();
                break;
            case Scope::assembly:
                current_assembly->records.push_back(
                    std::move(next_record));
                current_record = &current_assembly->records.back();
                break;
            case Scope::instance:
                return syntax_failure(
                    "abaqus.syntax.instance_local_keyword",
                    "Keyword records inside *INSTANCE are unsupported.",
                    source);
        }
    }

    if (input.bad()) {
        return failure(
            DiagnosticStage::io,
            "abaqus.io.read_failed",
            "Failed while reading Abaqus input file.",
            SourceLocation{path, line_number, 0U});
    }

    if (current_instance.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_instance",
            "Abaqus *INSTANCE is not closed by *END INSTANCE.",
            current_instance->source);
    }
    if (current_part.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_part",
            "Abaqus *PART is not closed by *END PART.",
            current_part->source);
    }
    if (current_assembly.has_value()) {
        return syntax_failure(
            "abaqus.syntax.unclosed_assembly",
            "Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
            current_assembly->source);
    }

    return {std::move(deck), {}};
}

}  // namespace fesa
