/*
  Copyright (C) 2009-2014  Brazil
  Copyright (C) 2021-2023  Sutou Kouhei <kou@clear-code.com>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "grn.h"
#include <string.h>
#include <stddef.h>
#include "grn_snip.h"
#include "grn_ctx.h"

#if !defined MAX
#  define MAX(a, b) ((a) > (b) ? (a) : (b))
#endif

#if !defined MIN
#  define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif

static int
grn_bm_check_euc(const unsigned char *x, const size_t y)
{
  const unsigned char *p;
  for (p = x + y - 1; p >= x && *p >= 0x80U; p--)
    ;
  return (int)((x + y - p) & 1);
}

static int
grn_bm_check_sjis(const unsigned char *x, const size_t y)
{
  const unsigned char *p;
  for (p = x + y - 1; p >= x; p--)
    if ((*p < 0x81U) || (*p > 0x9fU && *p < 0xe0U) || (*p > 0xfcU)) break;
  return (int)((x + y - p) & 1);
}

/*
static void
grn_bm_suffixes(const unsigned char *x, size_t m, size_t *suff)
{
  size_t f, g;
  intptr_t i;
  f = 0;
  suff[m - 1] = m;
  g = m - 1;
  for (i = m - 2; i >= 0; --i) {
    if (i > (intptr_t) g && suff[i + m - 1 - f] < i - g)
      suff[i] = suff[i + m - 1 - f];
    else {
      if (i < (intptr_t) g)
        g = i;
      f = i;
      while (g > 0 && x[g] == x[g + m - 1 - f])
        --g;
      suff[i] = f - g;
    }
  }
}
*/

static void
grn_bm_preBmBc(const unsigned char *x, size_t m, size_t *bmBc)
{
  size_t i;
  for (i = 0; i < ASIZE; ++i) {
    bmBc[i] = m;
  }
  for (i = 0; i < m - 1; ++i) {
    bmBc[(unsigned int)x[i]] = m - (i + 1);
  }
}

#define GRN_BM_COMPARE                                                         \
  do {                                                                         \
    if (string_checks[found]) {                                                \
      size_t offset = cond->last_offset;                                       \
      ptrdiff_t found_alpha_head = cond->found_alpha_head;                     \
      /* calc real offset */                                                   \
      for (i = cond->last_found; i < found; i++) {                             \
        if (string_checks[i] > 0) {                                            \
          found_alpha_head = i;                                                \
          offset += (size_t)(string_checks[i]);                                \
        }                                                                      \
      }                                                                        \
      /* if real offset is in a character, move it the head of the character   \
       */                                                                      \
      if (string_checks[found] < 0) {                                          \
        offset -= (size_t)(string_checks[found_alpha_head]);                   \
        cond->last_found = found_alpha_head;                                   \
      } else {                                                                 \
        cond->last_found = found;                                              \
      }                                                                        \
      cond->start_offset = cond->last_offset = offset;                         \
      if (flags & GRN_SNIP_SKIP_LEADING_SPACES) {                              \
        while (cond->start_offset < string_original_length_in_bytes &&         \
               (i = grn_isspace(string_original + cond->start_offset,          \
                                string_encoding))) {                           \
          cond->start_offset += (size_t)(i);                                   \
        }                                                                      \
      }                                                                        \
      for (i = cond->last_found; i < found + m; i++) {                         \
        if (string_checks[i] > 0) {                                            \
          offset += (size_t)(string_checks[i]);                                \
        }                                                                      \
      }                                                                        \
      cond->end_offset = offset;                                               \
      cond->found = found + shift;                                             \
      cond->found_alpha_head = found_alpha_head;                               \
      /* printf("bm: cond:%p found:%zd last_found:%zd st_off:%zd               \
       * ed_off:%zd\n", cond,                                                  \
       * cond->found,cond->last_found,cond->start_offset,cond->end_offset); */ \
      return;                                                                  \
    }                                                                          \
  } while (0)

#define GRN_BM_BM_COMPARE                                                      \
  do {                                                                         \
    if (p[-2] == ck) {                                                         \
      for (i = 3; i <= m && p[-(intptr_t)i] == cp[-(intptr_t)i]; ++i) {        \
      }                                                                        \
      if (i > m) {                                                             \
        found = p - y - m;                                                     \
        GRN_BM_COMPARE;                                                        \
      }                                                                        \
    }                                                                          \
  } while (0)

