/*==============================================================================
  Лабораторна робота №9. Завдання 1. Варіант 19.
  Тема: обробка рядків на основі бібліотеки функцій string.h.

  Умова (таблиця 9.2, завдання 19.1): увести з клавіатури рядок слів, які
  відділяються символами пропуску, комами, точками. Слово є послідовністю
  символів без розділових символів усередині. Передбачити меню для виконання
  таких дій:
    - підрахувати кількість слів, які містять однакову кількість голосних
      і приголосних літер;
    - вивести на екран усі слова, довжина яких менша заданої з клавіатури;
    - видалити всі слова, передостання літера яких голосна.

  ОБМЕЖЕННЯ УМОВИ: забороняється використовувати STL, методи класу string,
  ітератори тощо. Дозволено використовувати функції заголовних файлів
  string.h, ctype.h, stdlib.h.

  ЗАУВАЖЕННЯ ЩОДО КОДУВАННЯ. Функції класифікації символів із ctype.h
  (isalpha та інші) працюють з окремими БАЙТАМИ і придатні лише для
  однобайтових кодувань. У сучасному UTF-8 українська літера займає два
  байти, тому isalpha() для неї дає хибний результат, а довжина слова,
  визначена як strlen(), була б удвічі більшою за кількість літер.
  Тому в програмі реалізовано власне декодування UTF-8 у код символу
  та класифікацію літер за кодами — це охоплює і латиницю, і кирилицю.
  Функції string.h (strtok, strcpy, strncpy, strchr) використовуються там,
  де побайтова робота є коректною.

  Виконав: Одарчук Олексій, КНУ імені Тараса Шевченка, ФІТ, група ІПЗ-11.

  Компілятор: gcc -std=c17
==============================================================================*/

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

/* Обмеження на розміри даних. У мові C межа масиву має бути сталим виразом
   часу компіляції, а змінна з модифікатором const ним не є — тому розміри
   задано директивами препроцесора, а не змінними. */
#define MAX_LINE 1024 /* довжина вхідного рядка в байтах */
#define MAX_WORDS 128 /* кількість слів                  */
#define MAX_WORD 64   /* довжина одного слова у байтах   */

/* Розділові символи, якими відділяються слова (за умовою варіанта). */
const char *DELIMITERS = " \t\n,.";

/*==============================================================================
  Робота з символами в кодуванні UTF-8
==============================================================================*/

/*------------------------------------------------------------------------------
  utf8Decode — визначити код символу та кількість байтів, які він займає.

  У кодуванні UTF-8 старші біти першого байта задають довжину послідовності:
      0xxxxxxx — 1 байт  (латиниця, цифри, розділові знаки);
      110xxxxx — 2 байти (зокрема кирилиця);
      1110xxxx — 3 байти;
      11110xxx — 4 байти.
  Продовжувальні байти мають вигляд 10xxxxxx і несуть по 6 значущих бітів.

  Кожен продовжувальний байт перевіряється. Якщо послідовність неповна або
  некоректна (зокрема обірвана завершальним нулем рядка), перший байт
  вважається окремим недійсним символом U+FFFD. Тому функція ніколи не
  «перестрибує» кінець рядка, навіть якщо текст уведено не в UTF-8.

  Параметри:
      s     [вхідний]  — покажчик на початок символу;
      bytes [вихідний] — адреса змінної для кількості байтів символу.
  Повертає : код символу (кодову позицію Unicode) або 0xFFFD.

  Локальні змінні:
      p      — байти символу як беззнакові числа;
      length — кількість байтів, яку задає перший байт;
      code   — накопичуваний код символу.
------------------------------------------------------------------------------*/
unsigned utf8Decode(const char *s, int *bytes)
{
    const unsigned char *p = (const unsigned char *)s;
    int length = 1;
    unsigned code = p[0];

    /* Маска лишає старші біти першого байта, решта бітів — початок коду. */
    if ((p[0] & 0x80) == 0x00) {
        *bytes = 1; /* 0xxxxxxx — однобайтовий символ */
        return code;
    } else if ((p[0] & 0xE0) == 0xC0) {
        length = 2; /* 110xxxxx: 5 значущих бітів */
        code = p[0] & 0x1F;
    } else if ((p[0] & 0xF0) == 0xE0) {
        length = 3; /* 1110xxxx: 4 значущі біти */
        code = p[0] & 0x0F;
    } else if ((p[0] & 0xF8) == 0xF0) {
        length = 4; /* 11110xxx: 3 значущі біти */
        code = p[0] & 0x07;
    } else {
        *bytes = 1; /* продовжувальний байт без початкового */
        return 0xFFFD;
    }

    /* Кожен продовжувальний байт додає до коду шість молодших бітів. */
    for (int i = 1; i < length; ++i) {
        if ((p[i] & 0xC0) != 0x80) {
            *bytes = 1; /* послідовність обірвана */
            return 0xFFFD;
        }
        code = (code << 6) | (p[i] & 0x3F);
    }

    *bytes = length;
    return code;
}

