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

  Умова (таблиця 11.2, завдання 19.1): створити текстовий файл, записавши
  в нього цілі числа, значення яких генерує генератор псевдовипадкових чисел
  у діапазоні, заданому користувачем з клавіатури. Знайти серед цих чисел
  такі, що є квадратами цілих чисел, і записати їх до нового текстового файлу.

  ВИМОГИ ДО ВИКОНАННЯ ЗАВДАННЯ 1:
    - завдання реалізовано за процедурно-орієнтованою технологією: код
      складається з функцій (введення даних, виведення результатів, розрахунок),
      а основна програма викликає їх залежно від вибору команд меню;
    - передбачено меню команд, які виконують окремі операції з обробки файлу;
    - програма створює текстовий файл введенням даних з клавіатури
      (додатково до генерації, якої вимагає варіант);
    - програма виводить вміст файлу на екран;
    - у кожній функції вхідні файли відкриваються, а вихідні закриваються.

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

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

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

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

/* Імена файлів: вхідного (з усіма числами) та вихідного (з квадратами). */
const char *SOURCE_FILE = "numbers.txt";
const char *SQUARE_FILE = "squares.txt";

/*------------------------------------------------------------------------------
  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);
    }
}

/*------------------------------------------------------------------------------
  isPerfectSquare — чи є число квадратом цілого числа.

  Від'ємні числа квадратами не є за означенням. Для невід'ємних обчислюється
  цілий корінь, після чого перевіряється рівність його квадрата вихідному
  числу. Порівнюються ЦІЛІ числа, а не дійсні: результат sqrt() для великих
  значень може бути на одиницю молодшого розряду меншим за точний, тому
  перевіряються три сусідні кандидати.

  Параметри:
      n    [вхідний]  — число, що перевіряється;
      root [вихідний] — адреса змінної для кореня; заповнюється, якщо число
                        є квадратом; може бути NULL.
  Повертає : true — число є квадратом цілого числа.

  Локальні змінні:
      candidate — наближене значення кореня;
      value     — кандидат на корінь, що перевіряється.
------------------------------------------------------------------------------*/
bool isPerfectSquare(long n, long *root)
{
    if (n < 0)
        return false;

    const long candidate = (long)sqrt((double)n);

    for (long value = candidate - 1; value <= candidate + 1; ++value) {
        if (value < 0)
            continue;

        if (value * value == n) {
            if (root != NULL)
                *root = value;
            return true;
        }
    }

    return false;
}

/*------------------------------------------------------------------------------
  createFileRandom — створити текстовий файл, заповнивши його
                     псевдовипадковими цілими числами із заданого діапазону.

  Файл відкривається на запис у цій самій функції та закривається до виходу
  з неї — відповідно до вимоги умови.

  Параметри:
      fileName [вхідний] — ім'я файлу;
      count    [вхідний] — кількість чисел;
      low, high [вхідні] — межі діапазону значень.
  Повертає : true — файл створено; false — файл не вдалося відкрити.

  Локальні змінні:
      file  — покажчик на структуру FILE;
      value — чергове згенероване число.
------------------------------------------------------------------------------*/
bool createFileRandom(const char *fileName, int count, int low, int high)
{
    FILE *const file = fopen(fileName, "w");

    if (file == NULL) {
        printf("Помилка: не вдалося створити файл \"%s\".\n", fileName);
        return false;
    }

    const int range = high - low + 1;

    /* Масштабування rand() на діапазон замість rand() % range: стандарт гарантує
       лише RAND_MAX >= 32767, і для ширшого діапазону остача не охопила б
       усіх значень. */
    for (int i = 0; i < count; ++i) {
        const int value =
            low + (int)((double)rand() / ((double)RAND_MAX + 1.0) * range);
        fprintf(file, "%d\n", value);
    }

    fclose(file);
    printf("Файл \"%s\" створено. Кількість чисел: %d, діапазон [%d; %d].\n", fileName,
           count, low, high);
    return true;
}

/*------------------------------------------------------------------------------
  createFileKeyboard — створити текстовий файл, увівши числа з клавіатури.

  Параметри:
      fileName [вхідний] — ім'я файлу;
      count    [вхідний] — кількість чисел.
  Повертає : true — файл створено; false — помилка або переривання введення.
------------------------------------------------------------------------------*/
bool createFileKeyboard(const char *fileName, int count)
{
    FILE *const file = fopen(fileName, "w");

    if (file == NULL) {
        printf("Помилка: не вдалося створити файл \"%s\".\n", fileName);
        return false;
    }

    printf("Уведіть цілі числа (усього %d):\n", count);

    for (int i = 0; i < count; ++i) {
        char prompt[40];
        snprintf(prompt, sizeof prompt, "  число %d: ", i + 1);

        int value = 0;
        if (!readInt(prompt, &value, -1000000, 1000000)) {
            fclose(file); /* закрити файл навіть при перериванні */
            return false;
        }

        fprintf(file, "%d\n", value);
    }

    fclose(file);
    printf("Файл \"%s\" створено. Кількість чисел: %d.\n", fileName, count);
    return true;
}

