@ 260927.1
current
#include <hypergrep/file_search.hpp>

file_search::file_search(std::string &pattern,
                         argparse::ArgumentParser &program) {
  initialize_search(pattern, program, options, &database, &scratch,
                    &file_filter_database, &file_filter_scratch);

  if (!program.is_used("-j")) {
    options.num_threads += 1;
  }
}

file_search::file_search(hs_database_t *database, hs_scratch_t *scratch,
                         const search_options &options)
    : database(database), scratch(scratch), options(options) {
  non_owning_database = true;
}

file_search::~file_search() {
  if (!non_owning_database) {
    if (scratch) {
      hs_free_scratch(scratch);
    }
    if (database) {
      hs_free_database(database);
    }
  }

  for (const auto &s : thread_local_scratch) {
    hs_free_scratch(s);
  }
}

void file_search::run(std::filesystem::path path,
                      std::optional<std::size_t> maybe_file_size) {

  if (!options.perform_search) {
    if (options.is_stdout) {
      fmt::print(fg(fmt::color::steel_blue), "{}\n", path.c_str());
    } else {
      fmt::print("{}\n", path.c_str());
    }
    return;
  }

  if (!database) {
    throw std::runtime_error("Database is NULL");
  }
  // Set up the scratch space
  hs_scratch_t *local_scratch = NULL;
  hs_error_t database_error = hs_alloc_scratch(database, &local_scratch);
  if (database_error != HS_SUCCESS) {
    throw std::runtime_error("Error allocating scratch space");
  }
  thread_local_scratch.push_back(local_scratch);

  // Memory map and search file in chunks multithreaded
  mmap_and_scan(std::move(path), maybe_file_size);
}

struct chunk_result {
  char *start{nullptr};
  char *end{nullptr};
  std::vector<std::pair<unsigned long long, unsigned long long>> matches{};
  std::size_t line_count{0};
};

