/*
 * uconv version 0.6.0
 * Aug 13, 2011 yoshidam  thread local
 * Jan  3, 2010 yoshidam  for ruby 1.9.1
 * Mar 12, 2003 yoshidam  for ruby 1.8.0
 * Sep  4, 2002 yoshidam  fixes memory leaks
 * Dec 10, 2001 yoshidam  supports the tainted status
 * Nov 23, 2001 yoshidam  appends shortest_flag
 * Nov 24, 1999 yoshidam  appends eliminate_zwnbsp_flag
 * Nov  5, 1999 yoshidam  supports SJIS
 * Feb 22, 1999 yoshidam
 * Jul 24, 1998 yoshidam
 * Jun 30, 1998 yoshidam
 *
 */

#define UCONV_VERSION "0.6.0"

#include "ruby.h"
#ifdef HAVE_RUBY_IO_H
#  include "ruby/io.h"
#else
#  include "rubyio.h"
#endif
#include <stdio.h>
#include <string.h>
#include "uconv.h"
#include "ustring.h"

#ifndef RSTRING_PTR
#  define RSTRING_PTR(s) (RSTRING(s)->ptr)
#  define RSTRING_LEN(s) (RSTRING(s)->len)
#endif

#ifdef HAVE_RUBY_ENCODING_H
static rb_encoding* enc_u8;
static rb_encoding* enc_u16le;
static rb_encoding* enc_u16be;
static rb_encoding* enc_u32le;
static rb_encoding* enc_u32be;
static rb_encoding* enc_euc;
static rb_encoding* enc_sjis;
static rb_encoding* enc_8bit;
#  define ENC_U8(o) (rb_enc_associate(o, enc_u8))
#  define ENC_U16LE(o) (rb_enc_associate(o, enc_u16le))
#  define ENC_U16BE(o) (rb_enc_associate(o, enc_u16be))
#  define ENC_U32LE(o) (rb_enc_associate(o, enc_u32le))
#  define ENC_U32BE(o) (rb_enc_associate(o, enc_u32b))
#  define ENC_EUC(o) (rb_enc_associate(o, enc_euc))
#  define ENC_SJIS(o) (rb_enc_associate(o, enc_sjis))
static inline VALUE
enc_utf16swap(VALUE o, VALUE src) {
  int src_enc = rb_enc_get_index(src);
  if (src_enc == rb_enc_to_index(enc_u16le))
    return rb_enc_associate(o, enc_u16be);
  if (src_enc == rb_enc_to_index(enc_u16be))
    return rb_enc_associate(o, enc_u16le);
  else
    return rb_enc_associate(o, enc_8bit);
}
static inline VALUE
enc_utf32swap(VALUE o, VALUE src) {
  int src_enc = rb_enc_get_index(src);
  if (src_enc == rb_enc_to_index(enc_u32le))
    return rb_enc_associate(o, enc_u32be);
  if (src_enc == rb_enc_to_index(enc_u32be))
    return rb_enc_associate(o, enc_u32le);
  else
    return rb_enc_associate(o, enc_8bit);
}
#  define ENC_U16SWAP(o, src) enc_utf16swap(o, src)
#  define ENC_U32SWAP(o, src) enc_utf32swap(o, src)
#else
#  define ENC_U8(o) (o)
#  define ENC_U16LE(o) (o)
#  define ENC_U16BE(o) (o)
#  define ENC_U32LE(o) (o)
#  define ENC_U32LE(o) (o)
#  define ENC_EUC(o) (o)
#  define ENC_SJIS(o) (o)
#  define ENC_U16SWAP(o, src) (o)
#  define ENC_U32SWAP(o, src) (o)
#endif

/*#define UTF32*/

#define REPLACEMENT_CHAR (0xFFFD)

static VALUE mUconv;

#define ELIMINATE_ZWNBSP_FLAG (1)
#define SHORTEST_FLAG (1)
#define REPLACE_INVALID (0)
#ifdef UCONV_THREAD_LOCAL
static ID id_eliminate_zwnbsp_flag;
static ID id_shortest_flag;
static ID id_replace_invalid;
#else
static int eliminate_zwnbsp_flag = ELIMINATE_ZWNBSP_FLAG;
static int shortest_flag = SHORTEST_FLAG;
static unsigned int replace_invalid = REPLACE_INVALID;
#endif
static int f_eliminate_zwnbsp_flag(void);
static int f_shortest_flag(void);
static unsigned int f_replace_invalid(void);

static VALUE eUconvError;

static ID id_unicode_eucjp_hook;
static ID id_unicode_sjis_hook;
static ID id_eucjp_hook;
static ID id_sjis_hook;

static ID id_unknown_euc_handler;
static ID id_unknown_sjis_handler;
static ID id_unknown_unicode_eucjp_handler;
static ID id_unknown_unicode_sjis_handler;
static ID id_unknown_unicode_handler;
static ID id_call;

#define CHECK_PROC(proc) do { \
  if (CLASS_OF(proc) != rb_cProc && proc != Qnil) \
    rb_raise(rb_eTypeError, "wrong argument type");     \
} while(0)