/*------------------------------------------------------------------------------
  toLowerCode — звести код літери до нижнього регістру.

  Обробляються латиниця (A..Z), основна кирилиця (А..Я) та окремі українські
  літери Є, І, Ї, Ґ, які в таблиці Unicode розташовані поза основним блоком.

  Параметри: code [вхідний] — код символу.
  Повертає : код відповідної малої літери або сам код, якщо він не є великою літерою.
------------------------------------------------------------------------------*/
unsigned toLowerCode(unsigned code)
{
    if (code >= 'A' && code <= 'Z')
        return code + 32; /* латиниця */
    if (code >= 0x0410 && code <= 0x042F)
        return code + 32; /* А..Я     */
    if (code == 0x0404)
        return 0x0454; /* Є -> є   */
    if (code == 0x0406)
        return 0x0456; /* І -> і   */
    if (code == 0x0407)
        return 0x0457; /* Ї -> ї   */
    if (code == 0x0490)
        return 0x0491; /* Ґ -> ґ   */
    return code;
}

/*------------------------------------------------------------------------------
  isLetterCode — чи є символ літерою (латиниця або кирилиця).

  Параметри: code [вхідний] — код символу.
  Повертає : true — символ є літерою.
------------------------------------------------------------------------------*/
bool isLetterCode(unsigned code)
{
    const unsigned c = toLowerCode(code);

    if (c >= 'a' && c <= 'z')
        return true; /* латиниця        */
    if (c >= 0x0430 && c <= 0x044F)
        return true; /* а..я            */
    if (c == 0x0454 || c == 0x0456 || c == 0x0457 || c == 0x0491)
        return true; /* є, і, ї, ґ      */
    return false;
}

/*------------------------------------------------------------------------------
  isVowelCode — чи є літера голосною.

  Українські голосні: а, е, є, и, і, ї, о, у, ю, я.
  Латинські голосні:  a, e, i, o, u, y.

  Параметри: code [вхідний] — код символу.
  Повертає : true — літера голосна.
------------------------------------------------------------------------------*/
bool isVowelCode(unsigned code)
{
    const unsigned c = toLowerCode(code);

    /* Латинські голосні. */
    if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y')
        return true;

    /* Українські голосні: а(0430) е(0435) и(0438) о(043E) у(0443)
                           ю(044E) я(044F) є(0454) і(0456) ї(0457). */
    if (c == 0x0430 || c == 0x0435 || c == 0x0438 || c == 0x043E || c == 0x0443 ||
        c == 0x044E || c == 0x044F || c == 0x0454 || c == 0x0456 || c == 0x0457)
        return true;

    return false;
}