/*------------------------------------------------------------------------------
  printFile — вивести вміст текстового файлу на екран.

  Числа виводяться по десять у рядку, щоб великий файл лишався читабельним.

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

  Локальні змінні:
      file  — покажчик на структуру FILE;
      value — прочитане число;
      count — лічильник прочитаних чисел.
------------------------------------------------------------------------------*/
long printFile(const char *fileName)
{
    FILE *const file = fopen(fileName, "r");

    if (file == NULL) {
        printf("Файл \"%s\" не існує. Спочатку створіть його.\n", fileName);
        return -1;
    }

    printf("Вміст файлу \"%s\":\n ", fileName);

    long value = 0;
    long count = 0;

    while (fscanf(file, "%ld", &value) == 1) {
        printf(" %8ld", value);
        ++count;

        if (count % 10 == 0)
            printf("\n ");
    }

    if (count % 10 != 0)
        printf("\n");

    fclose(file);

    printf("Усього чисел: %ld\n", count);
    return count;
}

/*------------------------------------------------------------------------------
  extractSquares — знайти у вхідному файлі числа, що є квадратами цілих чисел,
                   і записати їх до нового файлу.

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

  Параметри:
      sourceName [вхідний] — ім'я вхідного файлу;
      targetName [вхідний] — ім'я файлу для результату;
      total      [вихідний] — адреса лічильника переглянутих чисел.
  Повертає : кількість знайдених квадратів або -1 у разі помилки.

  Локальні змінні:
      source, target — покажчики на структури FILE;
      value — чергове прочитане число;
      root  — цілий корінь знайденого квадрата;
      found — лічильник знайдених квадратів.
------------------------------------------------------------------------------*/
long extractSquares(const char *sourceName, const char *targetName, long *total)
{
    FILE *source;
    FILE *target;

    source = fopen(sourceName, "r");

    if (source == NULL) {
        printf("Файл \"%s\" не існує. Спочатку створіть його.\n", sourceName);
        return -1;
    }

    target = fopen(targetName, "w");

    if (target == NULL) {
        printf("Помилка: не вдалося створити файл \"%s\".\n", targetName);
        fclose(source);
        return -1;
    }

    long value = 0, found = 0;
    *total = 0;

    printf("Знайдені квадрати цілих чисел:\n");

    while (fscanf(source, "%ld", &value) == 1) {
        ++(*total);

        long root = 0;
        if (isPerfectSquare(value, &root)) {
            fprintf(target, "%ld\n", value);
            printf("  %ld = %ld^2\n", value, root);
            ++found;
        }
    }

    fclose(source);
    fclose(target);

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

    return found;
}

/*------------------------------------------------------------------------------
  cmdCreate — команда меню: створити вхідний текстовий файл.
------------------------------------------------------------------------------*/
void cmdCreate()
{
    int count = 0;
    if (!readInt("Уведіть кількість чисел (1..10000): ", &count, 1, 10000))
        return;

    printf("Спосіб створення файлу:\n"
           "  1 - введення чисел з клавіатури\n"
           "  2 - генерація псевдовипадкових чисел у заданому діапазоні\n");

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

    if (choice == 1) {
        createFileKeyboard(SOURCE_FILE, count);
        return;
    }

    int low = 0, high = 0;
    if (!readInt("Уведіть нижню межу діапазону: ", &low, -1000000, 1000000))
        return;
    if (!readInt("Уведіть верхню межу діапазону: ", &high, low, 1000000))
        return;

    srand((unsigned)time(NULL));
    createFileRandom(SOURCE_FILE, count, low, high);
}

/*------------------------------------------------------------------------------
  cmdExtract — команда меню: відібрати квадрати цілих чисел у новий файл.
------------------------------------------------------------------------------*/
void cmdExtract()
{
    long total = 0;
    const long found = extractSquares(SOURCE_FILE, SQUARE_FILE, &total);

    if (found < 0)
        return;

    printf("\nПереглянуто чисел: %ld\n", total);
    printf("Знайдено квадратів: %ld\n", found);
    printf("Результат записано до файлу \"%s\".\n", SQUARE_FILE);
}

/*------------------------------------------------------------------------------
  Головна функція. Відображає меню та викликає відповідні функції обробки файлів.

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

    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;
        }

        printf("\n");

        if (choice == 1)
            cmdCreate();
        else if (choice == 2)
            printFile(SOURCE_FILE);
        else if (choice == 3)
            cmdExtract();
        else if (choice == 4)
            printFile(SQUARE_FILE);
        else {
            printf("Завершення роботи.\n");
            break;
        }
    }

    return 0;
}