/* Convert UTF-8 to UTF-16-LE (byte order: 21) */
static int
_u8tou16(unsigned char* in, UString* out)
{
  unsigned int u = 0;
  size_t inlen;
  unsigned int rep = f_replace_invalid();
  int sh = f_shortest_flag();

  UStr_alloc(out);
  inlen = strlen((char*)in);
  while (inlen > 0) {
    unsigned char c = in[0];
    if ((c & 0x80) == 0) { /* 0b0nnnnnnn (7bit) */
      if (c == 0)
	rb_warn("input may not be UTF-8 text!");
      UStr_addChar2(out, c, 0);
      in++;
      inlen--;
    }
    else if ((c & 0xe0) == 0xc0 && /* 0b110nnnnn (11bit) */
	     inlen >= 2 &&
	     (in[1] & 0xc0) == 0x80) {
      if (sh && (c == 0xc0 || c == 0xc1)) {
	if (rep) {
	  UStr_addWCharToU16LE(out, rep);
	  in += 2;
	  inlen -= 2;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 31) << 6) | (in[1] & 63);
      UStr_addChar2(out, u & 0xff, u >> 8);
      in += 2;
      inlen -= 2;
    }
    else if ((c & 0xf0) == 0xe0 && /* 0b1110nnnn (16bit) */
	     inlen >= 3 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80) {
      if (sh && c == 0xe0 && in[1] < 0xa0) {
	if (rep) {
	  UStr_addWCharToU16LE(out, rep);
	  in += 3;
	  inlen -= 3;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 15) << 12) | ((in[1] & 63) << 6) | (in[2] & 63);
      /* surrogate chars */
      if (u >= 0xd800 && u <= 0xdfff) {
	if (rep) {
	  UStr_addWCharToU16LE(out, rep);
	  in += 3;
	  inlen -= 3;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", u);
	}
      }
      UStr_addChar2(out, u & 0xff, u >> 8);
      in += 3;
      inlen -= 3;
    }
    else if ((c & 0xf8) == 0xf0 && /* 0b11110nnn (21bit) */
	     inlen >= 4 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80 &&
	     (in[3] & 0xc0) == 0x80) {
      if (sh && c == 0xf0 && in[1] < 0x90) {
	if (rep) {
	  UStr_addWCharToU16LE(out, rep);
	  in += 4;
	  inlen -= 4;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 7) << 18) | ((in[1] & 63) << 12) |
	((in[2] & 63) << 6) | (in[3] & 63);
      if (u < 0x10000)
	UStr_addChar2(out, u & 255, u >> 8);
      else if (u < 0x110000) {
	unsigned int high = ((u - 0x10000) >> 10) | 0xd800;
	unsigned int low = (u & 1023) | 0xdc00;
	UStr_addChar4(out, high & 255, high >> 8, low & 255, low >> 8);
      }
      else {
	if (rep) {
	  UStr_addWCharToU16LE(out, rep);
	  in += 4;
	  inlen -= 4;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", u);
	}
      }
      in += 4;
      inlen -= 4;
    }
    else {
      if (rep) {
	UStr_addWCharToU16LE(out, rep);
	in++;
	inlen--;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "illegal UTF-8 sequence (0x%02x)", c);
      }
    }
  }

  return out->len;
}

/* Convert UTF-8 to UCS-4-LE (byte order: 4321) */
static int
_u8tou4(unsigned char* in, UString* out)
{
  unsigned int u = 0;
  size_t inlen;
  unsigned int rep = f_replace_invalid();
  int sh = f_shortest_flag();

  UStr_alloc(out);
  inlen = strlen((char*)in);
  while (inlen > 0) {
    unsigned char c = in[0];
    if ((c & 0x80) == 0) { /* 0b0nnnnnnn (7bit) */
      if (c == 0)
	rb_warn("input may not be UTF-8 text!");
      UStr_addChar4(out, c, 0, 0, 0);
      in++;
      inlen--;
    }
    else if ((c & 0xe0) == 0xc0 && /* 0b110nnnnn (11bit) */
	     inlen >= 2 &&
	     (in[1] & 0xc0) == 0x80) {
      if (sh && (c == 0xc0 || c == 0xc1)) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 2;
	  inlen -= 2;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 31) << 6) | (in[1] & 63);
      UStr_addChar4(out, u & 0xff, u >> 8, 0, 0);
      in += 2;
      inlen -= 2;
    }
    else if ((c & 0xf0) == 0xe0 && /* 0b1110nnnn (16bit) */
	     inlen >= 3 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80) {
      if (sh && c == 0xe0 && in[1] < 0xa0) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 3;
	  inlen -= 3;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 15) << 12) | ((in[1] & 63) << 6) | (in[2] & 63);
      /* surrogate chars */
      if (u >= 0xd800 && u <= 0xdfff) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 3;
	  inlen -= 3;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", u);
	}
      }
      UStr_addChar4(out, u & 0xff, u >> 8, 0, 0);
      in += 3;
      inlen -= 3;
    }
    else if ((c & 0xf8) == 0xf0 && /* 0b11110nnn (21bit) */
	     inlen >= 4 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80 &&
	     (in[3] & 0xc0) == 0x80) {
      if (sh && c == 0xf0 && in[1] < 0x90) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 4;
	  inlen -= 4;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 7) << 18) | ((in[1] & 63) << 12) |
	((in[2] & 63) << 6) | (in[3] & 63);
#ifdef UTF32
      if (u < 0x110000) { /* UTF-32 ???? */
#endif /* UTF32 */
	UStr_addChar4(out, u & 0xff, (u >> 8) & 0xff,
		      (u >>16) & 0xff, u >> 24);
#ifdef UTF32
      }
      else {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 4;
	  inlen -= 4;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", u);
	}
      }
#endif /* UTF32 */
      in += 4;
      inlen -= 4;
    }
#ifndef UTF32
    else if ((c & 0xfc) == 0xf8 && /* 0b111110nn (26bit) */
	     inlen >= 5 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80 &&
	     (in[3] & 0xc0) == 0x80 &&
	     (in[4] & 0xc0) == 0x80) {
      if (sh && c == 0xf8 && in[1] < 0x88) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 5;
	  inlen -= 5;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 3) << 24) | ((in[1] & 63) << 18) |
	((in[2] & 63) << 12) | ((in[3] & 63) << 6) | (in[4] & 63);
      UStr_addChar4(out, u & 0xff, (u >> 8) & 0xff,
		    (u >>16) & 0xff, u >> 24);
      in += 5;
      inlen -= 5;
    }
    else if ((c & 0xfe) == 0xfc && /* 0b1111110n (31bit) */
	     inlen >= 6 &&
	     (in[1] & 0xc0) == 0x80 &&
	     (in[2] & 0xc0) == 0x80 &&
	     (in[3] & 0xc0) == 0x80 &&
	     (in[4] & 0xc0) == 0x80 &&
	     (in[5] & 0xc0) == 0x80) {
      if (sh && c == 0xfc && in[1] < 0x84) {
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  in += 6;
	  inlen -= 6;
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "non-shortest UTF-8 sequence (0x%02x)", c);
	}
      }
      u = ((c & 1) << 30) | ((in[1] & 63) << 24) |
	((in[2] & 63) << 18) | ((in[3] & 63) << 12) |
	((in[4] & 63) << 6) | (in[5] & 63);
      UStr_addChar4(out, u & 0xff, (u >> 8) & 0xff,
		    (u >>16) & 0xff, u >> 24);
      in += 6;
      inlen -= 6;
    }
#endif /* !UTF32 */
    else {
      if (rep) {
	UStr_addWCharToU32LE(out, rep);
	in++;
	inlen--;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "illegal UTF-8 sequence (0x%02x)", c);
      }
    }
  }

  return out->len;
}