void
grn_bm_tunedbm(grn_ctx *ctx, snip_cond *cond, grn_obj *string, int flags)
{
  register unsigned char *limit, ck;
  register const unsigned char *p, *cp;
  register size_t *bmBc, delta1;
  register ptrdiff_t i;

  const unsigned char *x;
  unsigned char *y;
  ptrdiff_t shift;
  ptrdiff_t found;

  const char *string_original;
  unsigned int string_original_length_in_bytes;
  const short *string_checks;
  grn_encoding string_encoding;
  const char *string_norm, *keyword_norm;
  unsigned int n, m;

  grn_string_get_original(ctx,
                          string,
                          &string_original,
                          &string_original_length_in_bytes);
  string_checks = grn_string_get_checks(ctx, string);
  string_encoding = grn_string_get_encoding(ctx, string);
  grn_string_get_normalized(ctx, string, &string_norm, &n, NULL);
  grn_string_get_normalized(ctx, cond->keyword, &keyword_norm, &m, NULL);

  y = (unsigned char *)string_norm;
  if (m == 1) {
    if (n > cond->found) {
      shift = 1;
      p = memchr(y + cond->found, keyword_norm[0], (size_t)(n - cond->found));
      if (p != NULL) {
        found = p - y;
        GRN_BM_COMPARE;
      }
    }
    cond->stopflag = SNIPCOND_STOP;
    return;
  }

  x = (unsigned char *)keyword_norm;
  bmBc = cond->bmBc;
  shift = cond->shift;

  /* Restart */
  p = y + m + cond->found;
  cp = x + m;
  ck = cp[-2];

  /* 12 means 1(initial offset) + 10 (in loop) + 1 (shift) */
  if (n - cond->found > 12 * m) {
    limit = y + n - 11 * m;
    while (p <= limit) {
      p += bmBc[p[-1]];
      if (!(delta1 = bmBc[p[-1]])) {
        goto check;
      }
      p += delta1;
      p += bmBc[p[-1]];
      p += bmBc[p[-1]];
      if (!(delta1 = bmBc[p[-1]])) {
        goto check;
      }
      p += delta1;
      p += bmBc[p[-1]];
      p += bmBc[p[-1]];
      if (!(delta1 = bmBc[p[-1]])) {
        goto check;
      }
      p += delta1;
      p += bmBc[p[-1]];
      p += bmBc[p[-1]];
      continue;
    check:
      GRN_BM_BM_COMPARE;
      p += shift;
    }
  }
  /* limit check + search */
  limit = y + n;
  while (p <= limit) {
    if (!(delta1 = bmBc[p[-1]])) {
      GRN_BM_BM_COMPARE;
      p += shift;
    }
    p += delta1;
  }
  cond->stopflag = SNIPCOND_STOP;
}

static size_t
count_mapped_chars(const char *str, const char *end)
{
  const char *p;
  size_t dl;

  dl = 0;
  for (p = str; p != end; p++) {
    switch (*p) {
    case '<':
    case '>':
      dl += 4; /* &lt; or &gt; */
      break;
    case '&':
      dl += 5; /* &amp; */
      break;
    case '"':
      dl += 6; /* &quot; */
      break;
    default:
      dl++;
      break;
    }
  }
  return dl;
}

grn_rc
grn_snip_cond_close(grn_ctx *ctx, snip_cond *cond)
{
  if (!cond) {
    return GRN_INVALID_ARGUMENT;
  }
  if (cond->keyword) {
    grn_obj_close(ctx, cond->keyword);
  }
  return GRN_SUCCESS;
}

static grn_rc
grn_snip_cond_init(grn_ctx *ctx,
                   snip_cond *sc,
                   const char *keyword,
                   unsigned int keyword_len,
                   grn_encoding enc,
                   grn_obj *normalizer,
                   int flags)
{
  const char *norm;
  unsigned int norm_blen;
  int f = GRN_STR_REMOVEBLANK;
  memset(sc, 0, sizeof(snip_cond));
  if (!(sc->keyword =
          grn_string_open(ctx, keyword, keyword_len, normalizer, f))) {
    GRN_LOG(ctx, GRN_LOG_ALERT, "grn_string_open on snip_cond_init failed!");
    return GRN_NO_MEMORY_AVAILABLE;
  }
  grn_string_get_normalized(ctx, sc->keyword, &norm, &norm_blen, NULL);
  if (!norm_blen) {
    grn_snip_cond_close(ctx, sc);
    return GRN_INVALID_ARGUMENT;
  }
  if (norm_blen != 1) {
    grn_bm_preBmBc((unsigned char *)norm, norm_blen, sc->bmBc);
    sc->shift = sc->bmBc[(unsigned char)norm[norm_blen - 1]];
    sc->bmBc[(unsigned char)norm[norm_blen - 1]] = 0;
  }
  return GRN_SUCCESS;
}

static void
grn_snip_cond_reinit(snip_cond *cond)
{
  cond->found = 0;
  cond->last_found = 0;
  cond->last_offset = 0;
  cond->start_offset = 0;
  cond->end_offset = 0;

  cond->count = 0;
  cond->stopflag = SNIPCOND_NONSTOP;
}

static inline char *
grn_snip_strndup(grn_ctx *ctx, const char *string, size_t string_len)
{
  char *copied_string;

  copied_string = GRN_MALLOC(string_len + 1);
  if (!copied_string) {
    return NULL;
  }
  grn_memcpy(copied_string, string, string_len);
  copied_string[string_len] = '\0'; /* not required, but for ql use */
  return copied_string;
}