/*------------------------------------------------------------------------------
  charCount — кількість СИМВОЛІВ у рядку (не байтів).

  Параметри: s [вхідний] — рядок у кодуванні UTF-8.
  Повертає : кількість символів.

  Локальні змінні:
      count — лічильник символів;
      bytes — довжина поточного символу в байтах.
------------------------------------------------------------------------------*/
int charCount(const char *s)
{
    int count = 0;
    int bytes = 0;

    for (const char *p = s; *p != '\0'; p += bytes) {
        utf8Decode(p, &bytes);
        ++count;
    }

    return count;
}

/*------------------------------------------------------------------------------
  printPadded — вивести рядок, доповнивши його пропусками до заданої ширини
                в СИМВОЛАХ.

  Специфікатор формату виду %-24s рахує байти, тому для тексту в кодуванні
  UTF-8 він вирівнює таблиці неправильно. Ширина обчислюється функцією
  charCount(), яка рахує саме символи.

  Параметри:
      s     [вхідний] — рядок, що виводиться;
      width [вхідний] — потрібна ширина поля в символах.
------------------------------------------------------------------------------*/
void printPadded(const char *s, int width)
{
    printf("%s", s);

    for (int i = charCount(s); i < width; ++i)
        putchar(' ');
}

/*------------------------------------------------------------------------------
  countVowelsConsonants — підрахувати голосні та приголосні літери слова.

  Параметри:
      word       [вхідний]  — слово;
      vowels     [вихідний] — адреса лічильника голосних;
      consonants [вихідний] — адреса лічильника приголосних.

  Символи, які не є літерами (цифри, дефіси всередині слова), не враховуються
  в жодному з лічильників.
------------------------------------------------------------------------------*/
void countVowelsConsonants(const char *word, int *vowels, int *consonants)
{
    *vowels = 0;
    *consonants = 0;

    int bytes = 0;
    for (const char *p = word; *p != '\0'; p += bytes) {
        const unsigned code = utf8Decode(p, &bytes);

        if (!isLetterCode(code))
            continue;

        if (isVowelCode(code))
            ++(*vowels);
        else
            ++(*consonants);
    }
}

/*------------------------------------------------------------------------------
  penultimateLetter — код передостанньої ЛІТЕРИ слова.

  Параметри:
      word  [вхідний]  — слово;
      found [вихідний] — адреса ознаки: true, якщо у слові щонайменше дві літери.
  Повертає : код передостанньої літери або 0, якщо літер менше двох.

  Літерами вважаються лише символи, які проходять перевірку isLetterCode(),
  тому цифри та інші символи не спотворюють результат.

  Локальні змінні:
      last, prev — коди останньої та передостанньої знайдених літер;
      total      — кількість знайдених літер.
------------------------------------------------------------------------------*/
unsigned penultimateLetter(const char *word, bool *found)
{
    unsigned last = 0, prev = 0;
    int total = 0;
    int bytes = 0;

    for (const char *p = word; *p != '\0'; p += bytes) {
        const unsigned code = utf8Decode(p, &bytes);

        if (!isLetterCode(code))
            continue;

        prev = last;
        last = code;
        ++total;
    }

    *found = (total >= 2);
    return *found ? prev : 0;
}

/*==============================================================================
  Робота зі списком слів
==============================================================================*/

/* Слова вхідного рядка та їх кількість. */
char g_words[MAX_WORDS][MAX_WORD];
int g_count = 0;