/* Convert UTF-16-LE (byte order: 21) to UTF-8 */
static int
_u16tou8(unsigned char* in, int len, UString* out, int eliminate_zwnbsp)
{
  int i;
  unsigned int rep = f_replace_invalid();

  UStr_alloc(out);

  if (len < 2) return 0;
  for (i = 0; i < len; i += 2) {
    unsigned int c = in[i] | (in[i+1] << 8);
    if (eliminate_zwnbsp && c == 0xfeff) { /* byte order mark */
      continue;
    }
    else if (c < 128) {         /* 0x0000-0x00FF */
      UStr_addChar(out, c);
    }
    else if (c < 2048) {        /* 0x0100-0x07FF */
      unsigned char b2 = c & 63;
      unsigned char b1 = c >> 6;
      UStr_addChar2(out, b1 | 192, b2 | 128);
    }
    else if (c >= 0xdc00 && c <= 0xdfff) { /* sole low surrogate */
      if (rep) {
	UStr_addWChar(out, rep);
	continue;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "invalid surrogate detected");
      }
    }
    else if (c >= 0xd800 && c <= 0xdbff) { /* high surrogate */
      unsigned int low;
      unsigned char b1, b2, b3, b4;
      if (i + 4 > len) { /* not enough length */
	if (rep) {
	  UStr_addWChar(out, rep);
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid surrogate detected");
	}
      }
      low = in[i+2] | (in[i+3] << 8);
      if (low < 0xdc00 || low > 0xdfff) { /* not low surrogate */
	if (rep) {
	  UStr_addWChar(out, rep);
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid surrogate detected");
	}
      }
      c = (((c & 1023)) << 10 | (low & 1023)) + 0x10000;
      b4 = c & 63;
      b3 = (c >> 6) & 63;
      b2 = (c >> 12) & 63;
      b1 = c >> 18;
      UStr_addChar4(out, b1 | 240, b2 | 128, b3 | 128, b4 | 128);
      i += 2;
    }
    else {                      /* 0x0800-0xFFFF */
      unsigned char b3 = c & 63;
      unsigned char b2 = (c >> 6) & 63;
      unsigned char b1 = c >> 12;
      UStr_addChar3(out, b1 | 224, b2 | 128, b3 | 128);
    }
  }

  return out->len;
}

/* Convert UCS-4-LE (byte order: 4321) to UTF-8 */
static int
_u4tou8(unsigned char* in, int len, UString* out, int eliminate_zwnbsp)
{
  int i;
  unsigned int rep = f_replace_invalid();

  UStr_alloc(out);

  if (len < 4) return 0;
  for (i = 0; i < len; i += 4) {
    unsigned int c = in[i] | (in[i+1] << 8) |
                             (in[i+2] << 16) | (in[i+3] << 24);
    if (eliminate_zwnbsp && c == 0xfeff) { /* byte order mark */
      continue;
    }
    else if (c < 128) {         /* 0x0000-0x00FF */
      UStr_addChar(out, c);
    }
    else if (c < 2048) {        /* 0x0100-0x07FF */
      unsigned char b2 = c & 63;
      unsigned char b1 = c >> 6;
      UStr_addChar2(out, b1 | 192, b2 | 128);
    }
    else if (c >= 0xd800 && c <=0xdfff) { /* surrogate chars */
      if (rep) {
	UStr_addWChar(out, rep);
	continue;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "illegal char detected (0x%04x)", c);
      }
    }
    else if (c < 0x10000) {     /* 0x0800-0xFFFF */
      unsigned char b3 = c & 63;
      unsigned char b2 = (c >> 6) & 63;
      unsigned char b1 = c >> 12;
      UStr_addChar3(out, b1 | 224, b2 | 128, b3 | 128);
    }
#ifdef UTF32
    else if (c < 0x110000) {     /* 0x00010000-0x0010FFFF */
#else
    else if (c < 0x200000) {     /* 0x00010000-0x001FFFFF */
#endif
      unsigned char b4 = c & 63;
      unsigned char b3 = (c >> 6) & 63;
      unsigned char b2 = (c >> 12) & 63;
      unsigned char b1 = c >> 18;
      UStr_addChar4(out, b1 | 240, b2 | 128, b3 | 128, b4 | 128);
    }
#ifndef UTF32
    else if (c < 0x4000000) {     /* 0x00200000-0x03FFFFFF */
      unsigned char b5 = c & 63;
      unsigned char b4 = (c >> 6) & 63;
      unsigned char b3 = (c >> 12) & 63;
      unsigned char b2 = (c >> 18) & 63;
      unsigned char b1 = c >> 24;
      UStr_addChar5(out, b1 | 248, b2 | 128, b3 | 128, b4 | 128, b5 | 128);
    }
    else if (c < 0x80000000) {     /* 0x04000000-0x7FFFFFFF */
      unsigned char b6 = c & 63;
      unsigned char b5 = (c >> 6) & 63;
      unsigned char b4 = (c >> 12) & 63;
      unsigned char b3 = (c >> 18) & 63;
      unsigned char b2 = (c >> 24) & 63;
      unsigned char b1 = (c >> 30) & 63;
      UStr_addChar6(out, b1 | 252, b2 | 128, b3 | 128,
		b4 | 128, b5 | 128, b6 | 128);
    }
#endif /* !UTF32 */
    else {
      if (rep)
	UStr_addWChar(out, rep);
      else {
	UStr_free(out);
	rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
      }
    }
  }

  return out->len;
}

/* Convert UCS-4-LE (byte order: 4321) to UTF-16-LE (byte order: 21) */
/* 10000-10FFFF -> D800-DBFF:DC00-DFFF */
static int
_u4tou16(unsigned char* in, int len, UString* out)
{
  int i;
  unsigned int rep = f_replace_invalid();

  UStr_alloc(out);

  if (len < 4) return 0;
  for (i = 0; i < len; i += 4) {
    unsigned int c = in[i] | (in[i+1] << 8) |
                             (in[i+2] << 16) | (in[i+3] << 24);
    if (c >= 0xd800 && c <= 0xdfff) { /* surrogate chars */
      if (rep) {
	UStr_addWCharToU16LE(out, rep);
	continue;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
      }
    }
    else if (c < 0x10000) {     /* 0x0000-0xFFFF */
      UStr_addChar2(out, in[i], in[i+1]);
    }
    else if (c < 0x110000) {    /* 0x00010000-0x0010FFFF */
      unsigned int high = ((c - 0x10000) >> 10) | 0xd800;
      unsigned int low = (c & 1023) | 0xdc00;
      UStr_addChar4(out, high & 255, high >> 8, low & 255, low >> 8);
    }
    else {
      if (rep) {
	UStr_addWCharToU16LE(out, rep);
	continue;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
      }
    }
  }

  return out->len;
}