static inline grn_rc
grn_snip_cond_set_tag(grn_ctx *ctx,
                      const char **dest_tag,
                      size_t *dest_tag_len,
                      const char *tag,
                      size_t tag_len,
                      const char *default_tag,
                      size_t default_tag_len,
                      int copy_tag)
{
  if (tag) {
    if (copy_tag) {
      char *copied_tag;
      copied_tag = grn_snip_strndup(ctx, tag, tag_len);
      if (!copied_tag) {
        return GRN_NO_MEMORY_AVAILABLE;
      }
      *dest_tag = copied_tag;
    } else {
      *dest_tag = tag;
    }
    *dest_tag_len = tag_len;
  } else {
    *dest_tag = default_tag;
    *dest_tag_len = default_tag_len;
  }
  return GRN_SUCCESS;
}

grn_rc
grn_snip_set_normalizer(grn_ctx *ctx, grn_obj *snip, grn_obj *normalizer)
{
  grn_snip *snip_;
  if (!snip) {
    return GRN_INVALID_ARGUMENT;
  }

  snip_ = (grn_snip *)snip;
  if (snip_->normalizer != normalizer) {
    if (snip_->normalizer && snip_->normalizer != GRN_NORMALIZER_AUTO) {
      grn_obj_unref(ctx, snip_->normalizer);
    }
    snip_->normalizer = normalizer;
    if (snip_->normalizer && snip_->normalizer != GRN_NORMALIZER_AUTO) {
      grn_obj_refer(ctx, snip_->normalizer);
    }
  }
  return GRN_SUCCESS;
}

grn_obj *
grn_snip_get_normalizer(grn_ctx *ctx, grn_obj *snip)
{
  grn_snip *snip_;

  if (!snip) {
    return NULL;
  }

  snip_ = (grn_snip *)snip;
  return snip_->normalizer;
}

grn_rc
grn_snip_set_normalizers(grn_ctx *ctx, grn_obj *snip, grn_obj *normalizers)
{
  grn_snip *snip_;
  if (!snip) {
    return GRN_INVALID_ARGUMENT;
  }

  if (!grn_obj_is_text_family_bulk(ctx, normalizers)) {
    return GRN_INVALID_ARGUMENT;
  }

  snip_ = (grn_snip *)snip;
  if (GRN_TEXT_LEN(normalizers) == 0) {
    GRN_BULK_REWIND(&(snip_->normalizers));
    return GRN_SUCCESS;
  }

  if (!snip_->lexicon) {
    /* This is only used for passing normalizer options to grn_string. */
    snip_->lexicon = grn_table_create(ctx,
                                      NULL,
                                      0,
                                      NULL,
                                      GRN_OBJ_TABLE_HASH_KEY,
                                      grn_ctx_at(ctx, GRN_DB_SHORT_TEXT),
                                      NULL);
    if (!snip_->lexicon) {
      return ctx->rc;
    }
  }
  grn_obj_set_info(ctx, snip_->lexicon, GRN_INFO_NORMALIZERS, normalizers);
  if (ctx->rc == GRN_SUCCESS) {
    GRN_TEXT_SET(ctx,
                 &(snip_->normalizers),
                 GRN_TEXT_VALUE(normalizers),
                 GRN_TEXT_LEN(normalizers));
  }
  return ctx->rc;
}

grn_obj *
grn_snip_get_normalizers(grn_ctx *ctx, grn_obj *snip)
{
  grn_snip *snip_;

  if (!snip) {
    return NULL;
  }

  snip_ = (grn_snip *)snip;
  return &(snip_->normalizers);
}

grn_rc
grn_snip_set_delimiter_regexp(grn_ctx *ctx,
                              grn_obj *snip,
                              const char *pattern,
                              int pattern_length)
{
  const char *tag = "[snip][delimiter-regexp][set]";

  if (!snip) {
    return GRN_INVALID_ARGUMENT;
  }

  GRN_API_ENTER;
#ifdef GRN_SUPPORT_REGEXP
  grn_snip *snip_ = (grn_snip *)snip;
  if (snip_->delimiter_regex) {
    onig_free(snip_->delimiter_regex);
  }
  if (snip_->delimiter_pattern) {
    GRN_FREE(snip_->delimiter_pattern);
  }
  if (pattern) {
    if (pattern_length < 0) {
      snip_->delimiter_pattern_length = strlen(pattern);
    } else {
      snip_->delimiter_pattern_length = (size_t)pattern_length;
    }
    snip_->delimiter_pattern =
      grn_snip_strndup(ctx, pattern, snip_->delimiter_pattern_length);
    if (snip_->delimiter_pattern) {
      snip_->delimiter_regex = grn_onigmo_new(ctx,
                                              pattern,
                                              snip_->delimiter_pattern_length,
                                              GRN_ONIGMO_OPTION_DEFAULT,
                                              GRN_ONIGMO_SYNTAX_DEFAULT,
                                              tag);
    }
  } else {
    snip_->delimiter_regex = NULL;
    snip_->delimiter_pattern = NULL;
  }
#else
  ERR(GRN_FUNCTION_NOT_IMPLEMENTED, "%s Onigmo isn't enabled", tag);
#endif
  GRN_API_RETURN(ctx->rc);
}