bool file_search::mmap_and_scan(std::string &&filename,
                                std::optional<std::size_t> maybe_file_size) {
  int fd = open(filename.data(), O_RDONLY, 0);
  if (fd == -1) {
    std::cerr << filename << ": " << std::strerror(errno) << " (os error " << errno << ")\n";
    return false;
  }

  // Get the size of the file
  std::size_t file_size{0};
  if (maybe_file_size.has_value()) {
    file_size = maybe_file_size.value();
  } else {
    struct stat sb;
    if (fstat(fd, &sb) == -1) {
      return false;
    }
    file_size = sb.st_size;
  }

  // Memory map the file
  char *buffer = (char *)mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
  if (buffer == MAP_FAILED) {
    return false;
  }

  const auto process_fn =
      (options.is_stdout || options.print_only_matching_parts ||
       options.show_column_numbers || options.show_byte_offset)
          ? process_matches
          : process_matches_nocolor_nostdout;

  // Use the data

  // NOTE: hs_scan takes a `unsigned int` buffer size (2^32-1)
  // If the file_size is larger than that (e.g., 13GB), then
  // the search would need to be performed in chunks
  std::size_t max_searchable_size =
      FILE_CHUNK_SIZE; // std::numeric_limits<unsigned int>::max() - 1; // max
                       // that hs_scan can handle

  // Algorithm:
  // Spawn N-1 threads (N = max hardware concurrency)
  // Each thread will be given: buffer, file_size, offset
  // Each thread will search from the bytes {buffer + offset, buffer + offset +
  // search_size} Each thread will print its results

  auto max_concurrency = options.num_threads;
  if (max_concurrency > 1) {
    max_concurrency -= 1;
  }

  // Each thread will enqueue its results into its queue
  moodycamel::ConcurrentQueue<chunk_result> *output_queues =
      new moodycamel::ConcurrentQueue<chunk_result>[max_concurrency];

  std::vector<std::thread> threads(max_concurrency);
  thread_local_scratch.reserve(max_concurrency);

  std::atomic<std::size_t> num_threads_finished{0};
  std::atomic<std::size_t> num_results_enqueued{0}, num_results_dequeued{0};
  std::atomic<std::size_t> num_matches{0};
  std::atomic<bool> single_match_found{false};

  for (std::size_t i = 0; i < max_concurrency; ++i) {

    // Set up the scratch space
    hs_scratch_t *local_scratch = NULL;
    hs_error_t database_error = hs_alloc_scratch(database, &local_scratch);
    if (database_error != HS_SUCCESS) {
      fprintf(stderr, "Error allocating scratch space\n");
      hs_free_database(database);
      return false;
    }
    thread_local_scratch.push_back(local_scratch);

    // Spawn a reader thread
    threads[i] = std::thread(
        [this, i = i, max_concurrency = max_concurrency, buffer = buffer,
         file_size = file_size, max_searchable_size = max_searchable_size,
         &output_queues, &num_results_enqueued, &num_threads_finished,
         &single_match_found, &num_matches]() {
          // Set up the scratch space
          hs_scratch_t *local_scratch = thread_local_scratch[i];

          std::size_t offset{i * max_searchable_size};
          char *eof = buffer + file_size;

          while (true) {

            if (options.print_only_filenames && single_match_found) {
              break;
            }

            char *start = buffer + offset;
            if (start >= buffer + file_size) {
              // stop here
              num_threads_finished += 1;
              break;
            }

            if (offset > 0) {
              // Adjust start to go back to a newline boundary
              // Adjust end as well to go back to a newline boundary
              std::string_view chunk(buffer, start - buffer);

              auto last_newline = chunk.find_last_of('\n', start - buffer);
              if (last_newline == std::string_view::npos) {
                // No newline found, do nothing?
                // TODO: This could be an error scenario, check
              } else {
                start = buffer + last_newline;
              }
            }

            char *end = buffer + offset + max_searchable_size;
            if (end > eof) {
              end = eof;
            }

            // Update end to stop at a newline boundary
            std::string_view chunk(start, end - start);
            if (end != eof) {
              auto last_newline = chunk.find_last_of('\n', end - start);
              if (last_newline == std::string_view::npos) {
                // No newline found, do nothing?
                // TODO: This could be an error scenario, check
              } else {
                end = start + last_newline;
              }
            }

            // Perform the search
            std::vector<std::pair<unsigned long long, unsigned long long>>
                matches{};
            std::atomic<size_t> number_of_matches = 0;
            file_context ctx{number_of_matches, matches,
                             options.print_only_filenames};

            if (hs_scan(database, start, end - start, 0, local_scratch,
                        on_match, (void *)(&ctx)) != HS_SUCCESS) {
              if (options.print_only_filenames && ctx.number_of_matches > 0) {
                single_match_found = true;
              }
              num_threads_finished += 1;
              break;
            }

            num_matches += ctx.number_of_matches;

            // Save result
            std::size_t line_count_at_end_of_chunk =
                std::count(start, end, '\n');
            chunk_result local_chunk_result{start, end, std::move(matches),
                                            line_count_at_end_of_chunk};
            output_queues[i].enqueue(std::move(local_chunk_result));
            num_results_enqueued += 1;

            offset += max_concurrency * max_searchable_size;
          }
        });
  }

  std::size_t num_matching_lines{0};
  bool filename_printed{false};
  std::size_t current_line_number = 1;
  std::size_t i = 0;

  if (!options.print_only_filenames) {
    // In this main thread
    // Dequeue from output_queues, process matches
    // and print output
    while (!(num_threads_finished == max_concurrency &&
             num_results_enqueued == num_results_dequeued)) {
      chunk_result next_result{};

      auto found = output_queues[i].try_dequeue(next_result);
      if (found) {

        // Do something with result
        // Print it
        std::string lines{};
        auto start = next_result.start;
        auto end = next_result.end;
        auto &matches = next_result.matches;
        if (!matches.empty()) {
          std::size_t previous_line_number = current_line_number;
          num_matching_lines += process_fn(
              filename.data(), start, end - start, next_result.matches,
              previous_line_number, lines, options.print_filenames,
              options.is_stdout, options.show_line_numbers,
              options.show_column_numbers, options.show_byte_offset,
              options.print_only_matching_parts, options.max_column_limit,
              (start - buffer), options.ltrim_each_output_line);

          if (!options.count_matching_lines && !options.count_matches &&
              !options.print_only_filenames && !lines.empty()) {

            if (options.print_filenames && !filename_printed) {
              if (options.is_stdout) {
                fmt::print(fg(fmt::color::steel_blue), "{}\n", filename);
              }
              filename_printed = true;
            }

            fmt::print("{}", lines);
          }
        }
        current_line_number += next_result.line_count;

        num_results_dequeued += 1;

        i += 1;
        if (i == max_concurrency) {
          i = 0;
        }
      }
    }
  }

  for (auto &t : threads) {
    t.join();
  }

  if ((num_matching_lines > 0 || options.count_include_zeros) &&
      options.count_matching_lines && !options.print_only_filenames) {
    if (options.print_filenames) {
      if (options.is_stdout) {
        fmt::print("{}:{}\n",
                   fmt::format(fg(fmt::color::steel_blue), "{}", filename),
                   num_matching_lines);
      } else {
        fmt::print("{}:{}\n", filename, num_matching_lines);
      }
    } else {
      fmt::print("{}\n", num_matching_lines);
    }
  } else if ((num_matches.load() > 0 || options.count_include_zeros) &&
             options.count_matches && !options.print_only_filenames) {
    if (options.print_filenames) {
      if (options.is_stdout) {
        fmt::print("{}:{}\n",
                   fmt::format(fg(fmt::color::steel_blue), "{}", filename),
                   num_matches.load());
      } else {
        fmt::print("{}:{}\n", filename, num_matches.load());
      }
    } else {
      fmt::print("{}\n", num_matches.load());
    }
  } else if (options.print_only_filenames && single_match_found) {
    if (options.is_stdout) {
      fmt::print(fg(fmt::color::steel_blue), "{}\n", filename);
    } else {
      fmt::print("{}\n", filename);
    }
  }

  // Unmap the file
  if (munmap(buffer, file_size) == -1) {
    return false;
  }

  // Close the file
  if (close(fd) == -1) {
    return false;
  }

  delete[] output_queues;

  if (options.is_stdout && num_matching_lines > 0) {
    fmt::print("\n");
  }

  return true;
}