/*------------------------------------------------------------------------------
  splitIntoWords — розібрати рядок на слова за розділовими символами.

  Використано бібліотечну функцію strtok() з string.h. Особливість її
  застосування: strtok ЗМІНЮЄ вхідний рядок, замінюючи розділові символи
  нульовими, і зберігає внутрішній стан між викликами — тому перший виклик
  отримує рядок, а наступні замість нього нульовий покажчик. Через це
  розбір виконується над копією рядка, а не над оригіналом.

  Параметри: line [вхідний] — вхідний рядок.
  Повертає : кількість знайдених слів.

  Локальні змінні:
      buffer — робоча копія рядка, яку змінює strtok;
      token  — покажчик на чергове слово;
      count  — лічильник слів.
------------------------------------------------------------------------------*/
int splitIntoWords(const char *line)
{
    char buffer[MAX_LINE];
    strncpy(buffer, line, MAX_LINE - 1);
    buffer[MAX_LINE - 1] = '\0';

    int count = 0;
    char *token = strtok(buffer, DELIMITERS);

    while (token != NULL && count < MAX_WORDS) {
        strncpy(g_words[count], token, MAX_WORD - 1);
        g_words[count][MAX_WORD - 1] = '\0';
        ++count;
        token = strtok(NULL, DELIMITERS);
    }

    return count;
}

/*------------------------------------------------------------------------------
  printWords — вивести поточний список слів у табличному вигляді.

  Параметри: title [вхідний] — заголовок таблиці.
------------------------------------------------------------------------------*/
void printWords(const char *title)
{
    printf("\n%s (слів: %d)\n", title, g_count);

    if (g_count == 0) {
        printf("  список порожній\n");
        return;
    }

    for (int i = 0; i < g_count; ++i) {
        int vowels = 0, consonants = 0;
        countVowelsConsonants(g_words[i], &vowels, &consonants);

        printf("  %2d. ", i + 1);
        printPadded(g_words[i], 22);
        printf(" довжина %2d, голосних %d, приголосних %d\n", charCount(g_words[i]),
               vowels, consonants);
    }
}

/*==============================================================================
  Команди меню
==============================================================================*/

/*------------------------------------------------------------------------------
  cmdCountBalanced — підрахувати слова з однаковою кількістю голосних
                     і приголосних літер.
  Повертає : кількість таких слів.
------------------------------------------------------------------------------*/
int cmdCountBalanced()
{
    int found = 0;

    printf("\nСлова з однаковою кількістю голосних і приголосних:\n");

    for (int i = 0; i < g_count; ++i) {
        int vowels = 0, consonants = 0;
        countVowelsConsonants(g_words[i], &vowels, &consonants);

        if (vowels == consonants) {
            printf("  %s (голосних %d = приголосних %d)\n", g_words[i], vowels,
                   consonants);
            ++found;
        }
    }

    if (found == 0)
        printf("  таких слів немає\n");

    printf("Кількість слів з однаковою кількістю голосних і приголосних: %d\n", found);
    return found;
}

/*------------------------------------------------------------------------------
  cmdShorterThan — вивести всі слова, довжина яких менша за задану.

  Параметри: limit [вхідний] — гранична довжина у символах.
  Повертає : кількість виведених слів.
------------------------------------------------------------------------------*/
int cmdShorterThan(int limit)
{
    int found = 0;

    printf("\nСлова, довжина яких менша за %d:\n", limit);

    for (int i = 0; i < g_count; ++i) {
        const int length = charCount(g_words[i]);

        if (length < limit) {
            printf("  %s (довжина %d)\n", g_words[i], length);
            ++found;
        }
    }

    if (found == 0)
        printf("  таких слів немає\n");

    return found;
}

/*------------------------------------------------------------------------------
  cmdDeletePenultimateVowel — видалити всі слова, передостання літера яких
                              голосна.

  Видалення виконується ущільненням масиву: слова, що лишаються, послідовно
  переписуються на початок, після чого кількість слів зменшується.

  Повертає : кількість видалених слів.

  Локальні змінні:
      kept    — кількість слів, що лишилися;
      removed — кількість видалених слів.
------------------------------------------------------------------------------*/
int cmdDeletePenultimateVowel()
{
    int kept = 0, removed = 0;

    printf("\nВидалення слів, передостання літера яких голосна:\n");

    for (int i = 0; i < g_count; ++i) {
        bool found = false;
        const unsigned code = penultimateLetter(g_words[i], &found);

        if (found && isVowelCode(code)) {
            printf("  видалено: %s\n", g_words[i]);
            ++removed;
            continue;
        }

        if (!found)
            printf("  збережено: %s (літер менше двох, передостанньої немає)\n",
                   g_words[i]);

        if (kept != i)
            strcpy(g_words[kept], g_words[i]);
        ++kept;
    }

    g_count = kept;

    if (removed == 0)
        printf("  жодного слова не видалено\n");

    return removed;
}