const char *
grn_snip_get_delimiter_regexp(grn_ctx *ctx, grn_obj *snip, size_t *length)
{
  grn_snip *snip_;

  if (!snip) {
    if (length) {
      *length = 0;
    }
    return NULL;
  }

  snip_ = (grn_snip *)snip;
  if (length) {
    *length = snip_->delimiter_pattern_length;
  }
  return snip_->delimiter_pattern;
}

static grn_rc
grn_snip_expand_cond(grn_ctx *ctx, grn_snip *snip)
{
  size_t new_cond_capacity = snip->cond_capacity * 2;
  size_t new_cond_size = sizeof(snip_cond) * new_cond_capacity;
  snip_cond *new_cond = (snip_cond *)GRN_REALLOC(snip->cond, new_cond_size);
  if (!new_cond) {
    return ctx->rc;
  }
  snip->cond = new_cond;
  snip->cond_capacity = new_cond_capacity;
  return GRN_SUCCESS;
}

grn_rc
grn_snip_add_cond(grn_ctx *ctx,
                  grn_obj *snip,
                  const char *keyword,
                  unsigned int keyword_len,
                  const char *opentag,
                  unsigned int opentag_len,
                  const char *closetag,
                  unsigned int closetag_len)
{
  grn_rc rc;
  int copy_tag;
  snip_cond *cond;
  unsigned int norm_blen;
  grn_snip *snip_;

  snip_ = (grn_snip *)snip;
  if (!snip_ || !keyword || !keyword_len) {
    return GRN_INVALID_ARGUMENT;
  }

  if (snip_->cond_len >= snip_->cond_capacity) {
    rc = grn_snip_expand_cond(ctx, snip_);
    if (rc) {
      return rc;
    }
  }

  cond = snip_->cond + snip_->cond_len;
  rc = grn_snip_cond_init(ctx,
                          cond,
                          keyword,
                          keyword_len,
                          snip_->encoding,
                          (GRN_TEXT_LEN(&(snip_->normalizers)) > 0)
                            ? snip_->lexicon
                            : snip_->normalizer,
                          snip_->flags);
  if (rc) {
    return rc;
  }
  grn_string_get_normalized(ctx, cond->keyword, NULL, &norm_blen, NULL);
  if (norm_blen > snip_->width) {
    grn_snip_cond_close(ctx, cond);
    return GRN_INVALID_ARGUMENT;
  }

  copy_tag = snip_->flags & GRN_SNIP_COPY_TAG;
  rc = grn_snip_cond_set_tag(ctx,
                             &(cond->opentag),
                             &(cond->opentag_len),
                             opentag,
                             opentag_len,
                             snip_->defaultopentag,
                             snip_->defaultopentag_len,
                             copy_tag);
  if (rc) {
    grn_snip_cond_close(ctx, cond);
    return rc;
  }

  rc = grn_snip_cond_set_tag(ctx,
                             &(cond->closetag),
                             &(cond->closetag_len),
                             closetag,
                             closetag_len,
                             snip_->defaultclosetag,
                             snip_->defaultclosetag_len,
                             copy_tag);
  if (rc) {
    if (opentag && copy_tag) {
      GRN_FREE((void *)cond->opentag);
    }
    grn_snip_cond_close(ctx, cond);
    return rc;
  }

  snip_->cond_len++;
  return GRN_SUCCESS;
}

static size_t
grn_snip_find_firstbyte(const char *string,
                        grn_encoding encoding,
                        size_t offset,
                        ssize_t doffset)
{
  switch (encoding) {
  case GRN_ENC_EUC_JP:
    while (!(grn_bm_check_euc((unsigned char *)string, offset)))
      offset = (size_t)((ssize_t)offset + doffset);
    break;
  case GRN_ENC_SJIS:
    if (!(grn_bm_check_sjis((unsigned char *)string, offset)))
      offset = (size_t)((ssize_t)offset + doffset);
    break;
  case GRN_ENC_UTF8:
    while ((signed char)string[offset] <= (signed char)0xc0)
      offset = (size_t)((ssize_t)offset + doffset);
    break;
  default:
    break;
  }
  return offset;
}

static inline grn_rc
grn_snip_set_default_tag(grn_ctx *ctx,
                         const char **dest_tag,
                         size_t *dest_tag_len,
                         const char *tag,
                         unsigned int tag_len,
                         int copy_tag)
{
  if (copy_tag && tag) {
    char *copied_tag;
    copied_tag = grn_snip_strndup(ctx, tag, tag_len);
    if (!copied_tag) {
      return GRN_NO_MEMORY_AVAILABLE;
    }
    *dest_tag = copied_tag;
  } else {
    *dest_tag = tag;
  }
  *dest_tag_len = tag_len;
  return GRN_SUCCESS;
}

