/*==============================================================================
  Лабораторна робота №11. Завдання 2. Варіант 19.
  Тема: обробка бінарних файлів.

  Умова (таблиця 11.2, завдання 19.2): створити масив структур. Кожна
  структура складається з таких елементів: назва фірми, продукт, що
  продається — комп'ютери і програмне забезпечення, регіон збуту, вартість
  продажу, термін постачання. Створений масив структур записати до бінарного
  файла. Виконати такі операції з бінарним файлом:
    - доповнити бінарний файл новими записами;
    - замінити вибраний користувачем запис у бінарному файлі на новий,
      значення полів якого ввести з клавіатури;
    - видалити з бінарного файлу вибраний користувачем запис.
  Здійснити пошук у бінарному файлі та вивести у вигляді таблиць:
    - список комп'ютерів, що продаються у заданому регіоні конкретною фірмою;
    - вартість проданого програмного забезпечення у задані терміни;
    - найрентабельніші фірми (з найбільшою вартістю продажів).
  Результати запитів записати до нового бінарного файлу і вивести на екран.

  ВАЖЛИВО: усі дані для запитів беруться З ФАЙЛУ, а не з масиву в оперативній
  пам'яті. Кожна функція запиту самостійно відкриває файл даних, послідовно
  зчитує з нього записи, записує знайдене до окремого файлу результатів
  і закриває обидва файли. На екран виводиться вміст файлу результатів.

  ІНСТРУМЕНТАРІЙ: завдання 2 реалізовано КЛАСАМИ ПОТОКІВ fstream, ifstream,
  ofstream. Завдання 1 тієї самої лабораторної роботи реалізовано
  стандартними функціями stdio.h — таким чином опрацьовано обидва
  інструментарії роботи з файлами.

  Заміна запису виконується ПРЯМИМ ДОСТУПОМ: позиція запису обчислюється як
  номер, помножений на розмір структури, після чого потрібне місце у файлі
  перезаписується без переписування всього файлу.

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

  Компілятор: g++ -std=c++17
==============================================================================*/

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <ctime>

/* Обмеження на розміри даних та імена бінарних файлів. */
const int MAX_RECORDS = 200; /* найбільша кількість записів у файлі даних */
const int MAX_NAME = 32;
const char *DATA_FILE = "sales.dat";
const char *COMPUTERS_FILE = "computers.dat"; /* результат запиту 1 */
const char *SOFTWARE_FILE = "software.dat";   /* результат запиту 2 */
const char *FIRMS_FILE = "firms.dat";         /* результат запиту 3 */

/* Вид продукту: умова прямо називає два види. */
enum ProductKind { KIND_COMPUTER = 0, KIND_SOFTWARE = 1 };

/* Термін постачання. */
struct Date {
    int day;
    int month;
    int year;
};

/*------------------------------------------------------------------------------
  Sale — запис про продаж. Структура має сталий розмір (масиви символів
  замість рядків змінної довжини), що є обов'язковою умовою для запису
  в бінарний файл із прямим доступом: лише за сталого розміру запису
  його позицію можна обчислити множенням номера на sizeof(Sale).
------------------------------------------------------------------------------*/
struct Sale {
    char firm[MAX_NAME];
    ProductKind kind;
    char product[MAX_NAME];
    char region[MAX_NAME];
    double price;
    Date delivery;
};

/* Сумарні продажі однієї фірми — запис файлу результатів запиту 3. */
struct FirmTotal {
    char firm[MAX_NAME];
    long sales;
    double total;
};

/*==============================================================================
  Допоміжні функції
==============================================================================*/

/*------------------------------------------------------------------------------
  utf8Width — ширина рядка в символах, а не в байтах (кирилиця в UTF-8
              займає два байти на літеру).
  Параметри: s [вхідний] — рядок.  Повертає: кількість символів.
------------------------------------------------------------------------------*/
int utf8Width(const char *s)
{
    int width = 0;

    for (const unsigned char *p = (const unsigned char *)s; *p != '\0'; ++p)
        /* Продовжувальний байт UTF-8 має вигляд 10xxxxxx: маска 0xC0 лишає
           два старші біти, і якщо вони не дорівнюють 10, це початок символу. */
        if ((*p & 0xC0) != 0x80)
            ++width;

    return width;
}

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

    for (int i = utf8Width(s); i < width; ++i)
        std::cout << ' ';
}

