/*
 * Copyright (c) 1997-2006  Motoyuki Kasahara
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include "build-pre.h"
#include "eb.h"
#include "error.h"
#include "text.h"
#include "build-post.h"

/*
 * The maximum number of arguments for an escape sequence.
 */
#define EB_MAX_ARGV     7

/*
 * Read next when the length of cached data is shorter than this value.
 */
#define SIZE_FEW_REST    48

/*
 * Special skip-code that represents `no skip-code is set'.
 */
#define SKIP_CODE_NONE  -1

/*
 * Cache data buffer.
 */
static char cache_buffer[EB_SIZE_PAGE];

/*
 * Book code of which `cache_buffer' records data.
 */
static EB_Book_Code cache_book_code = EB_BOOK_NONE;

/*
 * Location of cache data loaded in `cache_buffer'.
 */
static off_t cache_location;

/*
 * Length of cache data loaded in `cache_buffer'.
 */
static size_t cache_length;

/*
 * Null hook.
 */
static const EB_Hook null_hook = {EB_HOOK_NULL, NULL};

/*
 * Mutex for cache variables.
 */
#ifdef ENABLE_PTHREAD
static pthread_mutex_t cache_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

/*
 * Unexported functions.
 */
static EB_Error_Code eb_read_text_internal(EB_Book *book,
  EB_Appendix *appendix, EB_Hookset *hookset, void *container,
  size_t text_max_length, char *text, ssize_t *text_length,
  int forward_only);
static int eb_is_stop_code(EB_Book *book, EB_Appendix *appendix,
  unsigned int code0, unsigned int code1);


/*
 * Initialize text context of `book'.
 */
void
eb_initialize_text_context(EB_Book *book)
{
  LOG(("in: eb_initialize_text_context(book=%d)", (int)book->code));

  book->text_context.code = EB_TEXT_INVALID;
  book->text_context.location = -1;
  book->text_context.out = NULL;
  book->text_context.out_rest_length = 0;
  book->text_context.unprocessed = NULL;
  book->text_context.unprocessed_size = 0;
  book->text_context.out_step = 0;
  book->text_context.narrow_flag = 0;
  book->text_context.printable_count = 0;
  book->text_context.file_end_flag = 0;
  book->text_context.text_status = EB_TEXT_STATUS_CONTINUED;
  book->text_context.skip_code = SKIP_CODE_NONE;
  book->text_context.auto_stop_code = -1;
  book->text_context.candidate[0] = '\0';
  book->text_context.is_candidate = 0;
  book->text_context.ebxac_gaiji_flag = 0;

  LOG(("out: eb_initialize_text_context()"));
}


/*
 * Finalize text context of `book'.
 */
void
eb_finalize_text_context(EB_Book *book)
{
  LOG(("in: eb_finalize_text_context(book=%d)", (int)book->code));

  if (book->text_context.unprocessed != NULL)
    free(book->text_context.unprocessed);

  LOG(("out: eb_finalize_text_context()"));
}


/*
 * Reset text context of `book'.
 * Note that `contexxt_code' and `context_location' are unchanged.
 */
void
eb_reset_text_context(EB_Book *book)
{
  LOG(("in: eb_reset_text_context(book=%d)", (int)book->code));

  eb_finalize_text_context(book);

  book->text_context.out = NULL;
  book->text_context.out_rest_length = 0;
  book->text_context.unprocessed = NULL;
  book->text_context.unprocessed_size = 0;
  book->text_context.out_step = 0;
  book->text_context.narrow_flag = 0;
  book->text_context.printable_count = 0;
  book->text_context.file_end_flag = 0;
  book->text_context.text_status = EB_TEXT_STATUS_CONTINUED;
  book->text_context.skip_code = SKIP_CODE_NONE;
  book->text_context.auto_stop_code = -1;
  book->text_context.candidate[0] = '\0';
  book->text_context.is_candidate = 0;
  book->text_context.ebxac_gaiji_flag = 0;

  LOG(("out: eb_reset_text_context()"));
}


/*
 * Invalidate text context of `book'.
 */
void
eb_invalidate_text_context(EB_Book *book)
{
  LOG(("in: eb_invalidate_text_context(book=%d)", (int)book->code));

  eb_finalize_text_context(book);
  eb_initialize_text_context(book);
  book->text_context.code = EB_TEXT_INVALID;

  LOG(("out: eb_invalidate_text_context()"));
}


/*
 * Reposition the offset of the subbook file.
 */