/* Convert UTF-16-LE (byte order: 21) to UCS-4-LE (byte order: 4321) */
/* D800-DBFF:DC00-DFFF -> 10000-10FFFF */
static int
_u16tou4(unsigned char* in, int len, UString* out)
{
  int i;
  unsigned int rep = f_replace_invalid();

  UStr_alloc(out);

  if (len < 2) return 0;
  for (i = 0; i < len; i += 2) {
    unsigned int c = in[i] | (in[i+1] << 8);
    if (c >= 0xdc00 && c <= 0xdfff) { /* sole low surrogate */
      if (rep) {
	UStr_addWCharToU32LE(out, rep);
	continue;
      }
      else {
	UStr_free(out);
	rb_raise(eUconvError, "invalid surrogate detected");
      }
    }
    else if (c >= 0xd800 && c <= 0xdbff) { /* high surrogate */
      unsigned int low;
      if (i + 4 > len) { /* not enough length */
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid surrogate detected");
	}
      }
      low = in[i+2] | (in[i+3] << 8);
      if (low < 0xdc00 || low > 0xdfff) { /* not low surrogate */
	if (rep) {
	  UStr_addWCharToU32LE(out, rep);
	  continue;
	}
	else {
	  UStr_free(out);
	  rb_raise(eUconvError, "invalid surrogate detected");
	}
      }
      c = (((c & 1023)) << 10 | (low & 1023)) + 0x10000;
      UStr_addChar4(out, c & 255, (c >> 8) & 255, c >> 16, 0);
      i += 2;
    }
    else {
      UStr_addChar4(out, in[i], in[i+1], 0, 0);
    }
  }

  return out->len;
}

static VALUE
exception_handler(VALUE args, VALUE errinfo) {
  return errinfo;
}

static VALUE
call_unicode_handler(unsigned long code)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_unknown_unicode_handler, 1, INT2FIX(code));
  Check_Type(ret, T_STRING);
  return ret;
}

static VALUE
unknown_unicode_handler(unsigned long code)
{
  return rb_rescue((VALUE (*)(void*))call_unicode_handler, code,
		   exception_handler, Qnil);
}


#ifdef USE_EUC
static VALUE
call_euc_handler(const unsigned char* seq)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_unknown_euc_handler, 1, rb_str_new2((char*)seq));
  unsigned int c;
  Check_Type(ret, T_FIXNUM);
  c = FIX2INT(ret);
  if (c > 0x10ffff)
    rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  return ret;
}

static VALUE
unknown_euc_handler(const unsigned char* seq)
{
  return rb_rescue((VALUE (*)(void*))call_euc_handler, (int)seq,
		   exception_handler, Qnil);
}

static VALUE
unknown_euc_proc(const unsigned char* seq) {
  VALUE proc;
  VALUE ret;
  unsigned int c;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unknown_euc_handler);
#else
  proc = rb_ivar_get(mUconv, id_unknown_euc_handler);
#endif
  ret = rb_funcall(proc, id_call, 1, rb_str_new2((char*)seq));
  Check_Type(ret, T_FIXNUM);
  c = FIX2INT(ret);
  if (c > 0x10ffff)
    rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  return ret;
}

static unknown_euc*
check_euc_handler()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unknown_euc_handler) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unknown_euc_handler) != Qnil)
#endif
    return unknown_euc_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_euc_handler, 0))
    return unknown_euc_handler;
  return NULL;
}

static VALUE
call_e2u_hook(const unsigned char* seq)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_eucjp_hook, 1, rb_str_new2((char*)seq));
  if (ret != Qnil) {
    unsigned int c;
    Check_Type(ret, T_FIXNUM);
    c = FIX2INT(ret);
    if (c > 0x10ffff)
      rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  }
  return ret;
}

static VALUE
e2u_hook(const unsigned char* seq)
{
  return rb_rescue((VALUE (*)(void*))call_e2u_hook, (int)seq,
		   exception_handler, Qnil);
}

static VALUE
e2u_proc(const unsigned char* seq) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_eucjp_hook);
#else
  proc = rb_ivar_get(mUconv, id_eucjp_hook);
#endif
  ret = rb_funcall(proc, id_call, 1, rb_str_new2((char*)seq));
  if (ret != Qnil) {
    unsigned int c;
    Check_Type(ret, T_FIXNUM);
    c = FIX2INT(ret);
    if (c > 0x10ffff)
      rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  }
  return ret;
}

static unknown_sjis*
check_e2u_hook()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_eucjp_hook) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_eucjp_hook) != Qnil)
#endif
    return e2u_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_eucjp_hook, 0))
    return e2u_hook;
  return NULL;
}

static VALUE
call_u2e_handler(unsigned long code)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_unknown_unicode_eucjp_handler, 1, INT2FIX(code));
  Check_Type(ret, T_STRING);
  return ret;
}

static VALUE
unknown_u2e_handler(unsigned long code)
{
  return rb_rescue((VALUE (*)(void*))call_u2e_handler, code,
		   exception_handler, Qnil);
}

static VALUE
unknown_u2e_proc(unsigned long code) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unknown_unicode_eucjp_handler);
#else
  proc = rb_ivar_get(mUconv, id_unknown_unicode_eucjp_handler);
#endif
  ret = rb_funcall(proc, id_call, 1, INT2FIX(code));
  Check_Type(ret, T_STRING);
  return ret;
}

static unknown_unicode*
check_unknown_u2e_handler()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unknown_unicode_eucjp_handler) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unknown_unicode_eucjp_handler) != Qnil)
#endif
    return unknown_u2e_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_unicode_eucjp_handler, 0))
    return unknown_u2e_handler;
  else if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_unicode_handler, 0))
    return unknown_unicode_handler;
  return NULL;
}

static VALUE
call_u2e_hook(unsigned long code)
{
  VALUE ret;

  ret = rb_funcall((VALUE)mUconv, id_unicode_eucjp_hook, 1, INT2FIX(code));
  if (ret != Qnil)
    Check_Type(ret, T_STRING);
  return ret;
}

static VALUE
u2e_hook(unsigned long code)
{
  return rb_rescue((VALUE (*)(void*))call_u2e_hook, code,
		   exception_handler, Qnil);
}

static VALUE
u2e_proc(unsigned long code) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unicode_eucjp_hook);
#else
  proc = rb_ivar_get(mUconv, id_unicode_eucjp_hook);
#endif
  ret = rb_funcall(proc, id_call, 1, INT2FIX(code));
  if (ret != Qnil)
    Check_Type(ret, T_STRING);
  return ret;
}

static unknown_unicode*
check_u2e_hook()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unicode_eucjp_hook) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unicode_eucjp_hook) != Qnil)
#endif
    return u2e_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unicode_eucjp_hook, 0))
    return u2e_hook;
  return NULL;
}
#endif /* USE_EUC */