bool file_search::scan_line(std::string &line, std::size_t &current_line_number,
                            bool &break_loop) {
  static hs_scratch_t *local_scratch = NULL;
  static bool scratch_allocated = [this]() -> bool {
    if (!database) {
      throw std::runtime_error("Database is NULL");
    }
    // Set up the scratch space
    hs_error_t database_error = hs_alloc_scratch(database, &local_scratch);
    if (database_error != HS_SUCCESS) {
      throw std::runtime_error("Error allocating scratch space");
    }
    thread_local_scratch.push_back(local_scratch);

    return true;
  }();

  if (!scratch_allocated) {
    return false;
  }

  const auto process_fn =
      (options.is_stdout || options.print_only_matching_parts ||
       options.show_column_numbers || options.show_byte_offset)
          ? process_matches
          : process_matches_nocolor_nostdout;

  // Perform the search
  bool result{false};
  std::vector<std::pair<unsigned long long, unsigned long long>> matches{};
  std::atomic<size_t> number_of_matches = 0;
  file_context ctx{number_of_matches, matches, options.print_only_filenames};

  if (hs_scan(database, line.data(), line.size(), 0, local_scratch, on_match,
              (void *)(&ctx)) != HS_SUCCESS) {
    if (options.print_only_filenames && ctx.number_of_matches > 0) {
      break_loop = true;
    }
    result = false;
  } else {
    if (ctx.number_of_matches > 0) {
      result = true;
    }
  }

  // Process matches with this line number as the start line number
  // (for this chunk)
  if (ctx.number_of_matches > 0) {
    std::string lines{};
    const std::string filename{""};
    process_fn(filename.data(), line.data(), line.size(), ctx.matches,
               current_line_number, lines, false, options.is_stdout,
               options.show_line_numbers, options.show_column_numbers,
               options.show_byte_offset, options.print_only_matching_parts,
               options.max_column_limit, 0, options.ltrim_each_output_line);

    if (!options.count_matching_lines && !options.print_only_filenames &&
        result && !lines.empty()) {
      fmt::print("{}", lines);
    } else if (options.print_only_filenames) {
      if (options.is_stdout) {
        fmt::print(fg(fmt::color::steel_blue), "<stdin>\n");
      } else {
        fmt::print("<stdin>\n");
      }
    }
  }

  current_line_number += 1;

  return result;
}