EB_Error_Code
eb_seek_text(EB_Book *book, const EB_Position *position)
{
  EB_Error_Code error_code;

  pthread_mutex_lock(&cache_mutex);
  eb_lock(&book->lock);
  LOG(("in: eb_seek_text(book=%d, position={%d,%d})", (int)book->code,
    position->page, position->offset));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  if (position->page <= 0 || position->offset < 0) {
    error_code = EB_ERR_FAIL_SEEK_TEXT;
    goto failed;
  }

  /*
   * Set text-context variables.
   */
  eb_reset_text_context(book);
  book->text_context.code = EB_TEXT_SEEKED;
  book->text_context.location = ((off_t) position->page - 1) * EB_SIZE_PAGE
    + position->offset;

  /*
   * Unlock cache data and the book.
   */
  LOG(("out: eb_seek_text() = %s", eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);
  pthread_mutex_unlock(&cache_mutex);

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  eb_invalidate_text_context(book);
  LOG(("out: eb_seek_text() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  pthread_mutex_unlock(&cache_mutex);
  return error_code;
}


/*
 * Get the current text position of the subbook file.
 */
EB_Error_Code
eb_tell_text(EB_Book *book, EB_Position *position)
{
  EB_Error_Code error_code;

  eb_lock(&book->lock);
  LOG(("in: eb_tell_text(book=%d)", (int)book->code));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  position->page = book->text_context.location / EB_SIZE_PAGE + 1;
  position->offset = book->text_context.location % EB_SIZE_PAGE;

  LOG(("out: eb_seek_text(position={%d,%d}) = %s",
    position->page, position->offset, eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  eb_invalidate_text_context(book);
  LOG(("out: eb_seek_text() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Get text in the current subbook in `book'.
 */
EB_Error_Code
eb_read_text(EB_Book *book, EB_Appendix *appendix, EB_Hookset *hookset,
  void *container, size_t text_max_length, char *text, ssize_t *text_length)
{
  EB_Error_Code error_code;
  const EB_Hook *hook;
  EB_Position position;

  eb_lock(&book->lock);
  if (appendix != NULL)
    eb_lock(&appendix->lock);
  if (hookset != NULL)
    eb_lock(&hookset->lock);
  LOG(("in: eb_read_text(book=%d, appendix=%d, text_max_length=%ld)",
    (int)book->code, (appendix != NULL) ? (int)appendix->code : 0,
    (long)text_max_length));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  /*
   * Use `eb_default_hookset' when `hookset' is `NULL'.
   */
  if (hookset == NULL)
    hookset = &eb_default_hookset;

  /*
   * Set text mode to `text'.
   */
  if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code == EB_TEXT_SEEKED) {
    eb_tell_text(book, &position);
    eb_reset_text_context(book);

    if (book->subbook_current->menu.start_page <= position.page
      && position.page <= book->subbook_current->menu.end_page)
      book->text_context.code = EB_TEXT_OPTIONAL_TEXT;
    else if (book->subbook_current->image_menu.start_page <= position.page
      && position.page <= book->subbook_current->image_menu.end_page)
      book->text_context.code = EB_TEXT_OPTIONAL_TEXT;
    else if (book->subbook_current->copyright.start_page <= position.page
      && position.page <= book->subbook_current->copyright.end_page)
      book->text_context.code = EB_TEXT_OPTIONAL_TEXT;
    else
      book->text_context.code = EB_TEXT_MAIN_TEXT;

    hook = hookset->hooks + EB_HOOK_INITIALIZE;
    if (hook->function != NULL) {
      error_code = hook->function(book, appendix, container,
        EB_HOOK_INITIALIZE, 0, NULL);
      if (error_code != EB_SUCCESS)
        goto failed;
    }
  } else if (book->text_context.code != EB_TEXT_MAIN_TEXT
    && book->text_context.code != EB_TEXT_OPTIONAL_TEXT) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  error_code = eb_read_text_internal(book, appendix, hookset, container,
    text_max_length, text, text_length, 0);
  if (error_code != EB_SUCCESS)
    goto failed;

  LOG(("out: eb_read_text(text_length=%ld) = %s", (long)*text_length,
    eb_error_string(EB_SUCCESS)));
  if (hookset != &eb_default_hookset)
    eb_unlock(&hookset->lock);
  if (appendix != NULL)
    eb_unlock(&appendix->lock);
  eb_unlock(&book->lock);
  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  eb_invalidate_text_context(book);
  LOG(("out: eb_read_text() = %s", eb_error_string(error_code)));
  if (hookset != &eb_default_hookset)
    eb_unlock(&hookset->lock);
  if (appendix != NULL)
    eb_unlock(&appendix->lock);
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Get text in the current subbook in `book'.
 */
EB_Error_Code
eb_read_heading(EB_Book *book, EB_Appendix *appendix, EB_Hookset *hookset,
  void *container, size_t text_max_length, char *text, ssize_t *text_length)
{
  EB_Error_Code error_code;
  const EB_Hook *hook;

  eb_lock(&book->lock);
  if (appendix != NULL)
    eb_lock(&appendix->lock);
  if (hookset != NULL)
    eb_lock(&hookset->lock);
  LOG(("in: eb_read_heading(book=%d, appendix=%d, text_max_length=%ld)",
    (int)book->code, (appendix != NULL) ? (int)appendix->code : 0,
    (long)text_max_length));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  /*
   * Use `eb_default_hookset' when `hookset' is `NULL'.
   */
  if (hookset == NULL)
    hookset = &eb_default_hookset;

  /*
   * Set text mode to `heading'.
   */
  if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code == EB_TEXT_SEEKED) {
    eb_reset_text_context(book);
    book->text_context.code = EB_TEXT_HEADING;

    hook = hookset->hooks + EB_HOOK_INITIALIZE;
    if (hook->function != NULL) {
      error_code = hook->function(book, appendix, container,
        EB_HOOK_INITIALIZE, 0, NULL);
      if (error_code != EB_SUCCESS)
        goto failed;
    }
  } else if (book->text_context.code != EB_TEXT_HEADING) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  error_code = eb_read_text_internal(book, appendix, hookset, container,
    text_max_length, text, text_length, 0);
  if (error_code != EB_SUCCESS)
    goto failed;

  LOG(("out: eb_read_heading(text_length=%ld) = %s", (long)*text_length,
    eb_error_string(EB_SUCCESS)));
  if (hookset != &eb_default_hookset)
    eb_unlock(&hookset->lock);
  if (appendix != NULL)
    eb_unlock(&appendix->lock);
  eb_unlock(&book->lock);
  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  eb_invalidate_text_context(book);
  LOG(("out: eb_read_heading() = %s", eb_error_string(error_code)));
  if (hookset != &eb_default_hookset)
    eb_unlock(&hookset->lock);
  if (appendix != NULL)
    eb_unlock(&appendix->lock);
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Read data from the subbook file directly.
 */
EB_Error_Code
eb_read_rawtext(EB_Book *book, size_t text_max_length, char *text,
  ssize_t *text_length)
{
  EB_Error_Code error_code;

  eb_lock(&book->lock);
  LOG(("in: eb_read_rawtext(book=%d, text_max_length=%ld)",
    (int)book->code, (long)text_max_length));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  /*
   * Set text mode to `rawtext'.
   */
  if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code == EB_TEXT_SEEKED) {
    eb_reset_text_context(book);
    book->text_context.code = EB_TEXT_RAWTEXT;
  } else if (book->text_context.code != EB_TEXT_RAWTEXT) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  /*
   * Seek START file and read data.
   */
  if (zio_lseek(&book->subbook_current->text_zio,
    book->text_context.location, SEEK_SET) == -1) {
    error_code = EB_ERR_FAIL_SEEK_TEXT;
    goto failed;
  }
  *text_length = zio_read(&book->subbook_current->text_zio, text,
    text_max_length);
  book->text_context.location += *text_length;
  if (*text_length < 0) {
    error_code = EB_ERR_FAIL_READ_TEXT;
    goto failed;
  }

  LOG(("out: eb_read_rawtext(text_length=%ld) = %s", (long)*text_length,
    eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  *text_length = -1;
  eb_invalidate_text_context(book);
  LOG(("out: eb_read_rawtext() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Get text or heading.
 */
static EB_Error_Code
eb_read_text_internal(EB_Book *book, EB_Appendix *appendix,
  EB_Hookset *hookset, void *container, size_t text_max_length, char *text,
  ssize_t *text_length, int forward_only)
{
  EB_Error_Code error_code;
  EB_Text_Context *context;
  unsigned char c1, c2;
  char *cache_p;
  const EB_Hook *hook;
  unsigned char *candidate_p;
  size_t candidate_length;
  size_t cache_rest_length;
  size_t in_step;
  unsigned int argv[EB_MAX_ARGV];
  int argc;

  pthread_mutex_lock(&cache_mutex);
  LOG(("in: eb_read_text_internal(book=%d, appendix=%d, \
text_max_length=%ld, forward=%d)",
    (int)book->code, (appendix != NULL) ? (int)appendix->code : 0,
    (long)text_max_length, forward_only));

  /*
   * Initialize variables.
   */
  context = &book->text_context;
  context->out = text;
  context->out_rest_length = text_max_length;
  if (context->is_candidate) {
    candidate_length = strlen(context->candidate);
    candidate_p = (unsigned char *)context->candidate + candidate_length;
  } else {
    candidate_length = 0;
    candidate_p = NULL;
  }

  /*
   * If unprocessed string are rest in `context->unprocessed',
   * copy them to `context->out'.
   */
  if (context->unprocessed != NULL) {
    if (!forward_only) {
      if (context->out_rest_length < context->unprocessed_size)
        goto succeeded;
      memcpy(context->out, context->unprocessed,
        context->unprocessed_size);
      context->out += context->unprocessed_size;
      context->out_rest_length -= context->unprocessed_size;
    }
    free(context->unprocessed);
    context->unprocessed = NULL;
    context->unprocessed_size = 0;
  }

  /*
   * Return immediately if text-end-flag has been set.
   */
  if (context->text_status != EB_TEXT_STATUS_CONTINUED)
    goto succeeded;

  /*
   * Check for cache data.
   * If cache data is not what we need, discard it.
   */
  if (book->code == cache_book_code
    && cache_location <= context->location
    && context->location < cache_location + cache_length) {
    cache_p = cache_buffer + (context->location - cache_location);
    cache_rest_length = cache_length
      - (context->location - cache_location);
  } else {
    cache_book_code = EB_BOOK_NONE;
    cache_p = cache_buffer;
    cache_length = 0;
    cache_rest_length = 0;
  }

  for (;;) {
    in_step = 0;
    context->out_step = 0;
    argc = 1;

    /*
     * If it reaches to the near of the end of the cache buffer,
     * then moves remaind cache text to the beginning of the cache
     * buffer, and reads a next chunk from a file.
     */
    if (cache_rest_length < SIZE_FEW_REST && !context->file_end_flag) {
      ssize_t read_result;

      if (0 < cache_rest_length)
        memmove(cache_buffer, cache_p, cache_rest_length);
      if (zio_lseek(&book->subbook_current->text_zio,
        context->location + cache_rest_length, SEEK_SET) == -1) {
        error_code = EB_ERR_FAIL_SEEK_TEXT;
        goto failed;
      }

      read_result = zio_read(&book->subbook_current->text_zio,
        cache_buffer + cache_rest_length,
        EB_SIZE_PAGE - cache_rest_length);
      if (read_result < 0) {
        error_code = EB_ERR_FAIL_READ_TEXT;
        goto failed;
      } else if (read_result != EB_SIZE_PAGE - cache_rest_length)
        context->file_end_flag = 1;

      cache_book_code = book->code;
      cache_location = context->location;
      cache_length = cache_rest_length + read_result;
      cache_p = cache_buffer;
      cache_rest_length = cache_length;
    }

    /*
     * Get 1 byte from the buffer.
     */
    if (cache_rest_length < 1) {
      error_code = EB_ERR_UNEXP_TEXT;
      goto failed;
    }
    c1 = eb_uint1(cache_p);

    if (c1 == 0x1f) {
      hook = &null_hook;

      /*
       * This is escape sequences.
       */
      if (cache_rest_length < 2) {
        error_code = EB_ERR_UNEXP_TEXT;
        goto failed;
      }
      argv[0] = eb_uint2(cache_p);
      c2 = eb_uint1(cache_p + 1);

      switch (c2) {
      case 0x02:
        /* beginning of text */
        in_step = 2;
        break;

      case 0x03:
        /* end of text (don't set `in_step') */
        context->text_status = EB_TEXT_STATUS_HARD_STOP;
        if (forward_only) {
          error_code = EB_ERR_END_OF_CONTENT;
          goto failed;
        }
        goto succeeded;

      case 0x04:
        /* beginning of NARROW */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_NARROW;
        context->narrow_flag = 1;
        break;

      case 0x05:
        /* end of NARROW */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_NARROW;
        context->narrow_flag = 0;
        break;

      case 0x06:
        /* beginning of subscript */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_SUBSCRIPT;
        break;

      case 0x07:
        /* end of subscript */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_SUBSCRIPT;
        break;

      case 0x09:
        /* set indent */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 2;
        argv[1] = eb_uint2(cache_p + 2);

        if (0 < context->printable_count
          && context->code == EB_TEXT_MAIN_TEXT) {
          if (eb_is_stop_code(book, appendix, argv[0], argv[1])) {
            context->text_status = EB_TEXT_STATUS_SOFT_STOP;
            goto succeeded;
          }
        }

        hook = hookset->hooks + EB_HOOK_SET_INDENT;
        break;

      case 0x0a:
        /* newline */
        in_step = 2;
        if (context->code == EB_TEXT_HEADING) {
          context->text_status = EB_TEXT_STATUS_SOFT_STOP;
          context->location += in_step;
          goto succeeded;
        }
        hook = hookset->hooks + EB_HOOK_NEWLINE;
        break;

      case 0x0b:
        /* beginning of unicode */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_UNICODE;
        break;

      case 0x0c:
        /* end of unicode */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_UNICODE;
        break;

      case 0x0e:
        /* beginning of superscript */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_SUPERSCRIPT;
        break;

      case 0x0f:
        /* end of superscript */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_SUPERSCRIPT;
        break;

      case 0x10:
        /* beginning of newline prohibition */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_NO_NEWLINE;
        break;

      case 0x11:
        /* end of newline prohibition */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_NO_NEWLINE;
        break;

      case 0x12:
        /* beginning of emphasis */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_EMPHASIS;
        break;

      case 0x13:
        /* end of emphasis */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_EMPHASIS;
        break;

      case 0x14:
        in_step = 4;
        context->skip_code = 0x15;
        break;

      case 0x1a: case 0x1b: case 0x1e: case 0x1f:
        /* emphasis; described in JIS X 4081-1996 */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        /* Some old EB books don't take an argument. */
        if (book->disc_code != EB_DISC_EPWING
          && eb_uint1(cache_p + 2) >= 0x1f)
          in_step = 2;
        break;

      case 0x1c:
        if (book->character_code == EB_CHARCODE_JISX0208_GB2312) {
          /* beginning of EBXA-C gaiji */
          in_step = 2;
          hook = hookset->hooks + EB_HOOK_BEGIN_EBXAC_GAIJI;
          context->ebxac_gaiji_flag = 1;
        } else {
          in_step = 4;
          if (cache_rest_length < in_step) {
            error_code = EB_ERR_UNEXP_TEXT;
            goto failed;
          }
          /* Some old EB books don't take an argument. */
          if (book->disc_code != EB_DISC_EPWING
            && eb_uint1(cache_p + 2) >= 0x1f)
            in_step = 2;
        }
        break;

      case 0x1d:
        if (book->character_code == EB_CHARCODE_JISX0208_GB2312) {
          /* end of EBXA-C gaiji */
          in_step = 2;
          hook = hookset->hooks + EB_HOOK_END_EBXAC_GAIJI;
          context->ebxac_gaiji_flag = 0;
        } else {
          in_step = 4;
          if (cache_rest_length < in_step) {
            error_code = EB_ERR_UNEXP_TEXT;
            goto failed;
          }
          /* Some old EB books don't take an argument. */
          if (book->disc_code != EB_DISC_EPWING
            && eb_uint1(cache_p + 2) >= 0x1f)
            in_step = 2;
        }
        break;

      case 0x32:
        /* beginning of reference to monochrome graphic */
        in_step = 2;
        argc = 4;
        argv[1] = 0;
        argv[2] = 0;
        argv[3] = 0;
        hook = hookset->hooks + EB_HOOK_BEGIN_MONO_GRAPHIC;
        break;

      case 0x39:
        /* beginning of MPEG movie */
        in_step = 46;
        argc = 6;
        argv[1] = eb_uint4(cache_p + 2);
        argv[2] = eb_uint4(cache_p + 22);
        argv[3] = eb_uint4(cache_p + 26);
        argv[4] = eb_uint4(cache_p + 30);
        argv[5] = eb_uint4(cache_p + 34);
        hook = hookset->hooks + EB_HOOK_BEGIN_MPEG;
        break;

      case 0x3c:
        /* beginning of inline color graphic */
        in_step = 20;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 4;
        argv[1] = eb_uint2(cache_p + 2);
        argv[2] = eb_bcd4(cache_p + 14);
        argv[3] = eb_bcd2(cache_p + 18);
        if (argv[1] >> 8 == 0x00)
          hook = hookset->hooks + EB_HOOK_BEGIN_IN_COLOR_BMP;
        else
          hook = hookset->hooks + EB_HOOK_BEGIN_IN_COLOR_JPEG;
        break;

      case 0x35: case 0x36: case 0x37: case 0x38: case 0x3a:
      case 0x3b: case 0x3d: case 0x3e: case 0x3f:
        in_step = 2;
        context->skip_code = eb_uint1(cache_p + 1) + 0x20;
        break;

      case 0x41:
        /* beginning of keyword */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 2;
        argv[1] = eb_uint2(cache_p + 2);

        if (0 < context->printable_count
          && context->code == EB_TEXT_MAIN_TEXT) {
          if (eb_is_stop_code(book, appendix, argv[0], argv[1])) {
            context->text_status = EB_TEXT_STATUS_SOFT_STOP;
            goto succeeded;
          }
        }
        if (context->auto_stop_code < 0)
          context->auto_stop_code = eb_uint2(cache_p + 2);

        hook = hookset->hooks + EB_HOOK_BEGIN_KEYWORD;
        break;

      case 0x42:
        /* beginning of reference */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        if (eb_uint1(cache_p + 2) != 0x00)
          in_step -= 2;
        hook = hookset->hooks + EB_HOOK_BEGIN_REFERENCE;
        break;

      case 0x43:
        /* beginning of an entry of a candidate */
        in_step = 2;
        if (context->skip_code == SKIP_CODE_NONE) {
          context->candidate[0] = '\0';
          context->is_candidate = 1;
          candidate_length = 0;
          candidate_p = (unsigned char *)context->candidate;
        }
        hook = hookset->hooks + EB_HOOK_BEGIN_CANDIDATE;
        break;

      case 0x44:
        /* beginning of monochrome graphic */
        in_step = 12;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 4;
        argv[1] = eb_uint2(cache_p + 2);
        argv[2] = eb_bcd4(cache_p + 4);
        argv[3] = eb_bcd4(cache_p + 8);
        if (0 < argv[2] && 0 < argv[3])
          hook = hookset->hooks + EB_HOOK_BEGIN_MONO_GRAPHIC;
        break;

      case 0x45:
        /* beginning of graphic block */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        if (eb_uint1(cache_p + 2) != 0x1f) {
          argc = 2;
          argv[1] = eb_bcd4(cache_p + 2);
        } else {
          in_step = 2;
        }
        break;

      case 0x4a:
        /* beginning of WAVE sound */
        in_step = 18;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 6;
        argv[1] = eb_uint4(cache_p + 2);
        argv[2] = eb_bcd4(cache_p + 6);
        argv[3] = eb_bcd2(cache_p + 10);
        argv[4] = eb_bcd4(cache_p + 12);
        argv[5] = eb_bcd2(cache_p + 16);
        hook = hookset->hooks + EB_HOOK_BEGIN_WAVE;
        break;

      case 0x4b:
        /* beginning of paged reference */
        in_step = 8;
        if (cache_rest_length < in_step + 2) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 3;
        argv[1] = eb_bcd4(cache_p + 2);
        argv[2] = eb_bcd2(cache_p + 6);
        if (cache_p[8]==0x1f && cache_p[9]==0x6b) {
          context->text_status = EB_TEXT_STATUS_SOFT_STOP;
          hook = hookset->hooks + EB_HOOK_GRAPHIC_REFERENCE;
          in_step = 10;
        } else {
          hook = hookset->hooks + EB_HOOK_BEGIN_GRAPHIC_REFERENCE;
        }
        break;

      case 0x4c:
        /* beginning of image page */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_BEGIN_IMAGE_PAGE;
        break;

      case 0x4d:
        /* beginning of color graphic (BMP or JPEG) */
        in_step = 20;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 4;
        argv[1] = eb_uint2(cache_p + 2);
        argv[2] = eb_bcd4(cache_p + 14);
        argv[3] = eb_bcd2(cache_p + 18);
        if (argv[1] >> 8 == 0x00)
          hook = hookset->hooks + EB_HOOK_BEGIN_COLOR_BMP;
        else
          hook = hookset->hooks + EB_HOOK_BEGIN_COLOR_JPEG;
        break;

      case 0x4f:
        /* beginning of clickable area */
        in_step = 34;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 7;
        argv[1] = eb_bcd2(cache_p + 8);
        argv[2] = eb_bcd2(cache_p + 10);
        argv[3] = eb_bcd2(cache_p + 12);
        argv[4] = eb_bcd2(cache_p + 14);
        argv[5] = eb_bcd4(cache_p + 28);
        argv[6] = eb_bcd2(cache_p + 32);
        hook = hookset->hooks + EB_HOOK_BEGIN_CLICKABLE_AREA;
        break;

      case 0x49: case 0x4e:

        in_step = 2;
        context->skip_code = eb_uint1(cache_p + 1) + 0x20;
        break;

      case 0x52:
        /* end of reference to monochrome graphic */
        in_step = 8;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 3;
        argv[1] = eb_bcd4(cache_p + 2);
        argv[2] = eb_bcd2(cache_p + 6);
        hook = hookset->hooks + EB_HOOK_END_MONO_GRAPHIC;
        break;

      case 0x53:
        /* end of EB sound */
        in_step = 10;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        break;

      case 0x59:
        /* end of MPEG movie */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_MPEG;
        break;

      case 0x5c:
        /* end of inline color graphic */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_IN_COLOR_GRAPHIC;
        break;

      case 0x61:
        /* end of keyword */
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_KEYWORD;
        break;

      case 0x62:
        /* end of reference */
        in_step = 8;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 3;
        argv[1] = eb_bcd4(cache_p + 2);
        argv[2] = eb_bcd2(cache_p + 6);
        hook = hookset->hooks + EB_HOOK_END_REFERENCE;
        break;

      case 0x63:
        /* end of an entry of a candidate */
        in_step = 8;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 3;
        argv[1] = eb_bcd4(cache_p + 2);
        argv[2] = eb_bcd2(cache_p + 6);
        if (argv[1] == 0 && argv[2] == 0)
          hook = hookset->hooks + EB_HOOK_END_CANDIDATE_LEAF;
        else
          hook = hookset->hooks + EB_HOOK_END_CANDIDATE_GROUP;
        break;

      case 0x64:
        /* end of monochrome graphic */
        in_step = 8;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 3;
        argv[1] = eb_bcd4(cache_p + 2);
        argv[2] = eb_bcd2(cache_p + 6);
        hook = hookset->hooks + EB_HOOK_END_MONO_GRAPHIC;
        break;

      case 0x6b:
        /* end of paged reference */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_GRAPHIC_REFERENCE;
        break;

      case 0x6a:
        /* end of WAVE sound */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_WAVE;
        break;

      case 0x6c:
        /* end of image page */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        context->text_status = EB_TEXT_STATUS_SOFT_STOP;
        hook = hookset->hooks + EB_HOOK_END_IMAGE_PAGE;
        break;

      case 0x6d:
        /* end of color graphic (BMP or JPEG) */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_COLOR_GRAPHIC;
        break;

      case 0x6f:
        /* end of clickable area */
        in_step = 2;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        hook = hookset->hooks + EB_HOOK_END_CLICKABLE_AREA;
        break;

      case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75:
      case 0x76: case 0x77: case 0x78: case 0x79: case 0x7a: case 0x7b:
      case 0x7c: case 0x7d: case 0x7e: case 0x7f:
      case 0x80: case 0x81: case 0x82: case 0x83: case 0x84: case 0x85:
      case 0x86: case 0x87: case 0x88: case 0x89: case 0x8a: case 0x8b:
      case 0x8c: case 0x8d: case 0x8e: case 0x8f:
        in_step = 2;
        context->skip_code = eb_uint1(cache_p + 1) + 0x20;
        break;

      case 0xe0:
        /* character modification */
        in_step = 4;
        if (cache_rest_length < in_step) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }
        argc = 2;
        argv[1] = eb_uint2(cache_p + 2);
        hook = hookset->hooks + EB_HOOK_BEGIN_DECORATION;

        /* Some old EB books don't take an argument. */
        if (book->disc_code != EB_DISC_EPWING
          && eb_uint1(cache_p + 2) >= 0x1f) {
          in_step = 2;
          hook = &null_hook;
        }
        break;

      case 0xe1:
        in_step = 2;
        hook = hookset->hooks + EB_HOOK_END_DECORATION;
        break;

      case 0xe4: case 0xe6: case 0xe8: case 0xea: case 0xec: case 0xee:
      case 0xf0: case 0xf2: case 0xf4: case 0xf6: case 0xf8: case 0xfa:
      case 0xfc: case 0xfe:
        in_step = 2;
        context->skip_code = eb_uint1(cache_p + 1) + 0x01;
        break;

      default:
        in_step = 2;
        if (context->skip_code == eb_uint1(cache_p + 1))
          context->skip_code = SKIP_CODE_NONE;
        break;
      }

      if (context->skip_code == SKIP_CODE_NONE
        && hook->function != NULL
        && !forward_only) {
        error_code = hook->function(book, appendix, container,
          hook->code, argc, argv);
        if (error_code != EB_SUCCESS)
          goto failed;
      }

      /*
       * Post process.  Clean a candidate.
       */
      if (c2 == 0x63) {
        /* end of an entry of candidate */
        context->is_candidate = 0;
      }

    } else if (book->character_code == EB_CHARCODE_ISO8859_1) {
      /*
       * The book is mainly written in ISO 8859 1.
       */
      context->printable_count++;

      if ((0x20 <= c1 && c1 < 0x7f) || (0xa0 <= c1 && c1 <= 0xff)) {
        /*
         * This is an ISO 8859 1 character.
         */
        in_step = 1;
        argv[0] = eb_uint1(cache_p);

        if (context->skip_code == SKIP_CODE_NONE) {
          if (context->is_candidate
            && candidate_length < EB_MAX_WORD_LENGTH) {
            *candidate_p++ = c1 | 0x80;
            *candidate_p = '\0';
            candidate_length++;
          }

          hook = hookset->hooks + EB_HOOK_ISO8859_1;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte1(book, c1);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              EB_HOOK_ISO8859_1, argc, argv);
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        }
      } else {
        /*
         * This is a local character.
         */
        if (cache_rest_length < 2) {
          error_code = EB_ERR_UNEXP_TEXT;
          goto failed;
        }

        in_step = 2;
        argv[0] = eb_uint2(cache_p);
        if (context->skip_code == SKIP_CODE_NONE) {
          hook = hookset->hooks + EB_HOOK_NARROW_FONT;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte1(book, c1);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              EB_HOOK_NARROW_FONT, argc, argv);
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        }
      }

    } else {
      /*
       * The book is written in JIS X 0208 or JIS X 0208 + GB 2312.
       */
      context->printable_count++;
      in_step = 2;

      if (cache_rest_length < 2) {
        error_code = EB_ERR_UNEXP_TEXT;
        goto failed;
      }

      c2 = eb_uint1(cache_p + 1);

      if (context->skip_code != SKIP_CODE_NONE) {
        /* nothing to be done. */
      } else if (0x20 < c1 && c1 < 0x7f && 0x20 < c2 && c2 < 0x7f) {
        /*
         * This is a JIS X 0208 KANJI character.
         */
        argv[0] = eb_uint2(cache_p) | 0x8080;

        if (context->is_candidate
          && candidate_length < EB_MAX_WORD_LENGTH - 1) {
          *candidate_p++ = c1 | 0x80;
          *candidate_p++ = c2 | 0x80;
          *candidate_p = '\0';
          candidate_length += 2;
        }

        if (context->ebxac_gaiji_flag) {
          hook = hookset->hooks + EB_HOOK_EBXAC_GAIJI;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte2(book, c1 | 0x80,
              c2 | 0x80);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              //EB_HOOK_EBXAC_GAIJI, 0, argv);
              EB_HOOK_EBXAC_GAIJI, argc, argv); // jichi: keep argc
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        } else if (context->narrow_flag) {
          hook = hookset->hooks + EB_HOOK_NARROW_JISX0208;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte2(book, c1 | 0x80,
              c2 | 0x80);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              //EB_HOOK_NARROW_JISX0208, 0, argv);
              EB_HOOK_NARROW_JISX0208, argc, argv); // jichi: keep argc
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        } else {
          hook = hookset->hooks + EB_HOOK_WIDE_JISX0208;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte2(book, c1 | 0x80,
              c2 | 0x80);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              EB_HOOK_WIDE_JISX0208, argc, argv);
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        }
      } else if (0x20 < c1 && c1 < 0x7f && 0xa0 < c2 && c2 < 0xff) {
        /*
         * This is a GB 2312 HANJI character.
         */
        argv[0] = eb_uint2(cache_p) | 0x8000;

        if (context->is_candidate
          && candidate_length < EB_MAX_WORD_LENGTH - 1) {
          *candidate_p++ = c1 | 0x80;
          *candidate_p++ = c2;
          *candidate_p   = '\0';
          candidate_length += 2;
        }

        hook = hookset->hooks + EB_HOOK_GB2312;
        if (forward_only) {
          ; /* do nothing */
        } else if (hook->function == NULL) {
          error_code = eb_write_text_byte2(book, c1 | 0x80, c2);
          if (error_code != EB_SUCCESS)
            goto failed;
        } else {
          error_code = hook->function(book, appendix, container,
            //EB_HOOK_GB2312, 0, argv);
            EB_HOOK_GB2312, argc, argv); // jichi: preserve argc
          if (error_code != EB_SUCCESS)
            goto failed;
        }
      } else if (0xa0 < c1 && c1 < 0xff && 0x20 < c2 && c2 < 0x7f) {
        /*
         * This is a local character.
         */
        argv[0] = eb_uint2(cache_p);

        if (context->narrow_flag) {
          hook = hookset->hooks + EB_HOOK_NARROW_FONT;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte2(book, c1, c2);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              EB_HOOK_NARROW_FONT, argc, argv);
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        } else {
          hook = hookset->hooks + EB_HOOK_WIDE_FONT;
          if (forward_only) {
            ; /* do nothing */
          } else if (hook->function == NULL) {
            error_code = eb_write_text_byte2(book, c1, c2);
            if (error_code != EB_SUCCESS)
              goto failed;
          } else {
            error_code = hook->function(book, appendix, container,
              EB_HOOK_WIDE_FONT, argc, argv);
            if (error_code != EB_SUCCESS)
              goto failed;
          }
        }
      }
    }

    /*
     * Update variables.
     */
    cache_p += in_step;
    cache_rest_length -= in_step;
    context->location += in_step;
    in_step = 0;

    /*
     * Break if an unprocessed character is remained.
     */
    if (context->unprocessed != NULL)
      break;
    /*
     * Break if EB_TEXT_STATUS_SOFT_STOP is set.
     */
    if (context->text_status == EB_TEXT_STATUS_SOFT_STOP)
      break;
  }

  succeeded:
  if (!forward_only) {
    *text_length = (context->out - text);
    *(context->out) = '\0';
  }

  LOG(("out: eb_read_text_internal(text_length=%ld) = %s",
    (text_length == NULL) ? 0L : (long)*text_length,
    eb_error_string(EB_SUCCESS)));
  pthread_mutex_unlock(&cache_mutex);

  return EB_SUCCESS;

  /*
   * An error occurs...
   * Discard cache if read error occurs.
   */
  failed:
  if (!forward_only) {
    *text_length = -1;
    *text = '\0';
  }
  if (error_code == EB_ERR_FAIL_READ_TEXT)
    cache_book_code = EB_BOOK_NONE;
  LOG(("out: eb_read_text_internal() = %s", eb_error_string(error_code)));
  pthread_mutex_unlock(&cache_mutex);
  return error_code;
}


/*
 * Check whether an escape sequence is stop-code or not.
 */
static int
eb_is_stop_code(EB_Book *book, EB_Appendix *appendix, unsigned int code0,
  unsigned int code1)
{
  int result;

  if (appendix == NULL
    || appendix->subbook_current == NULL
    || appendix->subbook_current->stop_code0 == 0) {
    result = (code0 == 0x1f41
      && code1 == book->text_context.auto_stop_code);
  } else {
    result = (code0 == appendix->subbook_current->stop_code0
      && code1 == appendix->subbook_current->stop_code1);
  }

  return result;
}


/*
 * Have the current text context reached the end of text?
 */
int
eb_is_text_stopped(EB_Book *book)
{
  int is_stopped = 0;

  eb_lock(book);
  LOG(("in: eb_is_text_stopped(book=%d)", (int)book->code));

  if (book->subbook_current != NULL) {
    if (book->text_context.code == EB_TEXT_HEADING
      || book->text_context.code == EB_TEXT_MAIN_TEXT
      || book->text_context.code == EB_TEXT_OPTIONAL_TEXT) {
      if (book->text_context.text_status != EB_TEXT_STATUS_CONTINUED
        && book->text_context.unprocessed == NULL) {
        is_stopped = 1;
      }
    }
  }

  LOG(("out: eb_is_text_stopped() = %d", is_stopped));
  return is_stopped;
}


/*
 * Write a byte to a text buffer.
 */
EB_Error_Code
eb_write_text_byte1(EB_Book *book, int byte1)
{
  EB_Error_Code error_code;
  char stream[1];

  LOG(("in: eb_write_text_byte1(book=%d, byte1=%d)",
    (int)book->code, byte1));

  /*
   * If the text buffer has enough space to write `byte1',
   * save the byte in `book->text_context.unprocessed'.
   */
  if (book->text_context.unprocessed != NULL
    || book->text_context.out_rest_length < 1) {
    *(unsigned char *)stream = byte1;
    error_code = eb_write_text(book, stream, 1);
    if (error_code != EB_SUCCESS)
      goto failed;
  } else {
    *(book->text_context.out) = byte1;
    book->text_context.out++;
    book->text_context.out_rest_length--;
    book->text_context.out_step++;
  }

  LOG(("out: eb_write_text_byte1() = %s", eb_error_string(EB_SUCCESS)));

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  LOG(("out: eb_write_text_byte1() = %s", eb_error_string(error_code)));
  return error_code;
}


/*
 * Write two bytes to a text buffer for output.
 */
EB_Error_Code
eb_write_text_byte2(EB_Book *book, int byte1, int byte2)
{
  EB_Error_Code error_code;
  char stream[2];

  LOG(("in: eb_write_text_byte2(book=%d, byte1=%d, byte2=%d)",
    (int)book->code, byte1, byte2));

  /*
   * If the text buffer has enough space to write `byte1' and `byte2',
   * save the bytes in `book->text_context.unprocessed'.
   */
  if (book->text_context.unprocessed != NULL
    || book->text_context.out_rest_length < 2) {
    *(unsigned char *)stream = byte1;
    *(unsigned char *)(stream + 1) = byte2;
    error_code = eb_write_text(book, stream, 2);
    if (error_code != EB_SUCCESS)
      goto failed;
  } else {
    *(book->text_context.out) = byte1;
    book->text_context.out++;
    *(book->text_context.out) = byte2;
    book->text_context.out++;
    book->text_context.out_rest_length -= 2;
    book->text_context.out_step += 2;
  }

  LOG(("out: eb_write_text_byte2() = %s", eb_error_string(EB_SUCCESS)));

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  LOG(("out: eb_write_text_byte2() = %s", eb_error_string(error_code)));
  return error_code;
}


/*
 * Write a string to a text buffer.
 */
EB_Error_Code
eb_write_text_string(EB_Book *book, const char *string)
{
  EB_Error_Code error_code;
  size_t string_length;

  LOG(("in: eb_write_text_string(book=%d, string=%s)",
    (int)book->code, eb_quoted_string(string)));

  /*
   * If the text buffer has enough space to write `sting',
   * save the string in `book->text_context.unprocessed'.
   */
  string_length = strlen(string);

  if (book->text_context.unprocessed != NULL
    || book->text_context.out_rest_length < string_length) {
    error_code = eb_write_text(book, string, string_length);
    if (error_code != EB_SUCCESS)
      goto failed;
  } else {
    memcpy(book->text_context.out, string, string_length);
    book->text_context.out += string_length;
    book->text_context.out_rest_length -= string_length;
    book->text_context.out_step += string_length;
  }

  LOG(("out: eb_write_text_string() = %s", eb_error_string(EB_SUCCESS)));

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  LOG(("out: eb_write_text_string() = %s", eb_error_string(error_code)));
  return error_code;
}


/*
 * Write a stream with `length' bytes to a text buffer.
 */
EB_Error_Code
eb_write_text(EB_Book *book, const char *stream, size_t stream_length)
{
  EB_Error_Code error_code;
  char *reallocated;

  LOG(("in: eb_write_text(book=%d, stream=%s)",
    (int)book->code, eb_quoted_stream(stream, stream_length)));

  /*
   * If the text buffer has enough space to write `stream',
   * save the stream in `book->text_context.unprocessed'.
   */
  if (book->text_context.unprocessed != NULL) {
    reallocated = (char *)realloc(book->text_context.unprocessed,
      book->text_context.unprocessed_size + stream_length);
    if (reallocated == NULL) {
      free(book->text_context.unprocessed);
      book->text_context.unprocessed = NULL;
      book->text_context.unprocessed_size = 0;
      error_code = EB_ERR_MEMORY_EXHAUSTED;
      goto failed;
    }
    memcpy(reallocated + book->text_context.unprocessed_size, stream,
      stream_length);
    book->text_context.unprocessed = reallocated;
    book->text_context.unprocessed_size += stream_length;

  } else if (book->text_context.out_rest_length < stream_length) {
    book->text_context.unprocessed
      = (char *)malloc(book->text_context.out_step + stream_length);
    if (book->text_context.unprocessed == NULL) {
      error_code = EB_ERR_MEMORY_EXHAUSTED;
      goto failed;
    }
    book->text_context.unprocessed_size
      = book->text_context.out_step + stream_length;
    memcpy(book->text_context.unprocessed,
      book->text_context.out - book->text_context.out_step,
      book->text_context.out_step);
    memcpy(book->text_context.unprocessed + book->text_context.out_step,
      stream, stream_length);
    book->text_context.out -= book->text_context.out_step;
    book->text_context.out_step = 0;

  } else {
    memcpy(book->text_context.out, stream, stream_length);
    book->text_context.out += stream_length;
    book->text_context.out_rest_length -= stream_length;
    book->text_context.out_step += stream_length;
  }

  LOG(("out: eb_write_text() = %s", eb_error_string(EB_SUCCESS)));

  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  LOG(("out: eb_write_text() = %s", eb_error_string(error_code)));
  return error_code;
}


/*
 * Get the current candidate word for multi search.
 */
const char *
eb_current_candidate(EB_Book *book)
{
  LOG(("in: eb_current_candidate(book=%d)", (int)book->code));

  if (!book->text_context.is_candidate)
    book->text_context.candidate[0] = '\0';

  LOG(("out: eb_current_candidate() = %s",
    eb_quoted_string(book->text_context.candidate)));

  return book->text_context.candidate;
}


/*
 * Forward text position to the next paragraph.
 */
EB_Error_Code
eb_forward_text(EB_Book *book, EB_Appendix *appendix)
{
  EB_Error_Code error_code;

  eb_lock(&book->lock);
  LOG(("in: eb_forward_text(book=%d, appendix=%d)", (int)book->code,
    (appendix != NULL) ? (int)appendix->code : 0));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  if (book->text_context.code == EB_TEXT_SEEKED) {
    book->text_context.code = EB_TEXT_MAIN_TEXT;
  } else if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code != EB_TEXT_MAIN_TEXT
    && book->text_context.code != EB_TEXT_OPTIONAL_TEXT) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  if (book->text_context.text_status == EB_TEXT_STATUS_SOFT_STOP) {
    book->text_context.text_status = EB_TEXT_STATUS_CONTINUED;
    goto succeeded;
  } else if (book->text_context.text_status == EB_TEXT_STATUS_HARD_STOP) {
    error_code = EB_ERR_END_OF_CONTENT;
    goto failed;
  }

  /*
   * Forward text.
   */
  error_code = eb_read_text_internal(book, appendix, &eb_default_hookset,
    NULL, EB_SIZE_PAGE, NULL, NULL, 1);
  if (error_code != EB_SUCCESS)
    goto failed;

  /*
   * Unlock the book and hookset.
   */
  succeeded:
  eb_reset_text_context(book);
  LOG(("out: eb_forward_text() = %s", eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);
  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  if (error_code != EB_ERR_END_OF_CONTENT)
    eb_invalidate_text_context(book);
  LOG(("out: eb_forward_text() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Forward heading position to the next paragraph.
 * (for keyword search.)
 */
EB_Error_Code
eb_forward_heading(EB_Book *book)
{
  EB_Error_Code error_code;

  eb_lock(&book->lock);
  LOG(("in: eb_forward_heading(book=%d)", (int)book->code));

  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  if (book->text_context.code == EB_TEXT_SEEKED) {
    book->text_context.code = EB_TEXT_HEADING;
  } else if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code != EB_TEXT_HEADING) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  if (book->text_context.text_status == EB_TEXT_STATUS_SOFT_STOP) {
    book->text_context.text_status = EB_TEXT_STATUS_CONTINUED;
    goto succeeded;
  } else if (book->text_context.text_status == EB_TEXT_STATUS_HARD_STOP) {
    error_code = EB_ERR_END_OF_CONTENT;
    goto failed;
  }

  /*
   * Forward text.
   */
  error_code = eb_read_text_internal(book, NULL, &eb_default_hookset,
    NULL, EB_SIZE_PAGE, NULL, NULL, 1);
  if (error_code != EB_SUCCESS)
    goto failed;

  eb_reset_text_context(book);

  /*
   * Unlock cache data.
   */
  succeeded:
  LOG(("out: eb_forward_heading() = %s", eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);
  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  if (error_code != EB_ERR_END_OF_CONTENT)
    eb_invalidate_text_context(book);
  LOG(("out: eb_forward_heading() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  return error_code;
}


/*
 * Backward text position to the previous paragraph.
 */
EB_Error_Code
eb_backward_text(EB_Book *book, EB_Appendix *appendix)
{
  EB_Error_Code error_code;
  EB_Text_Context saved_context;
  off_t current_location;
  off_t forward_location;
  off_t read_location;
  off_t backward_location = -1;
  char text_buffer[EB_SIZE_PAGE];
  char *text_buffer_p;
  ssize_t read_result;
  int stop_code0, stop_code1;

  eb_lock(&book->lock);
  LOG(("in: eb_backward_text(book=%d, appendix=%d)", (int)book->code,
    (appendix != NULL) ? (int)appendix->code : 0));

  /*
   * Current subbook must have been set and START file must exist.
   */
  if (book->subbook_current == NULL) {
    error_code = EB_ERR_NO_CUR_SUB;
    goto failed;
  }
  if (zio_file(&book->subbook_current->text_zio) < 0) {
    error_code = EB_ERR_NO_TEXT;
    goto failed;
  }

  if (book->text_context.code == EB_TEXT_SEEKED) {
    book->text_context.code = EB_TEXT_MAIN_TEXT;
  } else if (book->text_context.code == EB_TEXT_INVALID) {
    error_code = EB_ERR_NO_PREV_SEEK;
    goto failed;
  } else if (book->text_context.code != EB_TEXT_MAIN_TEXT
    && book->text_context.code != EB_TEXT_OPTIONAL_TEXT) {
    error_code = EB_ERR_DIFF_CONTENT;
    goto failed;
  }

  /*
   * Forward text to get auto-stop-code and location where the current
   * text stops.
   */
  if (book->text_context.text_status != EB_TEXT_STATUS_CONTINUED) {
    forward_location = book->text_context.location;
  } else {
    memcpy(&saved_context, &book->text_context, sizeof(EB_Text_Context));
    error_code = eb_read_text_internal(book, NULL, &eb_default_hookset,
      NULL, EB_SIZE_PAGE, NULL, NULL, 1);
    if (error_code != EB_SUCCESS && error_code != EB_ERR_END_OF_CONTENT)
      goto failed;
    forward_location = book->text_context.location;
    saved_context.auto_stop_code = book->text_context.auto_stop_code;
    memcpy(&book->text_context, &saved_context, sizeof(EB_Text_Context));
  }

  /*
   * Determine stop-code.
   */
  if (appendix == NULL
    || appendix->subbook_current == NULL
    || appendix->subbook_current->stop_code0 == 0) {
    stop_code0 = 0x1f41;
    stop_code1 = book->text_context.auto_stop_code;
  } else {
    stop_code0 = appendix->subbook_current->stop_code0;
    stop_code1 = appendix->subbook_current->stop_code1;
  }

  /*
   * If the text locator has pointed to `0x1f02' (beginning of text),
   * we cannot backward.
   */
  if (zio_lseek(&book->subbook_current->text_zio,
    book->text_context.location, SEEK_SET) == -1) {
    error_code = EB_ERR_FAIL_SEEK_TEXT;
    goto failed;
  }
  if (zio_read(&book->subbook_current->text_zio, text_buffer, 2) != 2) {
    error_code = EB_ERR_FAIL_READ_TEXT;
    goto failed;
  }
  if (eb_uint2(text_buffer) == 0x1f02) {
    error_code = EB_ERR_END_OF_CONTENT;
    goto failed;
  }

  /*
   * Backward text.
   */
  current_location = book->text_context.location;

  while (0 < book->text_context.location) {
    size_t backward_distance;
    int i;

    /*
     * Seek and read text.
     *
     * Since a stop code occupies 4 bytes and we start scanning
     * stop-code at preceding byte of the current location, we read
     * text in front of the current location and following 3 bytes.
     *
     *          start scanning
     *          |  current location
     *          |  |
     *  [..] [..] [..] [1F] [41] [00] [01]
     *           ===================
     *          may be stop-code
     */
    if (book->text_context.location < EB_SIZE_PAGE + 3)
      read_location = 0;
    else
      read_location = book->text_context.location - EB_SIZE_PAGE + 3;
    backward_distance = book->text_context.location - read_location;

    if (zio_lseek(&book->subbook_current->text_zio, read_location,
      SEEK_SET) == -1) {
      error_code = EB_ERR_FAIL_SEEK_TEXT;
      goto failed;
    }

    memset(text_buffer, 0x00, EB_SIZE_PAGE);
    read_result = zio_read(&book->subbook_current->text_zio, text_buffer,
      EB_SIZE_PAGE);
    if (read_result < 0 || read_result < backward_distance) {
      error_code = EB_ERR_FAIL_READ_TEXT;
      goto failed;
    }

    /*
     * Scan stop-code.
     */
    text_buffer_p = text_buffer + backward_distance - 1;
    i = backward_distance - 1;

    while (0 <= i) {
      if (eb_uint2(text_buffer_p) == 0x1f02) {
        book->text_context.location = read_location + i;
        if (current_location <= book->text_context.location + 2) {
          error_code = EB_ERR_END_OF_CONTENT;
          goto failed;
        }
        backward_location = book->text_context.location;
        goto loop_end;
      }
      if (book->text_context.code != EB_TEXT_MAIN_TEXT
        || eb_uint2(text_buffer_p) != stop_code0
        || eb_uint2(text_buffer_p + 2) != stop_code1) {
        text_buffer_p--;
        i--;
        continue;
      }

      eb_reset_text_context(book);
      book->text_context.location = read_location + i;
      error_code = eb_read_text_internal(book, appendix,
        &eb_default_hookset, NULL, EB_SIZE_PAGE, NULL, NULL, 1);
      if (error_code != EB_SUCCESS
        && error_code != EB_ERR_END_OF_CONTENT)
        goto failed;

      if (book->text_context.location >= current_location - 4
        && book->text_context.location <= current_location + 4
        && backward_location < 0)
        forward_location = current_location;
      if (book->text_context.location >= forward_location - 4
        && book->text_context.location <= forward_location + 4)
        backward_location = read_location + i;
      else if (book->text_context.location < forward_location)
        goto loop_end;

      text_buffer_p--;
      i--;
    }

    book->text_context.location = read_location - 1;
  }

  loop_end:
  if (backward_location < 0) {
    error_code = EB_ERR_UNEXP_TEXT;
    goto failed;
  }

  eb_reset_text_context(book);

  /*
   * Unlock the book and hookset.
   */
  LOG(("out: eb_forward_text() = %s", eb_error_string(EB_SUCCESS)));
  eb_unlock(&book->lock);
  return EB_SUCCESS;

  /*
   * An error occurs...
   */
  failed:
  eb_invalidate_text_context(book);
  LOG(("out: eb_backward_text() = %s", eb_error_string(error_code)));
  eb_unlock(&book->lock);
  return error_code;
}