/*------------------------------------------------------------------------------
  kindName — назва виду продукту.
  Параметри: kind [вхідний].  Повертає: рядок-константу.
------------------------------------------------------------------------------*/
const char *kindName(ProductKind kind)
{
    return kind == KIND_COMPUTER ? "комп'ютери" : "ПЗ";
}

/*------------------------------------------------------------------------------
  dateToNumber — звести дату до числа виду РРРРММДД для порівняння.
  Параметри: d [вхідний] — покажчик на дату.  Повертає: число РРРРММДД.
------------------------------------------------------------------------------*/
long dateToNumber(const Date *d)
{
    return (long)d->year * 10000 + (long)d->month * 100 + d->day;
}

/*------------------------------------------------------------------------------
  readInt — прочитати ціле число із заданого діапазону.
  Параметри: prompt [вхідний], value [вихідний], low, high [вхідні].
  Повертає : true — прочитано; false — вхідні дані вичерпано.
------------------------------------------------------------------------------*/
bool readInt(const char *prompt, int *value, int low, int high)
{
    for (;;) {
        std::cout << prompt;

        if (std::cin >> *value && *value >= low && *value <= high) {
            std::cin.ignore(1024, '\n');
            return true;
        }

        if (std::cin.eof())
            return false;

        std::cin.clear();
        std::cin.ignore(1024, '\n');
        std::cout << "Помилка: потрібне ціле число від " << low << " до " << high
                  << ".\n";
    }
}

/*------------------------------------------------------------------------------
  readDouble — прочитати дійсне число, не менше за задану межу.
  Параметри: prompt [вхідний], value [вихідний], low [вхідний].
  Повертає : true — прочитано; false — вхідні дані вичерпано.
------------------------------------------------------------------------------*/
bool readDouble(const char *prompt, double *value, double low)
{
    for (;;) {
        std::cout << prompt;

        if (std::cin >> *value && *value >= low) {
            std::cin.ignore(1024, '\n');
            return true;
        }

        if (std::cin.eof())
            return false;

        std::cin.clear();
        std::cin.ignore(1024, '\n');
        std::cout << "Помилка: потрібне дійсне число, не менше за " << low << ".\n";
    }
}

/*------------------------------------------------------------------------------
  trimIncompleteUtf8 — відкинути неповний символ UTF-8 у кінці рядка.

  Якщо рядок обрізано за розміром буфера, межа може пройти посередині
  багатобайтового символу (кирилична літера займає два байти). Такий
  залишок не є коректним символом, тому відкидається.

  Параметри: s [вхідний/вихідний] — рядок.

  Локальні змінні:
      length   — довжина рядка в байтах;
      lead     — індекс початкового байта останнього символу;
      expected — кількість байтів, яку задає початковий байт.
------------------------------------------------------------------------------*/
void trimIncompleteUtf8(char *s)
{
    const size_t length = std::strlen(s);

    if (length == 0)
        return;

    /* Продовжувальні байти мають вигляд 10xxxxxx: пропускаємо їх до
       початкового байта останнього символу. */
    size_t lead = length - 1;
    while (lead > 0 && ((unsigned char)s[lead] & 0xC0) == 0x80)
        --lead;

    /* Старші біти початкового байта задають довжину послідовності:
       110xxxxx — 2 байти, 1110xxxx — 3, 11110xxx — 4. */
    const unsigned char first = (unsigned char)s[lead];
    size_t expected = 1;
    if ((first & 0xE0) == 0xC0)
        expected = 2;
    else if ((first & 0xF0) == 0xE0)
        expected = 3;
    else if ((first & 0xF8) == 0xF0)
        expected = 4;

    if (length - lead < expected)
        s[lead] = '\0';
}