grn_obj *
grn_snip_open(grn_ctx *ctx,
              int flags,
              unsigned int width,
              unsigned int max_results,
              const char *defaultopentag,
              unsigned int defaultopentag_len,
              const char *defaultclosetag,
              unsigned int defaultclosetag_len,
              grn_snip_mapping *mapping)
{
  int copy_tag;
  grn_snip *ret = NULL;
  if (!(ret = GRN_CALLOC(sizeof(grn_snip)))) {
    GRN_LOG(ctx, GRN_LOG_ALERT, "grn_snip allocation failed on grn_snip_open");
    return NULL;
  }
  if (max_results > MAX_SNIP_RESULT_COUNT || max_results == 0) {
    GRN_LOG(ctx, GRN_LOG_WARNING, "max_results is invalid on grn_snip_open");
    GRN_FREE(ret);
    return NULL;
  }
  GRN_API_ENTER;
  ret->encoding = ctx->encoding;
  ret->flags = flags;
  ret->width = width;
  ret->max_results = max_results;
  ret->defaultopentag = NULL;
  ret->defaultclosetag = NULL;

  copy_tag = flags & GRN_SNIP_COPY_TAG;
  if (grn_snip_set_default_tag(ctx,
                               &(ret->defaultopentag),
                               &(ret->defaultopentag_len),
                               defaultopentag,
                               defaultopentag_len,
                               copy_tag)) {
    GRN_FREE(ret);
    GRN_API_RETURN(NULL);
  }

  if (grn_snip_set_default_tag(ctx,
                               &(ret->defaultclosetag),
                               &(ret->defaultclosetag_len),
                               defaultclosetag,
                               defaultclosetag_len,
                               copy_tag)) {
    if (copy_tag && ret->defaultopentag) {
      GRN_FREE((void *)ret->defaultopentag);
    }
    GRN_FREE(ret);
    GRN_API_RETURN(NULL);
  }

  ret->cond_capacity = DEFAULT_SNIP_COND_CAPACITY;
  ret->cond = (snip_cond *)GRN_MALLOC(sizeof(snip_cond) * ret->cond_capacity);

  if (!ret->cond) {
    if (copy_tag) {
      if (ret->defaultopentag) {
        GRN_FREE((void *)ret->defaultopentag);
      }
      if (ret->defaultclosetag) {
        GRN_FREE((void *)ret->defaultclosetag);
      }
    }
    GRN_FREE(ret);
    GRN_API_RETURN(NULL);
  }

  ret->cond_len = 0;
  ret->mapping = mapping;
  ret->nstr = NULL;
  ret->tag_count = 0;
  ret->snip_count = 0;
  if (ret->flags & GRN_SNIP_NORMALIZE) {
    ret->normalizer = GRN_NORMALIZER_AUTO;
  } else {
    ret->normalizer = NULL;
  }
  ret->lexicon = NULL;
  GRN_TEXT_INIT(&(ret->normalizers), 0);
  ret->delimiter_pattern = NULL;
  ret->delimiter_pattern_length = 0;
#ifdef GRN_SUPPORT_REGEXP
  ret->delimiter_regex = NULL;
#endif

  GRN_DB_OBJ_SET_TYPE(ret, GRN_SNIP);
  {
    grn_obj *db;
    grn_id id;
    db = grn_ctx_db(ctx);
    id = grn_obj_register(ctx, db, NULL, 0);
    DB_OBJ(ret)->header.domain = GRN_ID_NIL;
    DB_OBJ(ret)->range = GRN_ID_NIL;
    grn_db_obj_init(ctx, db, id, DB_OBJ(ret));
  }

  GRN_API_RETURN((grn_obj *)ret);
}

static grn_rc
exec_clean(grn_ctx *ctx, grn_snip *snip)
{
  snip_cond *cond, *cond_end;
  if (snip->nstr) {
    grn_obj_close(ctx, snip->nstr);
    snip->nstr = NULL;
  }
  snip->tag_count = 0;
  snip->snip_count = 0;
  for (cond = snip->cond, cond_end = cond + snip->cond_len; cond < cond_end;
       cond++) {
    grn_snip_cond_reinit(cond);
  }
  return GRN_SUCCESS;
}

grn_rc
grn_snip_close(grn_ctx *ctx, grn_snip *snip)
{
  snip_cond *cond, *cond_end;
  if (!snip) {
    return GRN_INVALID_ARGUMENT;
  }
  GRN_API_ENTER;
#ifdef GRN_SUPPORT_REGEXP
  if (snip->delimiter_regex) {
    onig_free(snip->delimiter_regex);
  }
#endif
  if (snip->delimiter_pattern) {
    GRN_FREE(snip->delimiter_pattern);
  }
  if (snip->normalizer && snip->normalizer != GRN_NORMALIZER_AUTO) {
    grn_obj_unref(ctx, snip->normalizer);
  }
  if (snip->lexicon) {
    grn_obj_close(ctx, snip->lexicon);
  }
  GRN_OBJ_FIN(ctx, &(snip->normalizers));
  if (snip->flags & GRN_SNIP_COPY_TAG) {
    size_t i;
    snip_cond *sc;
    const char *dot = snip->defaultopentag, *dct = snip->defaultclosetag;
    for (i = snip->cond_len, sc = snip->cond; i; i--, sc++) {
      if (sc->opentag != dot) {
        GRN_FREE((void *)sc->opentag);
      }
      if (sc->closetag != dct) {
        GRN_FREE((void *)sc->closetag);
      }
    }
    if (dot) {
      GRN_FREE((void *)dot);
    }
    if (dct) {
      GRN_FREE((void *)dct);
    }
  }
  if (snip->nstr) {
    grn_obj_close(ctx, snip->nstr);
  }
  for (cond = snip->cond, cond_end = cond + snip->cond_len; cond < cond_end;
       cond++) {
    grn_snip_cond_close(ctx, cond);
  }
  GRN_FREE(snip->cond);
  GRN_FREE(snip);
  GRN_API_RETURN(GRN_SUCCESS);
}

