text: // Copyright 2010-2014 Greg Hurrell. All rights reserved.
  // Licensed under the terms of the BSD 2-clause license.
  
  #include <stdlib.h>  /* for qsort() */
  #include <string.h>  /* for strncmp() */
  #include "matcher.h"
  #include "match.h"
  #include "ext.h"
  #include "ruby_compat.h"
  
  // order matters; we want this to be evaluated only after ruby.h
  #ifdef HAVE_PTHREAD_H
  #include <pthread.h> /* for pthread_create, pthread_join etc */
  #endif
  
  // comparison function for use with qsort
  int cmp_alpha(const void *a, const void *b)
  {
      match_t a_match = *(match_t *)a;
      match_t b_match = *(match_t *)b;
      VALUE   a_str   = a_match.path;
      VALUE   b_str   = b_match.path;
      char    *a_p    = RSTRING_PTR(a_str);
      long    a_len   = RSTRING_LEN(a_str);
      char    *b_p    = RSTRING_PTR(b_str);
      long    b_len   = RSTRING_LEN(b_str);
      int     order   = 0;
  
      if (a_len > b_len) {
          order = strncmp(a_p, b_p, b_len);
          if (order == 0)
              order = 1; // shorter string (b) wins
      } else if (a_len < b_len) {
          order = strncmp(a_p, b_p, a_len);
          if (order == 0)
              order = -1; // shorter string (a) wins
      } else {
          order = strncmp(a_p, b_p, a_len);
      }
  
      return order;
  }
  
  // comparison function for use with qsort
  int cmp_score(const void *a, const void *b)
  {
      match_t a_match = *(match_t *)a;
      match_t b_match = *(match_t *)b;
  
      if (a_match.score > b_match.score)
          return -1; // a scores higher, a should appear sooner
      else if (a_match.score < b_match.score)
          return 1;  // b scores higher, a should appear later
      else
          return cmp_alpha(a, b);
  }
  
  VALUE CommandTMatcher_initialize(int argc, VALUE *argv, VALUE self)
  {
      VALUE always_show_dot_files;
      VALUE never_show_dot_files;
      VALUE options;
      VALUE scanner;
  
      // process arguments: 1 mandatory, 1 optional
      if (rb_scan_args(argc, argv, "11", &scanner, &options) == 1)
          options = Qnil;
      if (NIL_P(scanner))
          rb_raise(rb_eArgError, "nil scanner");
  
      rb_iv_set(self, "@scanner", scanner);
  
      // check optional options hash for overrides
      always_show_dot_files = CommandT_option_from_hash("always_show_dot_files", options);
      never_show_dot_files = CommandT_option_from_hash("never_show_dot_files", options);
  
      rb_iv_set(self, "@always_show_dot_files", always_show_dot_files);
      rb_iv_set(self, "@never_show_dot_files", never_show_dot_files);
  
      return Qnil;
  }
  
  typedef struct {
      int thread_count;
      int thread_index;
      int case_sensitive;
      match_t *matches;
      long path_count;
      VALUE paths;
      VALUE abbrev;
      VALUE always_show_dot_files;
      VALUE never_show_dot_files;
  } thread_args_t;
  
  void *match_thread(void *thread_args)
  {
      long i;
      thread_args_t *args = (thread_args_t *)thread_args;
      for (i = args->thread_index; i < args->path_count; i += args->thread_count) {
          VALUE path = RARRAY_PTR(args->paths)[i];
          calculate_match(path,
                          args->abbrev,
                          args->case_sensitive,
                          args->always_show_dot_files,
                          args->never_show_dot_files,
                          &args->matches[i]);
      }
  
      return NULL;
  }
  
  VALUE CommandTMatcher_sorted_matches_for(int argc, VALUE *argv, VALUE self)
  {
      long i, limit, path_count, thread_count;
  #ifdef HAVE_PTHREAD_H
      long err;
      pthread_t *threads;
  #endif
      match_t *matches;
      thread_args_t *thread_args;
      VALUE abbrev;
      VALUE case_sensitive;
      VALUE always_show_dot_files;
      VALUE limit_option;
      VALUE never_show_dot_files;
      VALUE options;
      VALUE paths;
      VALUE results;
      VALUE scanner;
      VALUE sort_option;
      VALUE threads_option;
  
      // process arguments: 1 mandatory, 1 optional
      if (rb_scan_args(argc, argv, "11", &abbrev, &options) == 1)
          options = Qnil;
      if (NIL_P(abbrev))
          rb_raise(rb_eArgError, "nil abbrev");
  
      // check optional options has for overrides
      case_sensitive = CommandT_option_from_hash("case_sensitive", options);
      limit_option = CommandT_option_from_hash("limit", options);
      threads_option = CommandT_option_from_hash("threads", options);
      sort_option = CommandT_option_from_hash("sort", options);
  
      abbrev = StringValue(abbrev);
      if (case_sensitive != Qtrue)
          abbrev = rb_funcall(abbrev, rb_intern("downcase"), 0);
  
      // get unsorted matches
      scanner = rb_iv_get(self, "@scanner");
      paths = rb_funcall(scanner, rb_intern("paths"), 0);
      always_show_dot_files = rb_iv_get(self, "@always_show_dot_files");
      never_show_dot_files = rb_iv_get(self, "@never_show_dot_files");
  
      path_count = RARRAY_LEN(paths);
      matches = malloc(path_count * sizeof(match_t));
      if (!matches)
          rb_raise(rb_eNoMemError, "memory allocation failed");
  
      thread_count = NIL_P(threads_option) ? 1 : NUM2LONG(threads_option);
  
  #ifdef HAVE_PTHREAD_H
  #define THREAD_THRESHOLD 1000 /* avoid the overhead of threading when search space is small */
      if (path_count < THREAD_THRESHOLD)
          thread_count = 1;
      threads = malloc(sizeof(pthread_t) * thread_count);
      if (!threads)
          rb_raise(rb_eNoMemError, "memory allocation failed");
  #endif
  
      thread_args = malloc(sizeof(thread_args_t) * thread_count);
      if (!thread_args)
          rb_raise(rb_eNoMemError, "memory allocation failed");
      for (i = 0; i < thread_count; i++) {
          thread_args[i].thread_count = thread_count;
          thread_args[i].thread_index = i;
          thread_args[i].case_sensitive = case_sensitive == Qtrue;
          thread_args[i].matches = matches;
          thread_args[i].path_count = path_count;
          thread_args[i].paths = paths;
          thread_args[i].abbrev = abbrev;
          thread_args[i].always_show_dot_files = always_show_dot_files;
          thread_args[i].never_show_dot_files = never_show_dot_files;
  
  #ifdef HAVE_PTHREAD_H
          if (i == thread_count - 1) {
  #endif
              // for the last "worker", we'll just use the main thread
              (void)match_thread(&thread_args[i]);
  #ifdef HAVE_PTHREAD_H
          } else {
              err = pthread_create(&threads[i], NULL, match_thread, (void *)&thread_args[i]);
              if (err != 0)
                  rb_raise(rb_eSystemCallError, "pthread_create() failure (%d)", (int)err);
          }
  #endif
      }
  
  #ifdef HAVE_PTHREAD_H
      for (i = 0; i < thread_count - 1; i++) {
          err = pthread_join(threads[i], NULL);
          if (err != 0)
              rb_raise(rb_eSystemCallError, "pthread_join() failure (%d)", (int)err);
      }
      free(threads);
  #endif
  
      if (NIL_P(sort_option) || sort_option == Qtrue) {
          if (RSTRING_LEN(abbrev) == 0 ||
              (RSTRING_LEN(abbrev) == 1 && RSTRING_PTR(abbrev)[0] == '.'))
              // alphabetic order if search string is only "" or "."
              qsort(matches, path_count, sizeof(match_t), cmp_alpha);
          else
              // for all other non-empty search strings, sort by score
              qsort(matches, path_count, sizeof(match_t), cmp_score);
      }
  
      results = rb_ary_new();
  
      limit = NIL_P(limit_option) ? 0 : NUM2LONG(limit_option);
      if (limit == 0)
          limit = path_count;
      for (i = 0; i < path_count && limit > 0; i++) {
          if (matches[i].score > 0.0) {
              rb_funcall(results, rb_intern("push"), 1, matches[i].path);
              limit--;
          }
      }
  
      free(matches);
      return results;
  }
  