/*------------------------------------------------------------------------------
  readLine — прочитати текстовий рядок з клавіатури.
  Параметри: prompt [вхідний], buffer [вихідний], size [вхідний].
  Повертає : true — прочитано; false — вхідні дані вичерпано.
------------------------------------------------------------------------------*/
bool readLine(const char *prompt, char *buffer, int size)
{
    std::cout << prompt;
    std::cin.getline(buffer, size);

    if (std::cin.eof())
        return std::cin.gcount() > 0;

    if (std::cin.fail()) {
        /* Рядок довший за буфер: зайві символи відкидаються. */
        std::cin.clear();
        std::cin.ignore(1024, '\n');
        trimIncompleteUtf8(buffer);
        std::cout << "Увага: рядок задовгий, збережено лише його початок: " << buffer
                  << '\n';
    }
    return true;
}

/*==============================================================================
  Робота з бінарним файлом
==============================================================================*/

/*------------------------------------------------------------------------------
  recordCount — кількість записів у бінарному файлі.

  Обчислюється як розмір файлу, поділений на розмір однієї структури.
  Такий спосіб можливий саме тому, що всі записи мають однаковий розмір.

  Параметри: fileName [вхідний] — ім'я файлу.
  Повертає : кількість записів або -1, якщо файл не існує.

  Локальні змінні:
      file — вхідний файловий потік;
      size — розмір файлу в байтах.
------------------------------------------------------------------------------*/
long recordCount(const char *fileName)
{
    std::ifstream file;
    file.open(fileName, std::ios::binary | std::ios::ate);

    if (!file)
        return -1;

    const std::streampos size = file.tellg();
    file.close();

    return (long)(size / (std::streamoff)sizeof(Sale));
}

/*------------------------------------------------------------------------------
  printTableHeader — вивести заголовок таблиці записів.

  Заголовок відповідає переліку полів з умови варіанта і виводиться
  один раз перед даними.
------------------------------------------------------------------------------*/
void printTableHeader()
{
    std::cout << "  ";
    printPadded("№", 4);
    printPadded("Фірма", 14);
    printPadded("Продукт", 16);
    printPadded("Вид", 12);
    printPadded("Регіон збуту", 16);
    printPadded("Вартість", 13);
    printPadded("Термін постачання", 18);
    std::cout << "\n  ";

    for (int i = 0; i < 93; ++i)
        std::cout << '-';
    std::cout << '\n';
}

/*------------------------------------------------------------------------------
  printRecord — вивести один запис рядком таблиці.
  Параметри: sale [вхідний] — покажчик на структуру; index [вхідний] — номер рядка.
------------------------------------------------------------------------------*/
void printRecord(const Sale *sale, long index)
{
    char buffer[MAX_NAME];

    std::cout << "  ";
    snprintf(buffer, sizeof buffer, "%ld.", index);
    printPadded(buffer, 4);
    printPadded(sale->firm, 14);
    printPadded(sale->product, 16);
    printPadded(kindName(sale->kind), 12);
    printPadded(sale->region, 16);

    snprintf(buffer, sizeof buffer, "%.2f", sale->price);
    printPadded(buffer, 13);

    snprintf(buffer, sizeof buffer, "%02d.%02d.%d", sale->delivery.day,
             sale->delivery.month, sale->delivery.year);
    printPadded(buffer, 18);
    std::cout << '\n';
}

/*------------------------------------------------------------------------------
  printSalesFile — вивести записи бінарного файлу у вигляді таблиці.

  Записи зчитуються послідовно, доки не буде досягнуто кінця файлу.
  Заголовок таблиці виводиться один раз перед першим записом.

  Параметри:
      fileName [вхідний] — ім'я файлу;
      total    [вихідний] — сумарна вартість виведених записів.
  Повертає : кількість записів або -1, якщо файл не існує.
------------------------------------------------------------------------------*/
long printSalesFile(const char *fileName, double *total)
{
    std::ifstream file;
    file.open(fileName, std::ios::binary);

    if (!file)
        return -1;

    Sale sale;
    long count = 0;
    *total = 0.0;

    while (file.read((char *)&sale, sizeof(Sale))) {
        if (count == 0)
            printTableHeader();

        printRecord(&sale, ++count);
        *total += sale.price;
    }

    file.close();
    return count;
}

/*------------------------------------------------------------------------------
  printFirmHeader — вивести заголовок таблиці сум продажів по фірмах.
------------------------------------------------------------------------------*/
void printFirmHeader()
{
    std::cout << "  ";
    printPadded("Фірма", 16);
    printPadded("Продажів", 12);
    printPadded("Сумарна вартість", 20);
    std::cout << "\n  ";

    for (int i = 0; i < 48; ++i)
        std::cout << '-';
    std::cout << '\n';
}