#ifdef USE_SJIS
static VALUE
call_sjis_handler(const unsigned char* seq)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_unknown_sjis_handler, 1, rb_str_new2((char*)seq));
  unsigned int c;
  Check_Type(ret, T_FIXNUM);
  c = FIX2INT(ret);
  if (c > 0x10ffff)
    rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  return ret;
}

static VALUE
unknown_sjis_handler(const unsigned char* seq)
{
  return rb_rescue((VALUE (*)(void*))call_sjis_handler, (int)seq,
		   exception_handler, Qnil);
}

static VALUE
unknown_sjis_proc(const unsigned char* seq) {
  VALUE proc;
  VALUE ret;
  unsigned int c;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unknown_sjis_handler);
#else
  proc = rb_ivar_get(mUconv, id_unknown_sjis_handler);
#endif
  ret = rb_funcall(proc, id_call, 1, rb_str_new2((char*)seq));
  Check_Type(ret, T_FIXNUM);
  c = FIX2INT(ret);
  if (c > 0x10ffff)
    rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  return ret;
}

static unknown_sjis*
check_sjis_handler()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unknown_sjis_handler) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unknown_sjis_handler) != Qnil)
#endif
    return unknown_sjis_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_sjis_handler, 0))
    return unknown_sjis_handler;
  return NULL;
}

static VALUE
call_s2u_hook(const unsigned char* seq)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_sjis_hook, 1, rb_str_new2((char*)seq));
  if (ret != Qnil) {
    unsigned int c;
    Check_Type(ret, T_FIXNUM);
    c = FIX2INT(ret);
    if (c > 0x10ffff)
      rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  }
  return ret;
}

static VALUE
s2u_hook(const unsigned char* seq)
{
  return rb_rescue((VALUE (*)(void*))call_s2u_hook, (int)seq,
		   exception_handler, Qnil);
}

static VALUE
s2u_proc(const unsigned char* seq) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_sjis_hook);
#else
  proc = rb_ivar_get(mUconv, id_sjis_hook);
#endif
  ret = rb_funcall(proc, id_call, 1, rb_str_new2((char*)seq));
  if (ret != Qnil) {
    unsigned int c;
    Check_Type(ret, T_FIXNUM);
    c = FIX2INT(ret);
    if (c > 0x10ffff)
      rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", c);
  }
  return ret;
}

static unknown_sjis*
check_s2u_hook()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_sjis_hook) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_sjis_hook) != Qnil)
#endif
    return s2u_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_sjis_hook, 0))
    return s2u_hook;
  return NULL;
}

static VALUE
call_u2s_handler(unsigned long code)
{
  VALUE ret = rb_funcall((VALUE)mUconv,
                         id_unknown_unicode_sjis_handler, 1, INT2FIX(code));
  Check_Type(ret, T_STRING);
  return ret;
}

static VALUE
unknown_u2s_handler(unsigned long code)
{
  return rb_rescue((VALUE (*)(void*))call_u2s_handler, code,
		   exception_handler, Qnil);
}

static VALUE
unknown_u2s_proc(unsigned long code) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unknown_unicode_sjis_handler);
#else
  proc = rb_ivar_get(mUconv, id_unknown_unicode_sjis_handler);
#endif
  ret = rb_funcall(proc, id_call, 1, INT2FIX(code));
  Check_Type(ret, T_STRING);
  return ret;
}

static unknown_unicode*
check_unknown_u2s_handler()
{
 #ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unknown_unicode_sjis_handler) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unknown_unicode_sjis_handler) != Qnil)
#endif
    return unknown_u2s_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_unicode_sjis_handler, 0))
    return unknown_u2s_handler;
  else if (rb_method_boundp(CLASS_OF(mUconv), id_unknown_unicode_handler, 0))
    return unknown_unicode_handler;
  return NULL;
}

static VALUE
call_u2s_hook(unsigned long code)
{
  VALUE ret;

  ret = rb_funcall((VALUE)mUconv, id_unicode_sjis_hook, 1, INT2FIX(code));
  if (ret != Qnil)
    Check_Type(ret, T_STRING);
  return ret;
}

static VALUE
u2s_hook(unsigned long code)
{
  return rb_rescue((VALUE (*)(void*))call_u2s_hook, code,
		   exception_handler, Qnil);
}

static VALUE
u2s_proc(unsigned long code) {
  VALUE proc;
  VALUE ret;
#ifdef UCONV_THREAD_LOCAL
  proc = rb_thread_local_aref(rb_thread_current(),
                              id_unicode_sjis_hook);
#else
  proc = rb_ivar_get(mUconv, id_unicode_sjis_hook);
#endif
  ret = rb_funcall(proc, id_call, 1, INT2FIX(code));
  if (ret != Qnil)
    Check_Type(ret, T_STRING);
  return ret;
}

static unknown_unicode*
check_u2s_hook()
{
#ifdef UCONV_THREAD_LOCAL
  if (rb_thread_local_aref(rb_thread_current(),
                           id_unicode_sjis_hook) != Qnil)
#else
  if (rb_ivar_get(mUconv, id_unicode_sjis_hook) != Qnil)
#endif
    return u2s_proc;
  if (rb_method_boundp(CLASS_OF(mUconv), id_unicode_sjis_hook, 0))
    return u2s_hook;
  return NULL;
}
#endif /* USE_SJIS */


#ifdef USE_EUC
static VALUE
uconv_u2toeuc(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* u;
  VALUE ret;
  UString e;

  Check_Type(wstr, T_STRING);

  u = (unsigned char*)(RSTRING_PTR(wstr));
  len = RSTRING_LEN(wstr);
  if (u) {
    u2e_conv2(u, len, &e, check_unknown_u2e_handler(), check_u2e_hook());
    ret = rb_str_new((char*)e.str, e.len);
    UStr_free(&e);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ret, wstr);

  return ret;
}

static VALUE
uconv_euctou2(VALUE obj, VALUE estr)
{
  int len;
  unsigned char* e;
  UString u;
  VALUE ret;

  Check_Type(estr, T_STRING);

  e = (unsigned char*)(RSTRING_PTR(estr));
  if (e) {
    len = e2u_conv2(e, &u, check_euc_handler(), check_e2u_hook());

    ret = rb_str_new((char*)u.str, u.len);
    UStr_free(&u);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U16LE(ret), estr);

  return ret;
}
#endif /* USE_EUC */

