/*
 * Sentence Counter (C++20)
 * ========================
 * Counts sentences in text using rule-based boundary detection.
 *
 * Rules for identifying end of sentence:
 * 1. Sentences end with '.', '!', or '?'
 * 2. Ellipsis ('...' or unicode '…') is NOT a sentence terminator
 * 3. Abbreviations (Mr., Mrs., Dr., etc.) do NOT end a sentence
 * 4. Decimal numbers (3.14) do NOT end a sentence
 * 5. Initials (J. K. Rowling) do NOT end a sentence
 * 6. Quoted sentence endings count (He said, "Go!")
 * 7. Multiple terminators ('?!' or '!!!') count as ONE sentence end
 * 8. URLs and emails with dots do NOT end a sentence
 *
 * Compile: g++ -std=c++20 -O2 -o sentence_counter sentence_counter.cpp
 */

#include <algorithm>
#include <format>
#include <iostream>
#include <regex>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>

// ======= Abbreviation set =======

static const std::unordered_set<std::string> abbreviations = {
    "mr", "mrs", "ms", "dr", "prof", "sr", "jr", "st", "ave", "blvd",
    "gen", "gov", "sgt", "cpl", "pvt", "capt", "lt", "col", "maj",
    "rev", "hon", "pres", "vs", "etc", "approx", "dept", "est",
    "vol", "fig", "inc", "corp", "ltd", "co", "no", "al", "ed",
    "jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "oct",
    "nov", "dec", "mon", "tue", "wed", "thu", "fri", "sat", "sun",
    "am", "pm", "eg", "ie", "us", "uk", "phd",
};

// ======= Helper: normalise whitespace =======

static std::string normalise_whitespace(std::string_view sv)
{
    std::string result;
    result.reserve(sv.size());
    bool in_space = true;                       // collapse leading space too
    for (char c : sv) {
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
            if (!in_space) { result += ' '; in_space = true; }
        } else {
            result += c;
            in_space = false;
        }
    }
    // trim trailing space
    if (!result.empty() && result.back() == ' ') result.pop_back();
    return result;
}

// ======== Helper: regex_replace with callback ========
// std::regex_replace doesn't accept a callback, so we roll our own.

using MatchCallback = std::string(*)(const std::smatch&);

static std::string regex_replace_cb(const std::string& input,
                                    const std::regex& re,
                                    MatchCallback cb)
{
    std::string output;
    auto begin = std::sregex_iterator(input.begin(), input.end(), re);
    auto end   = std::sregex_iterator();
    std::size_t last_pos = 0;

    for (auto it = begin; it != end; ++it) {
        const auto& m = *it;
        output.append(input, last_pos, static_cast<std::size_t>(m.position()) - last_pos);
        output.append(cb(m));
        last_pos = static_cast<std::size_t>(m.position() + m.length());
    }
    output.append(input, last_pos);
    return output;
}

// ========== Helper: to_lower =============

static std::string to_lower(std::string s)
{
    std::ranges::transform(s, s.begin(), ::tolower);
    return s;
}

// ========== Helper: erase all occurrences of a char ==========

static std::string erase_char(std::string s, char ch)
{
    std::erase(s, ch);
    return s;
}

// ============ Helper: has any word character (\w) ================

static bool has_word_char(std::string_view sv)
{
    return std::ranges::any_of(sv, [](char c) {
        return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
    });
}

// ============= count_sentences =================