/*------------------------------------------------------------------------------
  printFirmRow — вивести рядок таблиці сум продажів по фірмах.
  Параметри: firm [вхідний] — покажчик на структуру з сумами фірми.
------------------------------------------------------------------------------*/
void printFirmRow(const FirmTotal *firm)
{
    char buffer[MAX_NAME];

    std::cout << "  ";
    printPadded(firm->firm, 16);

    snprintf(buffer, sizeof buffer, "%ld", firm->sales);
    printPadded(buffer, 12);

    snprintf(buffer, sizeof buffer, "%.2f", firm->total);
    printPadded(buffer, 20);
    std::cout << '\n';
}

/* Набори назв для генерації псевдовипадкових записів. */
const char *FIRMS[] = {"Everest", "Kvazar", "Sokil", "Dnipro-IT", "Karpaty"};
const char *REGIONS[] = {"Київський", "Львівський", "Одеський", "Харківський",
                         "Дніпровський"};
const char *COMPUTERS[] = {"Optima 5", "Nova Pro", "Titan X", "Bureau 300"};
const char *SOFTWARE[] = {"OblikPro", "SklavSoft", "DocFlow", "AntiVirus U"};

/*------------------------------------------------------------------------------
  generateRecord — заповнити запис псевдовипадковими даними.
  Параметри: sale [вихідний] — покажчик на структуру.
------------------------------------------------------------------------------*/
void generateRecord(Sale *sale)
{
    std::strcpy(sale->firm, FIRMS[rand() % 5]);
    std::strcpy(sale->region, REGIONS[rand() % 5]);

    sale->kind = (rand() % 2 == 0) ? KIND_COMPUTER : KIND_SOFTWARE;

    if (sale->kind == KIND_COMPUTER)
        std::strcpy(sale->product, COMPUTERS[rand() % 4]);
    else
        std::strcpy(sale->product, SOFTWARE[rand() % 4]);

    sale->price = 1000.0 + (rand() % 9000) + (rand() % 100) / 100.0;

    sale->delivery.year = 2025;
    sale->delivery.month = 1 + rand() % 12;
    sale->delivery.day = 1 + rand() % 28;
}

/*------------------------------------------------------------------------------
  inputRecord — заповнити запис даними з клавіатури.
  Параметри: sale [вихідний] — покажчик на структуру.
  Повертає : true — заповнено; false — вхідні дані вичерпано.
------------------------------------------------------------------------------*/
bool inputRecord(Sale *sale)
{
    if (!readLine("  Назва фірми: ", sale->firm, MAX_NAME))
        return false;

    int kind = 0;
    if (!readInt("  Вид продукту (1 - комп'ютери, 2 - ПЗ): ", &kind, 1, 2))
        return false;
    sale->kind = (kind == 1) ? KIND_COMPUTER : KIND_SOFTWARE;

    if (!readLine("  Назва продукту: ", sale->product, MAX_NAME))
        return false;
    if (!readLine("  Регіон збуту: ", sale->region, MAX_NAME))
        return false;
    if (!readDouble("  Вартість продажу: ", &sale->price, 0.0))
        return false;

    std::cout << "  Термін постачання:\n";
    if (!readInt("    рік (2000..2100): ", &sale->delivery.year, 2000, 2100))
        return false;
    if (!readInt("    місяць (1..12): ", &sale->delivery.month, 1, 12))
        return false;
    if (!readInt("    день (1..31): ", &sale->delivery.day, 1, 31))
        return false;

    return true;
}