grn_rc
grn_snip_exec(grn_ctx *ctx,
              grn_obj *snip,
              const char *string,
              unsigned int string_len,
              unsigned int *nresults,
              unsigned int *max_tagged_len)
{
  size_t i;
  grn_snip *snip_;
  int f = GRN_STR_WITH_CHECKS | GRN_STR_REMOVEBLANK;
  if (!snip || !string || !nresults || !max_tagged_len) {
    return GRN_INVALID_ARGUMENT;
  }
  GRN_API_ENTER;
  snip_ = (grn_snip *)snip;
  exec_clean(ctx, snip_);
  *nresults = 0;
  grn_obj *lexicon_or_normalizer = (GRN_TEXT_LEN(&(snip_->normalizers)) > 0)
                                     ? snip_->lexicon
                                     : snip_->normalizer;
  snip_->nstr =
    grn_string_open(ctx, string, string_len, lexicon_or_normalizer, f);
  if (!snip_->nstr) {
    exec_clean(ctx, snip_);
    GRN_LOG(ctx, GRN_LOG_ALERT, "grn_string_open on grn_snip_exec failed !");
    GRN_API_RETURN(ctx->rc);
  }
  for (i = 0; i < snip_->cond_len; i++) {
    grn_bm_tunedbm(ctx, snip_->cond + i, snip_->nstr, snip_->flags);
  }

  {
    _snip_tag_result *tag_result = snip_->tag_result;
    _snip_result *snip_result = snip_->snip_result;
    size_t last_end_offset = 0, last_last_end_offset = 0;
    size_t unfound_cond_count = snip_->cond_len;

    *max_tagged_len = 0;
    while (1) {
      size_t tagged_len = 0, last_tag_end = 0;
      int_least8_t all_stop = 1, found_cond = 0;
      snip_result->tag_count = 0;

      while (1) {
        size_t min_start_offset = (size_t)-1;
        size_t max_end_offset = 0;
        snip_cond *cond = NULL;

        /* get condition which have minimum offset and is not stopped */
        for (i = 0; i < snip_->cond_len; i++) {
          if (snip_->cond[i].stopflag == SNIPCOND_NONSTOP &&
              (min_start_offset > snip_->cond[i].start_offset ||
               (min_start_offset == snip_->cond[i].start_offset &&
                max_end_offset < snip_->cond[i].end_offset))) {
            min_start_offset = snip_->cond[i].start_offset;
            max_end_offset = snip_->cond[i].end_offset;
            cond = &snip_->cond[i];
          }
        }
        if (!cond) {
          break;
        }
        /* check whether condition is the first condition in snippet */
        if (snip_result->tag_count == 0) {
          /* skip condition if the number of rest snippet field is smaller than
           */
          /* the number of unfound keywords. */
          if (snip_->max_results - *nresults <= unfound_cond_count &&
              cond->count > 0) {
            bool exclude_other_cond = true;
            for (i = 0; i < snip_->cond_len; i++) {
              if ((snip_->cond + i) != cond &&
                  snip_->cond[i].end_offset <=
                    cond->start_offset + snip_->width &&
                  snip_->cond[i].count == 0) {
                exclude_other_cond = false;
                break;
              }
            }
            if (exclude_other_cond) {
              grn_bm_tunedbm(ctx, cond, snip_->nstr, snip_->flags);
              continue;
            }
          }
          snip_result->start_offset = cond->start_offset;
          snip_result->first_tag_result_idx = snip_->tag_count;
        } else {
#ifdef GRN_SUPPORT_REGEXP
          if (snip_->delimiter_regex) {
            size_t range_offset =
              MIN(snip_result->start_offset + snip_->width, cond->start_offset);
            OnigPosition position =
              onig_search(snip_->delimiter_regex,
                          string,
                          string + string_len,
                          string + snip_result->start_offset,
                          string + range_offset,
                          NULL,
                          ONIG_OPTION_NONE);
            if (position != ONIG_MISMATCH) {
              break;
            }
          }
#endif
          if (cond->start_offset >= snip_result->start_offset + snip_->width) {
            break;
          }
          /* check nesting to make valid HTML */
          /* ToDo: allow <test><te>te</te><st>st</st></test> */
          if (cond->start_offset < last_tag_end) {
            grn_bm_tunedbm(ctx, cond, snip_->nstr, snip_->flags);
            continue;
          }
        }
        if (cond->end_offset > snip_result->start_offset + snip_->width) {
          /* If a keyword gets across a snippet, */
          /* it was skipped and never to be tagged. */
          cond->stopflag = SNIPCOND_ACROSS;
          grn_bm_tunedbm(ctx, cond, snip_->nstr, snip_->flags);
        } else {
          found_cond = 1;
          if (cond->count == 0) {
            unfound_cond_count--;
          }
          cond->count++;
          last_end_offset = cond->end_offset;

          tag_result->cond = cond;
          tag_result->start_offset = cond->start_offset;
          tag_result->end_offset = last_tag_end = cond->end_offset;

          snip_result->tag_count++;
          tag_result++;
          tagged_len += cond->opentag_len + cond->closetag_len;
          if (++snip_->tag_count >= MAX_SNIP_TAG_COUNT) {
            break;
          }
          grn_bm_tunedbm(ctx, cond, snip_->nstr, snip_->flags);
        }
      }
      if (!found_cond) {
        break;
      }
      {
        /*
          No overlap case:
            keyword = hij
            width = 10
            string = abcdefghijklmnopqrstuvwxyz

            abcdefghijklmnopqrstuvwxyz
                   ^
                   match_start_offset
            abcdefghijklmnopqrstuvwxyz
                     ^
                     match_end_offset
            abcdefghijklmnopqrstuvwxyz
                   <->
                   match_width
            abcdefghijklmnopqrstuvwxyz
               <-->   <->
               around_width
            abcdefghijklmnopqrstuvwxyz
               <-->
               pre_width
            abcdefghijklmnopqrstuvwxyz
                      <->
                      post_width
            abcdefghijklmnopqrstuvwxyz
               <-------->
               snip_->width

          Overlapped case:
            keyword = hij
            width = 15
            string = abcdefghijklhijklmnopq

            abcdefghijklhijklmnopq
                   ^
                   match_start_offset
            abcdefghijklhijklmnopq
                          ^
                          match_end_offset
            abcdefghijklhijklmnopq
                   <------>
                   match_width
            abcdefghijklhijklmnopq
               <-->        <->
               around_width
            abcdefghijklhijklmnopq
               <-->
               pre_width
            abcdefghijklhijklmnopq
                           <->
                           post_width
            abcdefghijklhijklmnopq
               <------------->
               snip_->width

          Delimiter regexp case:
            keyword = hij
            width = 10
            delimiter_regexp = X
            string = abcdXfghijkXmnopqrstuvwxyz

            abcdXfghijkXmnopqrstuvwxyz
                   ^
                   match_start_offset
            abcdXfghijkXmnopqrstuvwxyz
                     ^
                     match_end_offset
            abcdXfghijkXmnopqrstuvwxyz
                   <->
                   match_width
            abcdXfghijkXmnopqrstuvwxyz
               <-->   <->
               around_width
            abcdXfghijkXmnopqrstuvwxyz
               <-->
               pre_width
            abcdXfghijkXmnopqrstuvwxyz
                      <->
                      post_width
            abcdXfghijkXmnopqrstuvwxyz
               <-------->
               snip_->width
            abcdXfghijkXmnopqrstuvwxyz
                 ^
                 snip_result->start_offset (after processed)
            abcdXfghijkXmnopqrstuvwxyz
                      ^
                      snip_result->end_offset (after processed)
         */
        size_t match_start_offset = snip_result->start_offset;
        size_t match_end_offset = last_end_offset;
        size_t match_width = match_end_offset - match_start_offset;
        size_t pre_width;
        size_t post_width;
        if (match_width >= snip_->width) {
          pre_width = post_width = 0;
        } else {
          size_t around_width = snip_->width - match_width;
          pre_width = post_width = around_width / 2;
          if (around_width % 2 == 1) {
            pre_width++;
          }
        }
        {
          size_t start_offset_min;
          if (match_start_offset < pre_width) {
            start_offset_min = 0;
          } else {
            if (match_end_offset + post_width >= string_len) {
              if (string_len < snip_->width) {
                start_offset_min = 0;
              } else {
                start_offset_min = string_len - snip_->width;
              }
            } else {
              start_offset_min = match_start_offset - pre_width;
            }
            start_offset_min = grn_snip_find_firstbyte(string,
                                                       snip_->encoding,
                                                       start_offset_min,
                                                       1);
          }
#ifdef GRN_SUPPORT_REGEXP
          if (snip_->delimiter_regex) {
            OnigRegion region;
            onig_region_init(&region);
            OnigPosition position = onig_search(snip_->delimiter_regex,
                                                string,
                                                string + string_len,
                                                string + match_start_offset,
                                                string + start_offset_min,
                                                &region,
                                                ONIG_OPTION_NONE);
            if (position != ONIG_MISMATCH) {
              start_offset_min = (size_t)(region.end[0]);
              onig_region_free(&region, 0);
            }
          }
#endif
          start_offset_min = MAX(start_offset_min, last_last_end_offset);
          snip_result->start_offset = grn_snip_find_firstbyte(string,
                                                              snip_->encoding,
                                                              start_offset_min,
                                                              1);
        }

        {
          size_t end_offset_max = snip_result->start_offset + snip_->width;
          if (end_offset_max < match_end_offset) {
            end_offset_max = match_end_offset + post_width;
          }
#ifdef GRN_SUPPORT_REGEXP
          if (snip_->delimiter_regex) {
            size_t valid_end_offset_max;
            if (end_offset_max >= string_len) {
              valid_end_offset_max = string_len;
            } else {
              valid_end_offset_max = grn_snip_find_firstbyte(string,
                                                             snip_->encoding,
                                                             end_offset_max,
                                                             -1);
            }
            OnigRegion region;
            onig_region_init(&region);
            OnigPosition position = onig_search(snip_->delimiter_regex,
                                                string,
                                                string + string_len,
                                                string + match_end_offset,
                                                string + valid_end_offset_max,
                                                &region,
                                                ONIG_OPTION_NONE);
            if (position != ONIG_MISMATCH) {
              end_offset_max = (size_t)(region.beg[0]);
              onig_region_free(&region, 0);
            }
          }
#endif
          if (end_offset_max >= string_len) {
            snip_result->end_offset = string_len;
          } else {
            snip_result->end_offset = grn_snip_find_firstbyte(string,
                                                              snip_->encoding,
                                                              end_offset_max,
                                                              -1);
          }
        }
      }
      last_last_end_offset = snip_result->end_offset;

      if (snip_->mapping == GRN_SNIP_MAPPING_HTML_ESCAPE) {
        tagged_len += count_mapped_chars(&string[snip_result->start_offset],
                                         &string[snip_result->end_offset]) +
                      1;
      } else {
        tagged_len += snip_result->end_offset - snip_result->start_offset + 1;
      }

      *max_tagged_len = MAX(*max_tagged_len, (unsigned int)tagged_len);

      snip_result->last_tag_result_idx = snip_->tag_count - 1;
      (*nresults)++;
      snip_result++;

      if (*nresults == snip_->max_results ||
          snip_->tag_count == MAX_SNIP_TAG_COUNT) {
        break;
      }
      for (i = 0; i < snip_->cond_len; i++) {
        if (snip_->cond[i].stopflag != SNIPCOND_STOP) {
          all_stop = 0;
          snip_->cond[i].stopflag = SNIPCOND_NONSTOP;
        }
      }
      if (all_stop) {
        break;
      }
    }
  }
  snip_->snip_count = *nresults;
  snip_->string = string;

  snip_->max_tagged_len = *max_tagged_len;

  GRN_API_RETURN(ctx->rc);
}