int count_sentences(std::string_view text)
{
    if (!has_word_char(text)) return 0;

    std::string original = normalise_whitespace(text);
    std::string masked   = original;

    // Pre-compiled regexes (static so they're built once)
    static const std::regex re_ellipsis_dots(R"(\.{2,})");
    static const std::regex re_url_http(R"(https?://[^\s.!?]+(?:\.[^\s.!?]+)*)");
    static const std::regex re_url_www(R"(www\.[^\s.!?]+(?:\.[^\s.!?]+)*)");
    static const std::regex re_email(R"(\S+@\S+\.\S+)");
    static const std::regex re_decimal(R"(\d+\.\d+)");
    static const std::regex re_abbrev(R"(\b([A-Za-z]{1,5}(?:\.[A-Za-z])*)\.)");
    static const std::regex re_initial(R"(\b([A-Z])\.)");
    static const std::regex re_multi_term(R"([.!?][.!?]+)");

    // - - Phase 1: Mask non-boundary punctuation 

    // 1a. Unicode ellipsis (UTF-8: 0xE2 0x80 0xA6) and multi-dots
    // Replace UTF-8 ellipsis '…'
    {
        const std::string ellipsis_utf8 = "\xe2\x80\xa6";
        std::string::size_type pos = 0;
        while ((pos = masked.find(ellipsis_utf8, pos)) != std::string::npos) {
            masked.replace(pos, ellipsis_utf8.size(), " ELLIPSIS ");
            pos += 10; // length of " ELLIPSIS "
        }
    }
    masked = std::regex_replace(masked, re_ellipsis_dots, " ELLIPSIS ");

    // 1b. Mask URLs
    masked = std::regex_replace(masked, re_url_http, " URL ");
    masked = std::regex_replace(masked, re_url_www,  " URL ");

    // 1c. Mask email addresses
    masked = std::regex_replace(masked, re_email, " EMAIL ");

    // 1d. Mask decimal / floating-point numbers
    masked = std::regex_replace(masked, re_decimal, " NUM ");

    // 1e. Mask known abbreviations
    masked = regex_replace_cb(masked, re_abbrev,
        [](const std::smatch& m) -> std::string {
            std::string word = to_lower(erase_char(m[1].str(), '.'));
            if (abbreviations.contains(word)) {
                return erase_char(m[1].str(), '.') + " ABBR ";
            }
            return m[0].str();
        });

    // 1f. Mask single-letter initials (e.g. "J. K. Rowling")
    masked = std::regex_replace(masked, re_initial, "$1 INIT ");

    // - - Phase 2: Count sentence-ending punctuation 

    // Collapse consecutive terminators into one
    masked = std::regex_replace(masked, re_multi_term, ".");

    // Count remaining terminators
    int count = static_cast<int>(
        std::ranges::count_if(masked, [](char c) {
            return c == '.' || c == '!' || c == '?';
        }));

    // - -  Phase 3: Handle edge cases 

    if (count == 0 && has_word_char(original)) {
        // Text with words but no terminator → 1 sentence
        count = 1;
    } else if (count > 0) {
        // Words after the last terminator → extra sentence
        auto last_dot   = masked.rfind('.');
        auto last_bang  = masked.rfind('!');
        auto last_quest = masked.rfind('?');

        auto safe_max = [](std::string::size_type a, std::string::size_type b) {
            if (a == std::string::npos) return b;
            if (b == std::string::npos) return a;
            return std::max(a, b);
        };

        auto last_term = safe_max(safe_max(last_dot, last_bang), last_quest);
        if (last_term != std::string::npos) {
            std::string_view trailing(masked);
            trailing.remove_prefix(last_term + 1);
            if (has_word_char(trailing)) {
                ++count;
            }
        }
    }

    return count;
}

// =========  Tests =========

struct TestCase {
    std::string text;
    int expected;
};

int main()
{
    const std::vector<TestCase> tests = {
        {"Hello world. How are you? I'm fine!",                         3},
        {"Mr. Smith went to Washington. He arrived at 3.14 p.m. "
         "and met Dr. Jones.",                                          2},
        {"Wait... Are you serious?! I can't believe it.",               2},
        {"J. K. Rowling wrote Harry Potter. It sold millions "
         "of copies.",                                                  2},
        {"She shouted, \"Stop right there!\" Then she ran.",            2},
        {"Visit https://example.com. It has great resources.",          2},
        {"This sentence has no ending punctuation",                     1},
        {"He paid $1,200.50 for the item. It was worth it",            2},
        {"",                                                            0},
        {"U.S. troops moved in. The operation was a success.",          2},
        {"He paid $1,200.50 for the item. It was worth it.",           2},
    };

    constexpr int width = 62;
    std::cout << std::string(width, '=') << '\n';
    std::cout << std::format("{:^{}}", "SENTENCE COUNTER (C++20) — TEST RESULTS", width) << '\n';
    std::cout << std::string(width, '=') << '\n';

    bool all_passed = true;
    for (const auto& [text, expected] : tests) {
        int result = count_sentences(text);
        bool passed = (result == expected);
        if (!passed) all_passed = false;

        std::string display = text.size() <= 50
            ? text
            : text.substr(0, 47) + "...";

        std::cout << std::format("\n  Text:     \"{}\"\n", display);
        std::cout << std::format("  Expected: {}  |  Got: {}  [{}]\n",
                                 expected, result, passed ? "pass" : "FAIL");
    }

    /* 
    std::cout << '\n' << std::string(width, '=') << '\n';
    std::cout << std::format("  {}\n",
        all_passed ? "ALL TESTS PASSED" : "SOME TESTS FAILED");
    std::cout << std::string(width, '=') << '\n';

    // Interactive mode
    std::cout << "\nEnter your own text below (empty line to quit):\n\n";
    std::string line;
    while (true) {
        std::cout << ">>> ";
        if (!std::getline(std::cin, line) || line.empty()) break;
        std::cout << std::format("    Sentences: {}\n\n", count_sentences(line));
    }
    */
    
    return all_passed ? 0 : 1;
}