/*------------------------------------------------------------------------------
  cmdCreateFile — команда меню: створити масив структур і записати його
                  до бінарного файлу.

  Файл відкривається у режимі std::ios::trunc, тобто попередній вміст
  знищується — команда створює файл заново.
------------------------------------------------------------------------------*/
void cmdCreateFile()
{
    int n = 0;
    if (!readInt("Уведіть кількість записів (1..200): ", &n, 1, MAX_RECORDS))
        return;

    std::cout << "Спосіб створення:\n"
                 "  1 - введення з клавіатури\n"
                 "  2 - генерація псевдовипадкових даних\n";

    int choice = 0;
    if (!readInt("Оберіть спосіб (1..2): ", &choice, 1, 2))
        return;

    Sale records[MAX_RECORDS];

    if (choice == 1) {
        for (int i = 0; i < n; ++i) {
            std::cout << "\n  --- запис " << (i + 1) << " ---\n";
            if (!inputRecord(&records[i]))
                return;
        }
    } else {
        srand((unsigned)time(nullptr));
        for (int i = 0; i < n; ++i)
            generateRecord(&records[i]);
    }

    std::ofstream file;

    file.open(DATA_FILE, std::ios::binary | std::ios::trunc);

    if (!file) {
        std::cout << "Помилка: не вдалося створити файл " << DATA_FILE << ".\n";
        return;
    }

    file.write((const char *)records, n * sizeof(Sale));
    file.close();

    std::cout << "\nМасив структур записано до бінарного файлу " << DATA_FILE
              << ". Кількість записів: " << n << ".\n";
}

/*------------------------------------------------------------------------------
  cmdPrintFile — команда меню: вивести вміст бінарного файлу.

  Записи зчитуються послідовно, доки не буде досягнуто кінця файлу.
------------------------------------------------------------------------------*/
void cmdPrintFile()
{
    if (recordCount(DATA_FILE) < 0) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }

    std::cout << "\nВміст бінарного файлу " << DATA_FILE << "\n\n";

    double total = 0.0;
    const long count = printSalesFile(DATA_FILE, &total);

    std::cout << "\n  Записів у файлі: " << count << '\n';
}

/*------------------------------------------------------------------------------
  cmdAppend — команда меню: доповнити бінарний файл новими записами.

  Файл відкривається в режимі std::ios::app — записи дописуються в кінець,
  наявний вміст зберігається.
------------------------------------------------------------------------------*/
void cmdAppend()
{
    const long total = recordCount(DATA_FILE);

    if (total < 0) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }
    if (total >= MAX_RECORDS) {
        std::cout << "У файлі вже найбільша допустима кількість записів ("
                  << MAX_RECORDS << ").\n";
        return;
    }

    /* Кількість записів у файлі не може перевищити MAX_RECORDS: на цю межу
       розраховано масиви у функціях видалення та запиту 3. */
    const int freeSlots = MAX_RECORDS - (int)total;
    int n = 0;
    char prompt[80];
    snprintf(prompt, sizeof prompt, "Скільки записів дописати (1..%d): ", freeSlots);

    if (!readInt(prompt, &n, 1, freeSlots))
        return;

    std::ofstream file;

    file.open(DATA_FILE, std::ios::binary | std::ios::app);

    if (!file) {
        std::cout << "Помилка: не вдалося відкрити файл для дописування.\n";
        return;
    }

    for (int i = 0; i < n; ++i) {
        std::cout << "\n  --- новий запис " << (i + 1) << " ---\n";

        Sale sale;
        if (!inputRecord(&sale)) {
            file.close();
            return;
        }

        file.write((const char *)&sale, sizeof(Sale));
    }

    file.close();

    std::cout << "\nДописано записів: " << n
              << ". Усього у файлі: " << recordCount(DATA_FILE) << ".\n";
}

/*------------------------------------------------------------------------------
  cmdReplace — команда меню: замінити вибраний користувачем запис.

  ПРЯМИЙ ДОСТУП до компонентів бінарного файлу. Позиція запису обчислюється
  як (номер - 1) * sizeof(Sale); покажчик запису встановлюється методом
  seekp(), після чого перезаписується рівно одна структура. Решта файлу
  не переписується — саме в цьому перевага бінарного файлу зі сталим
  розміром запису перед текстовим.
------------------------------------------------------------------------------*/
void cmdReplace()
{
    const long total = recordCount(DATA_FILE);

    if (total < 0) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }
    if (total == 0) {
        std::cout << "Файл порожній — замінювати нічого.\n";
        return;
    }

    int number = 0;
    char prompt[80];
    snprintf(prompt, sizeof prompt, "Номер запису для заміни (1..%ld): ", total);

    if (!readInt(prompt, &number, 1, (int)total))
        return;

    std::cout << "\n  --- новий вміст запису " << number << " ---\n";

    Sale sale;
    if (!inputRecord(&sale))
        return;

    /* Режим in|out відкриває наявний файл без знищення вмісту. */
    std::fstream file;
    file.open(DATA_FILE, std::ios::binary | std::ios::in | std::ios::out);

    if (!file) {
        std::cout << "Помилка: не вдалося відкрити файл для запису.\n";
        return;
    }

    file.seekp((number - 1) * sizeof(Sale), std::ios::beg);
    file.write((const char *)&sale, sizeof(Sale));
    file.close();

    std::cout << "\nЗапис " << number << " замінено.\n";
}