/*------------------------------------------------------------------------------
  readInt — прочитати ціле число із заданого діапазону.

  Параметри: prompt [вхідний], value [вихідний], low, high [вхідні].
  Повертає : true — число прочитано; false — вхідні дані вичерпано.
------------------------------------------------------------------------------*/
bool readInt(const char *prompt, int *value, int low, int high)
{
    for (;;) {
        printf("%s", prompt);
        const int scanned = scanf("%d", value);

        if (scanned == EOF)
            return false;
        if (scanned == 1 && *value >= low && *value <= high) {
            int c;
            /* залишок рядка */
            while ((c = getchar()) != '\n' && c != EOF) {
            }
            return true;
        }

        if (scanned != 1) {
            int c;
            /* очищення буфера */
            while ((c = getchar()) != '\n' && c != EOF) {
            }
        }
        printf("Помилка: потрібне ціле число від %d до %d.\n", low, high);
    }
}

/*------------------------------------------------------------------------------
  Головна функція. Читає рядок, розбиває його на слова та виконує команди меню.

  Локальні змінні:
      line   — вхідний рядок;
      choice — обраний пункт меню;
      limit  — гранична довжина слова для другої команди.
------------------------------------------------------------------------------*/
int main()
{
    printf("Лабораторна робота №9, завдання 1 (варіант 19)\n");
    printf("Виконав: студент групи ІПЗ-11 Одарчук Олексій\n");
    printf("Обробка рядка слів\n\n");
    printf("Слова відділяються пропусками, комами та крапками.\n");
    printf("Уведіть рядок: ");

    char line[MAX_LINE];
    if (fgets(line, MAX_LINE, stdin) == NULL) {
        printf("\nВхідні дані вичерпано.\n");
        return 1;
    }

    if (strchr(line, '\n') == NULL) {
        /* Рядок довший за буфер: зайві символи відкидаються. */
        int c;
        while ((c = getchar()) != '\n' && c != EOF) {
        }
    }

    g_count = splitIntoWords(line);

    if (g_count == 0) {
        printf("\nУ введеному рядку немає жодного слова.\n");
        return 2;
    }

    printWords("Вхідний список слів");

    for (;;) {
        printf("\n============================================================\n");
        printf("Меню команд:\n");
        printf(
            "  1 - підрахувати слова з однаковою кількістю голосних і приголосних\n");
        printf("  2 - вивести слова, довжина яких менша заданої\n");
        printf("  3 - видалити слова, передостання літера яких голосна\n");
        printf("  4 - вивести поточний список слів\n");
        printf("  5 - вихід\n");

        int choice = 0;
        if (!readInt("Оберіть команду (1..5): ", &choice, 1, 5)) {
            printf("\nВхідні дані вичерпано. Завершення роботи.\n");
            break;
        }

        if (choice == 1) {
            cmdCountBalanced();
        } else if (choice == 2) {
            int limit = 0;
            if (!readInt("Уведіть граничну довжину слова: ", &limit, 1, MAX_WORD))
                break;
            cmdShorterThan(limit);
        } else if (choice == 3) {
            const int removed = cmdDeletePenultimateVowel();
            printf("Видалено слів: %d\n", removed);
            printWords("Список після видалення");
        } else if (choice == 4) {
            printWords("Поточний список слів");
        } else {
            printf("\nЗавершення роботи.\n");
            break;
        }
    }

    return 0;
}