#ifdef USE_SJIS
static VALUE
uconv_u2tosjis(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* u;
  VALUE ret;
  UString s;

  Check_Type(wstr, T_STRING);

  u = (unsigned char*)(RSTRING_PTR(wstr));
  if (u) {
    len = RSTRING_LEN(wstr);

    u2s_conv2(u, len, &s, check_unknown_u2s_handler(), check_u2s_hook());
    ret = rb_str_new((char*)s.str, s.len);
    UStr_free(&s);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_SJIS(ret), wstr);

  return ret;
}

static VALUE
uconv_sjistou2(VALUE obj, VALUE sstr)
{
  int len;
  unsigned char* s;
  UString u;
  VALUE ret;

  Check_Type(sstr, T_STRING);

  s = (unsigned char*)(RSTRING_PTR(sstr));
  if (s) {
    len = s2u_conv2(s, &u, check_sjis_handler(), check_s2u_hook());

    ret = rb_str_new((char*)u.str, u.len);
    UStr_free(&u);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U16LE(ret), sstr);

  return ret;
}
#endif /* USE_SJIS */

static VALUE
uconv_u2swap(VALUE obj, VALUE wstr)
{
  int len;
  int i;
  unsigned char* u;
  unsigned char* out;
  VALUE ret;

  Check_Type(wstr, T_STRING);

  u = (unsigned char*)(RSTRING_PTR(wstr));
  len = RSTRING_LEN(wstr);
  if (!u || len < 2) return Qnil;
  ret = rb_str_new(NULL, len);
  out = (unsigned char*)(RSTRING_PTR(ret));

  for (i = 0; i < len; i+=2) {
    out[i] = u[i+1];
    out[i+1] = u[i];
  }
  OBJ_INFECT(ret, wstr);

  return ENC_U16SWAP(ret, wstr);
}

static VALUE
uconv_u2swap_b(VALUE obj, VALUE wstr)
{
  int len;
  int i;
  unsigned char* u;

  Check_Type(wstr, T_STRING);

  rb_str_modify(wstr);
  u = (unsigned char*)(RSTRING_PTR(wstr));
  len = RSTRING_LEN(wstr);
  if (!u || len < 2) return Qnil;

  for (i = 0; i < len; i+=2) {
    register unsigned char tmp = u[i+1];
    u[i+1] = u[i];
    u[i] = tmp;
  }
  return ENC_U16SWAP(wstr, wstr);
}

static VALUE
uconv_u4swap(VALUE obj, VALUE wstr)
{
  int len;
  int i;
  unsigned char* u;
  unsigned char* out;
  VALUE ret;

  Check_Type(wstr, T_STRING);

  u = (unsigned char*)(RSTRING_PTR(wstr));
  len = RSTRING_LEN(wstr);
  if (!u || len < 4) return Qnil;
  ret = rb_str_new(NULL, len);
  out = (unsigned char*)(RSTRING_PTR(ret));

  for (i = 0; i < len; i+=4) {
    out[i]   = u[i+3];
    out[i+1] = u[i+2];
    out[i+2] = u[i+1];
    out[i+3] = u[i];
  }
  OBJ_INFECT(ret, wstr);

  return ENC_U32SWAP(ret, wstr);
}

static VALUE
uconv_u4swap_b(VALUE obj, VALUE wstr)
{
  int len;
  int i;
  unsigned char* u;

  Check_Type(wstr, T_STRING);

  rb_str_modify(wstr);
  u = (unsigned char*)(RSTRING_PTR(wstr));
  len = RSTRING_LEN(wstr);
  if (len < 4) return Qnil;

  for (i = 0; i < len; i+=4) {
    register unsigned char tmp1 = u[i];
    register unsigned char tmp2 = u[i+1];
    u[i] = u[i+3];
    u[i+1] = u[i+2];
    u[i+2] = tmp2;
    u[i+3] = tmp1;
  }
  return ENC_U32SWAP(wstr, wstr);
}


static VALUE
uconv_u8tou16(VALUE obj, VALUE ustr)
{
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(ustr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(ustr));
  if (in) {
    _u8tou16(in, &out);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U16LE(ret), ustr);

  return ret;
}

static VALUE
uconv_u16tou8(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(wstr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(wstr));
  if (in) {
    len = RSTRING_LEN(wstr);
    _u16tou8(in, len, &out, f_eliminate_zwnbsp_flag());
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U8(ret), wstr);

  return ret;
}


static VALUE
uconv_u8tou4(VALUE obj, VALUE ustr)
{
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(ustr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(ustr));
  if (in) {
    _u8tou4(in, &out);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U32LE(ret), ustr);

  return ret;
}

static VALUE
uconv_u4tou8(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(wstr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(wstr));
  if (in) {
    len = RSTRING_LEN(wstr);
    _u4tou8(in, len, &out, f_eliminate_zwnbsp_flag());
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U8(ret), wstr);

  return ret;
}


static VALUE
uconv_u16tou4(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(wstr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(wstr));
  if (in) {
    len = RSTRING_LEN(wstr);
    _u16tou4(in, len, &out);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U32LE(ret), wstr);

  return ret;
}

static VALUE
uconv_u4tou16(VALUE obj, VALUE wstr)
{
  int len;
  unsigned char* in;
  UString out;
  VALUE ret;

  Check_Type(wstr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(wstr));
  if (in) {
    len = RSTRING_LEN(wstr);
    _u4tou16(in, len, &out);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U16LE(ret), wstr);

  return ret;
}

#ifdef USE_EUC
static VALUE
uconv_u8toeuc(VALUE obj, VALUE ustr)
{
  unsigned char* in;
  UString out;
  UString e;
  VALUE ret;
  volatile VALUE vout;

  Check_Type(ustr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(ustr));
  if (in) {
    _u8tou16(in, &out);
    vout = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
    u2e_conv2((unsigned char*)(RSTRING_PTR(vout)), RSTRING_LEN(vout),
	      &e, check_unknown_u2e_handler(), check_u2e_hook());
    ret = rb_str_new((char*)e.str, e.len);
    UStr_free(&e);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_EUC(ret), ustr);

  return ret;
}

static VALUE
uconv_euctou8(VALUE obj, VALUE estr)
{
  unsigned char* e;
  UString in;
  UString out;
  VALUE ret;
  VALUE vin;

  Check_Type(estr, T_STRING);
  e = (unsigned char*)(RSTRING_PTR(estr));
  if (e) {
    e2u_conv2(e, &in, check_euc_handler(), check_e2u_hook());
    vin = rb_str_new((char*)in.str, in.len);
    UStr_free(&in);
    _u16tou8((unsigned char*)(RSTRING_PTR(vin)), RSTRING_LEN(vin), &out, 1);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U8(ret), estr);

  return ret;
}
#endif /* USE_EUC */