/*------------------------------------------------------------------------------
  cmdDelete — команда меню: видалити вибраний користувачем запис.

  Бінарний файл не має операції вилучення частини вмісту, тому видалення
  виконується перезаписом: усі записи, крім вибраного, послідовно
  переписуються до файлу заново. Це принципова відмінність від заміни,
  яка обходиться прямим доступом.
------------------------------------------------------------------------------*/
void cmdDelete()
{
    const long total = recordCount(DATA_FILE);

    if (total < 0) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }
    if (total == 0) {
        std::cout << "Файл порожній — видаляти нічого.\n";
        return;
    }
    if (total > MAX_RECORDS) {
        std::cout << "Файл містить більше " << MAX_RECORDS
                  << " записів — видалення неможливе.\n";
        return;
    }

    int number = 0;
    char prompt[80];
    snprintf(prompt, sizeof prompt, "Номер запису для видалення (1..%ld): ", total);

    if (!readInt(prompt, &number, 1, (int)total))
        return;

    /* Крок 1: зчитати всі записи, крім вибраного. */
    std::ifstream input;
    input.open(DATA_FILE, std::ios::binary);

    if (!input) {
        std::cout << "Помилка: не вдалося відкрити файл для читання.\n";
        return;
    }

    Sale records[MAX_RECORDS];
    long kept = 0;
    long index = 0;
    Sale sale;

    while (input.read((char *)&sale, sizeof(Sale))) {
        ++index;
        if (index == number)
            continue; /* вибраний запис пропускається */
        records[kept++] = sale;
    }
    input.close();

    /* Крок 2: перезаписати файл без вилученого запису. */
    std::ofstream output;
    output.open(DATA_FILE, std::ios::binary | std::ios::trunc);

    if (!output) {
        std::cout << "Помилка: не вдалося відкрити файл для запису.\n";
        return;
    }

    output.write((const char *)records, kept * sizeof(Sale));
    output.close();

    std::cout << "\nЗапис " << number << " видалено. Залишилось записів: " << kept
              << ".\n";
}

/*==============================================================================
  Запити. Усі дані беруться з файлу, результат записується до нового
  бінарного файлу і виводиться на екран з цього файлу.
==============================================================================*/

/*------------------------------------------------------------------------------
  cmdQueryComputers — запит 1: список комп'ютерів, що продаються у заданому
                      регіоні конкретною фірмою.

  Знайдені записи записуються до файлу COMPUTERS_FILE, після чого вміст
  цього файлу виводиться на екран.

  Локальні змінні:
      input, output — файл даних і файл результатів;
      region, firm  — ключі пошуку;
      found, total  — кількість і сумарна вартість знайдених записів.
------------------------------------------------------------------------------*/
void cmdQueryComputers()
{
    char region[MAX_NAME], firm[MAX_NAME];

    if (!readLine("Уведіть регіон збуту: ", region, MAX_NAME))
        return;
    if (!readLine("Уведіть назву фірми:  ", firm, MAX_NAME))
        return;

    std::ifstream input;

    input.open(DATA_FILE, std::ios::binary);

    if (!input) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }

    std::ofstream output;

    output.open(COMPUTERS_FILE, std::ios::binary | std::ios::trunc);

    if (!output) {
        std::cout << "Помилка: не вдалося створити файл " << COMPUTERS_FILE << ".\n";
        return;
    }

    Sale sale;

    while (input.read((char *)&sale, sizeof(Sale)))
        if (sale.kind == KIND_COMPUTER && std::strcmp(sale.region, region) == 0 &&
            std::strcmp(sale.firm, firm) == 0)
            output.write((const char *)&sale, sizeof(Sale));

    input.close();
    output.close();

    std::cout << "\nЗапит 1. Комп'ютери, що продаються у регіоні \"" << region
              << "\" фірмою \"" << firm << "\"\n"
              << "Результат записано до бінарного файлу " << COMPUTERS_FILE << "\n\n";

    double total = 0.0;
    const long found = printSalesFile(COMPUTERS_FILE, &total);

    if (found <= 0)
        std::cout << "  За заданими ключами пошуку записів не знайдено.\n";
    else
        std::cout << "\n  Знайдено записів: " << found
                  << ", сумарна вартість: " << std::fixed << std::setprecision(2)
                  << total << '\n';
}

