#include "fesa/io/abaqus/input_reader.hpp"

#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>

namespace fesa {
namespace {

std::filesystem::path normalizedPath(const std::filesystem::path& path) {
    std::error_code error;
    const auto absolute = std::filesystem::absolute(path, error);
    return (error ? path : absolute).lexically_normal();
}

bool isAsciiWhitespace(char value) noexcept {
    return value == ' ' || value == '\t' || value == '\r' ||
           value == '\n' || value == '\f' || value == '\v';
}

std::string trim(std::string_view text) {
    while (!text.empty() && isAsciiWhitespace(text.front())) {
        text.remove_prefix(1U);
    }
    while (!text.empty() && isAsciiWhitespace(text.back())) {
        text.remove_suffix(1U);
    }
    return std::string{text};
}

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

std::vector<std::string> splitFields(std::string_view line) {
    std::vector<std::string> fields;
    std::size_t fieldStart = 0U;
    while (true) {
        const std::size_t separator = line.find(',', fieldStart);
        if (separator == std::string_view::npos) {
            fields.push_back(trim(line.substr(fieldStart)));
            break;
        }
        fields.push_back(trim(line.substr(fieldStart, separator - fieldStart)));
        fieldStart = separator + 1U;
    }
    return fields;
}

std::string contentIdentity(const std::string& bytes) {
    constexpr std::uint64_t offsetBasis = 14695981039346656037ULL;
    constexpr std::uint64_t prime = 1099511628211ULL;
    std::uint64_t hash = offsetBasis;
    // Hash the binary input before CRLF handling so provenance follows the
    // exact file bytes rather than a normalized text representation.
    for (const unsigned char byte : bytes) {
        hash ^= static_cast<std::uint64_t>(byte);
        hash *= prime;
    }

    std::ostringstream formatted;
    formatted << "fnv1a64:" << std::hex << std::setfill('0')
              << std::setw(16) << hash;
    return formatted.str();
}

Result<ParsedInput> failure(
    const std::filesystem::path& sourcePath,
    std::size_t line,
    std::string code,
    std::string keyword,
    std::string message) {
    Diagnostic diagnostic{
        Severity::error,
        std::move(code),
        {sourcePath, line},
        std::move(keyword),
        "",
        std::move(message)};
    return Result<ParsedInput>::failure(Status::failure(
        FailureCategory::input, {std::move(diagnostic)}));
}

} // namespace

Result<ParsedInput> AbaqusInputReader::read(
    const std::filesystem::path& inputPath) const {
    const auto sourcePath = normalizedPath(inputPath);
    std::ifstream stream{sourcePath, std::ios::binary};
    if (!stream) {
        return failure(
            sourcePath,
            0U,
            "input-file-unreadable",
            "",
            "The Abaqus input file could not be opened for reading.");
    }

    const std::string bytes{
        std::istreambuf_iterator<char>{stream},
        std::istreambuf_iterator<char>{}};
    if (stream.bad()) {
        return failure(
            sourcePath,
            0U,
            "input-file-unreadable",
            "",
            "The Abaqus input file could not be read completely.");
    }

    ParsedInput parsed{sourcePath, contentIdentity(bytes), {}};
    std::size_t lineStart = 0U;
    std::size_t lineNumber = 1U;
    while (lineStart < bytes.size()) {
        const std::size_t newline = bytes.find('\n', lineStart);
        const std::size_t lineEnd =
            newline == std::string::npos ? bytes.size() : newline;
        std::string originalLine = bytes.substr(lineStart, lineEnd - lineStart);
        if (!originalLine.empty() && originalLine.back() == '\r') {
            originalLine.pop_back();
        }

        const std::string trimmedLine = trim(originalLine);
        if (!trimmedLine.empty() && trimmedLine.rfind("**", 0U) != 0U) {
            if (trimmedLine.front() == '*') {
                const auto fields = splitFields(trimmedLine);
                const std::string keywordText =
                    fields.empty() ? std::string{} : trim(
                        std::string_view{fields[0]}.substr(1U));
                if (keywordText.empty()) {
                    return failure(
                        sourcePath,
                        lineNumber,
                        "malformed-keyword",
                        trimmedLine,
                        "A keyword line requires a non-empty keyword name.");
                }

                KeywordBlock block{
                    uppercaseAscii(keywordText),
                    originalLine,
                    {},
                    {},
                    {sourcePath, lineNumber}};
                for (std::size_t index = 1U; index < fields.size(); ++index) {
                    const std::string& field = fields[index];
                    if (field.empty()) {
                        return failure(
                            sourcePath,
                            lineNumber,
                            "malformed-keyword",
                            block.canonicalName,
                            "A keyword parameter name cannot be empty.");
                    }

                    const std::size_t equals = field.find('=');
                    const std::string parameterName = trim(std::string_view{field}.substr(
                        0U, equals));
                    if (parameterName.empty()) {
                        return failure(
                            sourcePath,
                            lineNumber,
                            "malformed-keyword",
                            block.canonicalName,
                            "A keyword parameter name cannot be empty.");
                    }

                    KeywordParameter parameter{
                        uppercaseAscii(parameterName), std::nullopt};
                    if (equals != std::string::npos) {
                        parameter.value = trim(
                            std::string_view{field}.substr(equals + 1U));
                    }
                    block.parameters.push_back(std::move(parameter));
                }
                parsed.blocks.push_back(std::move(block));
            } else {
                if (parsed.blocks.empty()) {
                    return failure(
                        sourcePath,
                        lineNumber,
                        "orphan-data-line",
                        "",
                        "A data line must follow a keyword line.");
                }
                parsed.blocks.back().data.push_back(
                    {splitFields(originalLine), {sourcePath, lineNumber}});
            }
        }

        if (newline == std::string::npos) {
            break;
        }
        lineStart = newline + 1U;
        ++lineNumber;
    }

    return Result<ParsedInput>::success(std::move(parsed));
}

} // namespace fesa