#ifdef USE_SJIS
static VALUE
uconv_u8tosjis(VALUE obj, VALUE ustr)
{
  unsigned char* in;
  UString out;
  UString s;
  VALUE ret;
  volatile VALUE vout;

  Check_Type(ustr, T_STRING);
  in = (unsigned char*)(RSTRING_PTR(ustr));
  if (in) {
    _u8tou16(in, &out);
    vout = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
    u2s_conv2((unsigned char*)(RSTRING_PTR(vout)), RSTRING_LEN(vout),
	      &s, check_unknown_u2s_handler(), check_u2s_hook());
    ret = rb_str_new((char*)s.str, s.len);
    UStr_free(&s);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_SJIS(ret), ustr);

  return ret;
}

static VALUE
uconv_sjistou8(VALUE obj, VALUE sstr)
{
  unsigned char* s;
  UString in;
  UString out;
  VALUE ret;
  VALUE vin;

  Check_Type(sstr, T_STRING);
  s = (unsigned char*)(RSTRING_PTR(sstr));
  if (s) {
    s2u_conv2(s, &in, check_sjis_handler(), check_s2u_hook());
    vin = rb_str_new((char*)in.str, in.len);
    UStr_free(&in);
    _u16tou8((unsigned char*)(RSTRING_PTR(vin)), RSTRING_LEN(vin), &out, 1);
    ret = rb_str_new((char*)out.str, out.len);
    UStr_free(&out);
  }
  else {
    ret = rb_str_new(NULL, 0);
  }
  OBJ_INFECT(ENC_U8(ret), sstr);

  return ret;
}
#endif /* USE_SJIS */

static int
f_eliminate_zwnbsp_flag()
{
#ifdef UCONV_THREAD_LOCAL
  VALUE val = rb_thread_local_aref(rb_thread_current(),
                                   id_eliminate_zwnbsp_flag);
  if (val == Qnil)
    return ELIMINATE_ZWNBSP_FLAG;
  else if (val == Qtrue)
    return 1;
  return 0;
#else
  return eliminate_zwnbsp_flag;
#endif
}

static VALUE
get_eliminate_zwnbsp_flag(VALUE obj)
{
  if (f_eliminate_zwnbsp_flag() == 0) return Qfalse;
  return Qtrue;
}

static VALUE
set_eliminate_zwnbsp_flag(VALUE obj, VALUE flag)
{
  int f = 0;
  if (flag == Qtrue)
    f = 1;
  else if (flag != Qfalse)
    rb_raise(rb_eTypeError, "wrong argument type");
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(),
                       id_eliminate_zwnbsp_flag,
                       flag);
#else
  eliminate_zwnbsp_flag = f;
#endif
  return flag;
}

static inline int
f_shortest_flag()
{
#ifdef UCONV_THREAD_LOCAL
  VALUE val = rb_thread_local_aref(rb_thread_current(),
                                   id_shortest_flag);
  if (val == Qnil)
    return SHORTEST_FLAG;
  else if (val == Qtrue)
    return 1;
  return 0;
#else
  return shortest_flag;
#endif
}

static VALUE
get_shortest_flag(VALUE obj)
{
  if (f_shortest_flag() == 0) return Qfalse;
  return Qtrue;
}

static VALUE
set_shortest_flag(VALUE obj, VALUE flag)
{
  int f = 0;
  if (flag == Qtrue)
    f = 1;
  else if (flag != Qfalse)
    rb_raise(rb_eTypeError, "wrong argument type");
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(),
                       id_shortest_flag,
                       flag);
#else
  shortest_flag = f;
#endif
  return flag;
}

static inline unsigned int
f_replace_invalid()
{
#ifdef UCONV_THREAD_LOCAL
  VALUE val = rb_thread_local_aref(rb_thread_current(),
                                   id_replace_invalid);
  if (val == Qnil)
    return REPLACE_INVALID;
  return NUM2UINT(val);
#else
  return replace_invalid;
#endif
}

static VALUE
get_replace_invalid(VALUE obj)
{
  unsigned int ret = f_replace_invalid();
  if (ret == 0) return Qnil;
  return UINT2NUM(ret);
}

static VALUE
set_replace_invalid(VALUE obj, VALUE uchar)
{
  unsigned int rep = 0;
  if (uchar == Qnil) {
    rep = 0;
  }
  else {
    rep = NUM2UINT(uchar);
  }
  if (rep > 0x10ffff)
    rb_raise(eUconvError, "invalid Unicode char detected (U-%04x)", rep);

#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(),
                       id_replace_invalid,
                       uchar);
#else
  replace_invalid = rep;
#endif
  return uchar;
}

#ifdef USE_EUC
static VALUE
set_unicode_euc_hook(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_unicode_eucjp_hook, proc);
#else
  rb_ivar_set(mUconv, id_unicode_eucjp_hook, proc);
#endif
  return Qnil;
}

static VALUE
set_euc_hook(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_eucjp_hook, proc);
#else
  rb_ivar_set(mUconv, id_eucjp_hook, proc);
#endif
  return Qnil;
}

static VALUE
set_unkwon_euc_handler(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_unknown_euc_handler, proc);
#else
  rb_ivar_set(mUconv, id_unknown_euc_handler, proc);
#endif
  return Qnil;
}

static VALUE
set_unkwon_unicode_euc_handler(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(),
                       id_unknown_unicode_eucjp_handler, proc);
#else
  rb_ivar_set(mUconv, id_unknown_unicode_eucjp_handler, proc);
#endif
  return Qnil;
}
#endif /* USE_EUC */

#ifdef USE_SJIS
static VALUE
set_unicode_sjis_hook(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_unicode_sjis_hook, proc);
#else
  rb_ivar_set(mUconv, id_unicode_sjis_hook, proc);
#endif
  return Qnil;
}

static VALUE
set_sjis_hook(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_sjis_hook, proc);
#else
  rb_ivar_set(mUconv, id_sjis_hook, proc);
#endif
  return Qnil;
}

static VALUE
set_unkwon_sjis_handler(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(), id_unknown_sjis_handler, proc);
#else
  rb_ivar_set(mUconv, id_unknown_sjis_handler, proc);
#endif
  return Qnil;
}

static VALUE
set_unkwon_unicode_sjis_handler(VALUE obj, VALUE proc)
{
  CHECK_PROC(proc);
#ifdef UCONV_THREAD_LOCAL
  rb_thread_local_aset(rb_thread_current(),
                       id_unknown_unicode_sjis_handler, proc);
#else
  rb_ivar_set(mUconv, id_unknown_unicode_sjis_handler, proc);
#endif
  return Qnil;
}
#endif /* USE_SJIS */