/*------------------------------------------------------------------------------
  cmdQuerySoftware — запит 2: вартість проданого програмного забезпечення
                     у задані терміни.

  Знайдені записи записуються до файлу SOFTWARE_FILE, після чого вміст
  цього файлу виводиться на екран.
------------------------------------------------------------------------------*/
void cmdQuerySoftware()
{
    Date from, to;

    std::cout << "Початок періоду постачання:\n";
    if (!readInt("  рік (2000..2100): ", &from.year, 2000, 2100))
        return;
    if (!readInt("  місяць (1..12): ", &from.month, 1, 12))
        return;
    if (!readInt("  день (1..31): ", &from.day, 1, 31))
        return;

    std::cout << "Кінець періоду постачання:\n";
    if (!readInt("  рік (2000..2100): ", &to.year, 2000, 2100))
        return;
    if (!readInt("  місяць (1..12): ", &to.month, 1, 12))
        return;
    if (!readInt("  день (1..31): ", &to.day, 1, 31))
        return;

    const long fromNumber = dateToNumber(&from);
    const long toNumber = dateToNumber(&to);

    if (fromNumber > toNumber) {
        std::cout << "\nПочаток періоду пізніший за його кінець — "
                     "період порожній.\n";
        return;
    }

    std::ifstream input;

    input.open(DATA_FILE, std::ios::binary);

    if (!input) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }

    std::ofstream output;

    output.open(SOFTWARE_FILE, std::ios::binary | std::ios::trunc);

    if (!output) {
        std::cout << "Помилка: не вдалося створити файл " << SOFTWARE_FILE << ".\n";
        return;
    }

    Sale sale;

    while (input.read((char *)&sale, sizeof(Sale))) {
        const long deliveryNumber = dateToNumber(&sale.delivery);

        if (sale.kind == KIND_SOFTWARE && deliveryNumber >= fromNumber &&
            deliveryNumber <= toNumber)
            output.write((const char *)&sale, sizeof(Sale));
    }

    input.close();
    output.close();

    char fromText[16], toText[16];
    snprintf(fromText, sizeof fromText, "%02d.%02d.%d", from.day, from.month,
             from.year);
    snprintf(toText, sizeof toText, "%02d.%02d.%d", to.day, to.month, to.year);

    std::cout << "\nЗапит 2. Програмне забезпечення з терміном постачання з "
              << fromText << " до " << toText << "\n"
              << "Результат записано до бінарного файлу " << SOFTWARE_FILE << "\n\n";

    double total = 0.0;
    const long found = printSalesFile(SOFTWARE_FILE, &total);

    if (found <= 0)
        std::cout << "  У заданий період програмне забезпечення "
                     "не постачалося.\n";
    else
        std::cout << "\n  Знайдено записів: " << found
                  << "\n  Вартість проданого програмного забезпечення: " << std::fixed
                  << std::setprecision(2) << total << '\n';
}