grn_rc
grn_snip_get_result(grn_ctx *ctx,
                    grn_obj *snip,
                    const unsigned int index,
                    char *result,
                    unsigned int *result_len)
{
  char *p;
  size_t i, j, k;
  _snip_result *sres;
  grn_snip *snip_;

  snip_ = (grn_snip *)snip;
  if (snip_->snip_count <= index || !snip_->nstr) {
    return GRN_INVALID_ARGUMENT;
  }

  GRN_ASSERT(snip_->snip_count != 0 && snip_->tag_count != 0);

  GRN_API_ENTER;
  sres = &snip_->snip_result[index];
  j = sres->first_tag_result_idx;
  for (p = result, i = sres->start_offset; i < sres->end_offset; i++) {
    for (; j <= sres->last_tag_result_idx &&
           snip_->tag_result[j].start_offset == i;
         j++) {
      if (snip_->tag_result[j].end_offset > sres->end_offset) {
        continue;
      }
      grn_memcpy(p,
                 snip_->tag_result[j].cond->opentag,
                 snip_->tag_result[j].cond->opentag_len);
      p += snip_->tag_result[j].cond->opentag_len;
    }

    if (snip_->mapping == GRN_SNIP_MAPPING_HTML_ESCAPE) {
      switch (snip_->string[i]) {
      case '<':
        *p++ = '&';
        *p++ = 'l';
        *p++ = 't';
        *p++ = ';';
        break;
      case '>':
        *p++ = '&';
        *p++ = 'g';
        *p++ = 't';
        *p++ = ';';
        break;
      case '&':
        *p++ = '&';
        *p++ = 'a';
        *p++ = 'm';
        *p++ = 'p';
        *p++ = ';';
        break;
      case '"':
        *p++ = '&';
        *p++ = 'q';
        *p++ = 'u';
        *p++ = 'o';
        *p++ = 't';
        *p++ = ';';
        break;
      default:
        *p++ = snip_->string[i];
        break;
      }
    } else {
      *p++ = snip_->string[i];
    }

    for (k = sres->last_tag_result_idx;
         snip_->tag_result[k].end_offset <= sres->end_offset;
         k--) {
      /* TODO: avoid all loop */
      if (snip_->tag_result[k].end_offset == i + 1) {
        grn_memcpy(p,
                   snip_->tag_result[k].cond->closetag,
                   snip_->tag_result[k].cond->closetag_len);
        p += snip_->tag_result[k].cond->closetag_len;
      }
      if (k <= sres->first_tag_result_idx) {
        break;
      }
    };
  }
  *p = '\0';

  if (result_len) {
    *result_len = (unsigned int)(p - result);
  }
  GRN_ASSERT((unsigned int)(p - result) <= snip_->max_tagged_len);

  GRN_API_RETURN(ctx->rc);
}