void
Init_uconv()
{
#ifdef HAVE_RUBY_ENCODING_H
  enc_u8 = rb_utf8_encoding();
  enc_u16le = rb_enc_find("UTF-16LE");
  enc_u16be = rb_enc_find("UTF-16BE");
  enc_u32le = rb_enc_find("UTF-32LE");
  enc_u32be = rb_enc_find("UTF-32BE");
  enc_euc = rb_enc_find("EUC-JP");
  enc_sjis = rb_enc_find("Windows-31J");
  enc_8bit = rb_ascii8bit_encoding();
#endif

  if (rb_const_defined(rb_cObject, rb_intern("Uconv")) == Qtrue)
    mUconv = rb_const_get(rb_cObject, rb_intern("Uconv"));
  else
    mUconv = rb_define_module("Uconv");
  eUconvError = rb_define_class_under(mUconv,
				      "Error", rb_eStandardError);

#ifdef USE_EUC
  id_unicode_eucjp_hook = rb_intern("unicode_euc_hook");
  id_eucjp_hook = rb_intern("euc_hook");
  id_unknown_euc_handler = rb_intern("unknown_euc_handler");
  id_unknown_unicode_eucjp_handler = rb_intern("unknown_unicode_euc_handler");
#endif
#ifdef USE_SJIS
  id_unicode_sjis_hook = rb_intern("unicode_sjis_hook");
  id_sjis_hook = rb_intern("sjis_hook");
  id_unknown_sjis_handler = rb_intern("unknown_sjis_handler");
  id_unknown_unicode_sjis_handler = rb_intern("unknown_unicode_sjis_handler");
#endif
  id_unknown_unicode_handler = rb_intern("unknown_unicode_handler");
  id_call = rb_intern("call");

#ifdef UCONV_THREAD_LOCAL
  id_eliminate_zwnbsp_flag = rb_intern("__eliminate_zwnbsp_flag__");
  id_shortest_flag = rb_intern("__shortest_flag__");
  id_replace_invalid = rb_intern("__replace_invalid__");
#endif

#ifdef USE_EUC
  rb_define_module_function(mUconv, "u16toeuc", uconv_u2toeuc, 1);
  rb_define_module_function(mUconv, "euctou16", uconv_euctou2, 1);
  rb_define_module_function(mUconv, "u2toeuc", uconv_u2toeuc, 1);
  rb_define_module_function(mUconv, "euctou2", uconv_euctou2, 1);

  rb_define_module_function(mUconv, "u8toeuc", uconv_u8toeuc, 1);
  rb_define_module_function(mUconv, "euctou8", uconv_euctou8, 1);
#endif

#ifdef USE_SJIS
  rb_define_module_function(mUconv, "u16tosjis", uconv_u2tosjis, 1);
  rb_define_module_function(mUconv, "sjistou16", uconv_sjistou2, 1);
  rb_define_module_function(mUconv, "u2tosjis", uconv_u2tosjis, 1);
  rb_define_module_function(mUconv, "sjistou2", uconv_sjistou2, 1);

  rb_define_module_function(mUconv, "u8tosjis", uconv_u8tosjis, 1);
  rb_define_module_function(mUconv, "sjistou8", uconv_sjistou8, 1);
#endif

  rb_define_module_function(mUconv, "u16swap", uconv_u2swap, 1);
  rb_define_module_function(mUconv, "u16swap!", uconv_u2swap_b, 1);
  rb_define_module_function(mUconv, "u2swap", uconv_u2swap, 1);
  rb_define_module_function(mUconv, "u2swap!", uconv_u2swap_b, 1);
  rb_define_module_function(mUconv, "u4swap", uconv_u4swap, 1);
  rb_define_module_function(mUconv, "u4swap!", uconv_u4swap_b, 1);

  rb_define_module_function(mUconv, "u8tou16", uconv_u8tou16, 1);
  rb_define_module_function(mUconv, "u8tou2",  uconv_u8tou16, 1);
  rb_define_module_function(mUconv, "u16tou8", uconv_u16tou8, 1);
  rb_define_module_function(mUconv, "u2tou8",  uconv_u16tou8, 1);
  rb_define_module_function(mUconv, "u8tou4", uconv_u8tou4, 1);
  rb_define_module_function(mUconv, "u4tou8", uconv_u4tou8, 1);
  rb_define_module_function(mUconv, "u16tou4", uconv_u16tou4, 1);
  rb_define_module_function(mUconv, "u4tou16", uconv_u4tou16, 1);

  rb_define_module_function(mUconv, "eliminate_zwnbsp",
			    get_eliminate_zwnbsp_flag, 0);
  rb_define_module_function(mUconv, "eliminate_zwnbsp=",
			    set_eliminate_zwnbsp_flag, 1);
  rb_define_module_function(mUconv, "shortest",
			    get_shortest_flag, 0);
  rb_define_module_function(mUconv, "shortest=",
			    set_shortest_flag, 1);
  rb_define_module_function(mUconv, "replace_invalid",
			    get_replace_invalid, 0);
  rb_define_module_function(mUconv, "replace_invalid=",
			    set_replace_invalid, 1);

#ifdef USE_EUC
  rb_define_module_function(mUconv, "unicode_euc_hook=",
			    set_unicode_euc_hook, 1);
  rb_define_module_function(mUconv, "euc_hook=",
			    set_euc_hook, 1);
  rb_define_module_function(mUconv, "unknown_euc_handler=",
			    set_unkwon_euc_handler, 1);
  rb_define_module_function(mUconv, "unknown_unicode_euc_handler=",
			    set_unkwon_unicode_euc_handler, 1);
#endif
#ifdef USE_SJIS
  rb_define_module_function(mUconv, "unicode_sjis_hook=",
			    set_unicode_sjis_hook, 1);
  rb_define_module_function(mUconv, "sjis_hook=",
			    set_sjis_hook, 1);
  rb_define_module_function(mUconv, "unknown_sjis_handler=",
			    set_unkwon_sjis_handler, 1);
  rb_define_module_function(mUconv, "unknown_unicode_sjis_handler=",
			    set_unkwon_unicode_sjis_handler, 1);
#endif
  /*
  rb_define_module_function(mUconv, "unknown_unicode_handler=",
			    set_unkwon_unicode_handler, 1);
  */

  rb_define_const(mUconv, "REPLACEMENT_CHAR",
		  INT2FIX(REPLACEMENT_CHAR));
  rb_define_const(mUconv, "VERSION",
		  rb_str_new2(UCONV_VERSION));
}