/*------------------------------------------------------------------------------
  cmdQueryFirms — запит 3: найрентабельніші фірми (з найбільшою вартістю
                  продажів).

  Записи зчитуються з файлу й групуються за назвою фірми. ВСІ фірми
  з максимальною сумою (найбільших значень може бути кілька) записуються
  до файлу FIRMS_FILE, після чого вміст цього файлу виводиться на екран.

  Локальні змінні:
      firms     — сумарні продажі кожної фірми;
      firmCount — кількість різних фірм;
      maxTotal  — найбільша сумарна вартість.
------------------------------------------------------------------------------*/
void cmdQueryFirms()
{
    std::ifstream input;
    input.open(DATA_FILE, std::ios::binary);

    if (!input) {
        std::cout << "Файл " << DATA_FILE << " не існує. Спочатку створіть його.\n";
        return;
    }

    FirmTotal firms[MAX_RECORDS];
    int firmCount = 0;
    Sale sale;

    while (input.read((char *)&sale, sizeof(Sale))) {
        int position = -1;

        for (int j = 0; j < firmCount; ++j)
            if (std::strcmp(firms[j].firm, sale.firm) == 0) {
                position = j;
                break;
            }

        if (position < 0) {
            if (firmCount == MAX_RECORDS)
                break; /* файл містить більше записів, ніж допускає програма */

            position = firmCount++;
            std::strcpy(firms[position].firm, sale.firm);
            firms[position].sales = 0;
            firms[position].total = 0.0;
        }

        firms[position].total += sale.price;
        ++firms[position].sales;
    }

    input.close();

    if (firmCount == 0) {
        std::cout << "Файл порожній — даних для запиту немає.\n";
        return;
    }

    double maxTotal = firms[0].total;
    for (int j = 1; j < firmCount; ++j)
        if (firms[j].total > maxTotal)
            maxTotal = firms[j].total;

    std::cout << "\nЗапит 3. Сумарна вартість продажів по фірмах\n\n";
    printFirmHeader();
    for (int j = 0; j < firmCount; ++j)
        printFirmRow(&firms[j]);

    std::ofstream output;

    output.open(FIRMS_FILE, std::ios::binary | std::ios::trunc);

    if (!output) {
        std::cout << "Помилка: не вдалося створити файл " << FIRMS_FILE << ".\n";
        return;
    }

    for (int j = 0; j < firmCount; ++j)
        if (firms[j].total == maxTotal)
            output.write((const char *)&firms[j], sizeof(FirmTotal));

    output.close();

    /* Виведення результату з файлу, до якого його щойно записано. */
    std::ifstream result;
    result.open(FIRMS_FILE, std::ios::binary);

    if (!result) {
        std::cout << "Помилка: не вдалося відкрити файл " << FIRMS_FILE << ".\n";
        return;
    }

    std::cout << "\nНайрентабельніші фірми (з найбільшою вартістю продажів)\n"
              << "Результат записано до бінарного файлу " << FIRMS_FILE << "\n\n";
    printFirmHeader();

    FirmTotal firm;
    while (result.read((char *)&firm, sizeof(FirmTotal)))
        printFirmRow(&firm);

    result.close();
}

/*------------------------------------------------------------------------------
  Головна функція. Відображає меню та викликає відповідні функції.
  Локальні змінні: choice — номер обраного пункту меню.
------------------------------------------------------------------------------*/
int main()
{
    std::cout << "Лабораторна робота №11, завдання 2 (варіант 19)\n"
                 "Виконав: студент групи ІПЗ-11 Одарчук Олексій\n"
                 "Обробка бінарних файлів\n"
                 "Файл даних: "
              << DATA_FILE << "\n";

    for (;;) {
        std::cout << "\n============================================================\n"
                     "Меню команд:\n"
                     "  1 - створити масив структур і записати до бінарного файлу\n"
                     "  2 - вивести вміст бінарного файлу\n"
                     "  3 - доповнити файл новими записами\n"
                     "  4 - замінити вибраний запис (прямий доступ)\n"
                     "  5 - видалити вибраний запис\n"
                     "  6 - запит: комп'ютери у заданому регіоні заданої фірми\n"
                     "  7 - запит: вартість проданого ПЗ у задані терміни\n"
                     "  8 - запит: найрентабельніші фірми\n"
                     "  9 - вихід\n";

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

        std::cout << '\n';

        if (choice == 1)
            cmdCreateFile();
        else if (choice == 2)
            cmdPrintFile();
        else if (choice == 3)
            cmdAppend();
        else if (choice == 4)
            cmdReplace();
        else if (choice == 5)
            cmdDelete();
        else if (choice == 6)
            cmdQueryComputers();
        else if (choice == 7)
            cmdQuerySoftware();
        else if (choice == 8)
            cmdQueryFirms();
        else {
            std::cout << "Завершення роботи.\n";
            break;
        }
    }

    return 0;
}
