/*
   Unix SMB/CIFS implementation.
   minimal iconv implementation
   Copyright (C) Andrew Tridgell 2001
   Copyright (C) Jelmer Vernooij 2002,2003

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

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

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

   From samba 3.0 beta and GNU libiconv-1.8
   It's bad but most of the time we can't use libc iconv service:
   - it doesn't round trip for most encoding
   - it doesn't know about Apple extension
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>

#include <atalk/byteorder.h>
#include <atalk/logger.h>
#include <atalk/unicode.h>

/*! Given a trailing UTF-8 byte, get the contribution from it to
 * the Unicode scalar value for a particular bit shift amount
 */
#define GETUCVAL(utf8_trailbyte,shift)  ((unsigned int) (( utf8_trailbyte & 0x3F) << shift))

/*! Given a unicode scalar, get a trail UTF-8 byte for a particular bit shift amount */
#define GETUTF8TRAILBYTE(uc,shift)      ((char)( 0x80 | ((uc >> shift) & 0x3F) ) )



static size_t   utf8_pull(void *, char **, size_t *, char **, size_t *);
static size_t   utf8_push(void *, char **, size_t *, char **, size_t *);

struct charset_functions charset_utf8 = {
    "UTF8",
    0x08000103,
    utf8_pull,
    utf8_push,
    CHARSET_VOLUME | CHARSET_MULTIBYTE | CHARSET_PRECOMPOSED,
    NULL,
    NULL,
    NULL
};

struct charset_functions charset_utf8_mac = {
    "UTF8-MAC",
    0x08000103,
    utf8_pull,
    utf8_push,
    CHARSET_VOLUME | CHARSET_CLIENT | CHARSET_MULTIBYTE | CHARSET_DECOMPOSED,
    NULL,
    NULL,
    NULL
};

/*!
 * @file
 * @sa The Unicode Standard Version 6.2 – Core Specification
 * http://www.unicode.org/versions/Unicode6.2.0/ch03.pdf
 * @code
 * Scalar Value               First    Second   Third    Fourth
 * 00000000 0xxxxxxx          0xxxxxxx
 * 00000yyy yyxxxxxx          110yyyyy 10xxxxxx
 * zzzzyyyy yyxxxxxx          1110zzzz 10yyyyyy 10xxxxxx
 * 000uuuuu zzzzyyyy yyxxxxxx 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx
 * @endcode
 */


/*!
 * @brief Convert from UTF-8 to UTF-16
 * @code
 * Code Points         First   Second  Third   Fourth
 * U+0000..U+007F      00..7F
 * U+0080..U+07FF      C2..DF  80..BF
 * U+0800..U+0FFF      E0      A0..BF  80..BF
 * U+1000..U+CFFF      E1..EC  80..BF  80..BF
 * U+D000..U+D7FF      ED      80..9F  80..BF
 * U+E000..U+FFFF      EE..EF  80..BF  80..BF
 * U+10000..U+3FFFF    F0      90..BF  80..BF  80..BF
 * U+40000..U+FFFFF    F1..F3  80..BF  80..BF  80..BF
 * U+100000..U+10FFFF  F4      80..8F  80..BF  80..BF
 * @endcode
 */
static size_t utf8_pull(void *cd _U_, char **inbuf, size_t *inbytesleft,
                        char **outbuf, size_t *outbytesleft)
{
    ucs2_t uc = 0;
    unsigned int codepoint;
    int len;

    while (*inbytesleft >= 1 && *outbytesleft >= 2) {
        unsigned char *c = (unsigned char *)*inbuf;
        len = 1;

        /* Arrange conditionals in the order of most frequent occurrence
         * for users of Latin-based chars */
        if ((c[0] & 0x80) == 0) {
            /* 0xxx xxxx */ /* 1 byte  */
            uc = c[0];
        } else if ((c[0] & 0xe0) == 0xc0) {
            /* 110y yyyy */ /* 2 bytes */
            if (*inbytesleft < 2) {
                goto inval;
            }

            if (c[0] < 0xc2) {
                goto ilseq;    /* C2-DF */
            }

            if ((c[1] & 0xc0) != 0x80) {
                goto ilseq;    /* 80-BF */
            }

            uc = (ucs2_t)(((c[0] & 0x1f) << 6) | GETUCVAL(c[1], 0)) ;
            len = 2;
        } else if ((c[0] & 0xf0) == 0xe0) {
            /* 1110 zzzz */ /* 3 bytes */
            if (*inbytesleft < 3) {
                goto inval;
            }

            if (!((c[0] == 0xe0                && (c[1] & 0xe0) == 0xa0)
                    || /* E0    A0-BF*/
                    (0xe1 <= c[0] && c[0] <= 0xec && (c[1] & 0xc0) == 0x80) || /* E1-EC 80-BF */
                    (c[0] == 0xed                 && (c[1] & 0xe0) == 0x80) || /* ED    80-9F */
                    ((c[0] & 0xfe) == 0xee        && (c[1] & 0xc0) == 0x80))) { /* EE-EF 80-BF */
                goto ilseq;
            }

            if ((c[2] & 0xc0) != 0x80) {
                goto ilseq;    /* 80-BF */
            }

            uc = (ucs2_t)(((c[0] & 0x0f) << 12) | GETUCVAL(c[1], 6) | GETUCVAL(c[2], 0)) ;
            len = 3;
        } else if ((c[0] & 0xf8) == 0xf0) {
            /* 1111 0uuu */ /* 4 bytes */
            if (*inbytesleft < 4) {
                goto inval;
            }

            if (*outbytesleft < 4) {
                goto toobig;
            }

            if (c[0] > 0xf4) {
                goto ilseq;    /* happens for surrogate pairs only */
            }

            if (!((c[0] == 0xf0                 && 0x90 <= c[1] && c[1] <= 0xbf)
                    || /* F0    90-BF */
                    (0xf1 <= c[0] && c[0] <= 0xf3 && (c[1] & 0xc0) == 0x80)
                    ||        /* F1-F3 80-BF */
                    (c[0] == 0xf4
                     && (c[1] & 0xc0) == 0x80))) {       /* F4    80-8F */
                goto ilseq;
            }

            if ((c[2] & 0xc0) != 0x80) {
                goto ilseq;    /* 80-BF */
            }

            if ((c[3] & 0xc0) != 0x80) {
                goto ilseq;    /* 80-BF */
            }

            codepoint = ((c[0] & 0x07) << 18) | GETUCVAL(c[1], 12) |
                        GETUCVAL(c[2], 6) |  GETUCVAL(c[3], 0);
            SSVAL(*outbuf, 0, (((codepoint - 0x10000) >> 10) + 0xD800)); /* hi  */
            SSVAL(*outbuf, 2, (0xDC00 + (codepoint & 0x03FF)));        /* low */
            len = 4;
            (*inbuf)  += 4;
            (*inbytesleft)  -= 4;
            (*outbytesleft) -= 4;
            (*outbuf) += 4;
            continue;
        } else {
            goto ilseq;
        }

        SSVAL(*outbuf, 0, uc);
        (*inbuf)  += len;
        (*inbytesleft)  -= len;
        (*outbytesleft) -= 2;
        (*outbuf) += 2;
    }

    if (*inbytesleft > 0) {
        goto toobig;
    }

    return 0;
toobig:
    LOG(log_debug, logtype_default, "short ucs-2 write");
    errno = E2BIG;
    return -1;
ilseq:
    LOG(log_debug, logtype_default, "malformed utf8 sequence");
    errno = EILSEQ;
    return -1;
inval:
    LOG(log_debug, logtype_default, "short utf8 char");
    errno = EINVAL;
    return -1;
}

/*! Convert from UTF-16 to UTF-8 */
static size_t utf8_push(void *cd _U_, char **inbuf, size_t *inbytesleft,
                        char **outbuf, size_t *outbytesleft)
{
    ucs2_t uc = 0;
    ucs2_t hi, low;
    unsigned int codepoint;
    int olen, ilen;

    while (*inbytesleft >= 2 && *outbytesleft >= 1) {
        unsigned char *c = (unsigned char *)*outbuf;
        uc = SVAL((*inbuf), 0);
        olen = 1;
        ilen = 2;

        /* Arrange conditionals in the order of most frequent occurrence for
           users of Latin-based chars */
        if (uc < 0x80) {
            c[0] = uc;
        } else if (uc < 0x800) {
            if (*outbytesleft < 2) {
                LOG(log_debug, logtype_default, "short utf8 write");
                goto toobig;
            }

            c[1] = GETUTF8TRAILBYTE(uc, 0);
            c[0] = (char)(0xc0 | ((uc >> 6) & 0x1f));
            olen = 2;
        } else if (uc >= 0x202a && uc <= 0x202e) {
            /* ignore bidi hint characters */
            olen = 0;
        }
        /*
         * A 2-byte uc value represents a stand-alone Unicode character if
         *     0 <= uc < 0xd800 or 0xdfff < uc <= 0xffff.
         * If  0xd800 <= uc <= 0xdfff, uc itself does not represent a Unicode character.
         * Rather, it is just part of a surrogate pair.  A surrogate pair consists of
         * a high surrogate in the range [0xd800 ... 0xdbff] and a low surrogate in the
         * range [0xdc00 ... 0xdfff].  Together the pair maps to a single Unicode character
         * whose scalar value is 64K or larger.  It is this scalar value that is transformed
         * to UTF-8, not the individual surrogates.
         *
         * See www.unicode.org/faq/utf_bom.html for more info.
         */
        else if (0xd800 <= uc && uc <= 0xdfff) {
            /* surrogate - needs 4 bytes from input and 4 bytes for output to UTF-8 */
            if (*outbytesleft < 4) {
                LOG(log_debug, logtype_default, "short utf8 write");
                goto toobig;
            }

            if (*inbytesleft < 4) {
                errno = EINVAL;
                return -1;
            }

            hi =  SVAL((*inbuf), 0);
            low = SVAL((*inbuf), 2);

            if (0xd800 <= hi && hi <= 0xdbff && 0xdc00 <= low && low <= 0xdfff) {
                codepoint = ((hi - 0xd800) << 10) + (low - 0xdc00) + 0x10000;
                c[3] = GETUTF8TRAILBYTE(codepoint, 0);
                c[2] = GETUTF8TRAILBYTE(codepoint, 6);
                c[1] = GETUTF8TRAILBYTE(codepoint, 12);
                c[0] = (char)(0xf0 | ((codepoint >> 18) & 0x07));
                ilen = olen = 4;
            } else { /* invalid values for surrogate */
                errno = EINVAL;
                return -1;
            }
        } else {
            if (*outbytesleft < 3) {
                LOG(log_debug, logtype_default, "short utf8 write");
                goto toobig;
            }

            c[2] = GETUTF8TRAILBYTE(uc, 0);
            c[1] = GETUTF8TRAILBYTE(uc, 6);
            c[0] = (char)(0xe0 | ((uc >> 12) & 0x0f));
            olen = 3;
        }

        (*inbytesleft)  -= ilen;
        (*outbytesleft) -= olen;
        (*inbuf)  += ilen;
        (*outbuf) += olen;
    }

    if (*inbytesleft == 1) {
        errno = EINVAL;
        return -1;
    }

    if (*inbytesleft > 1) {
        errno = E2BIG;
        return -1;
    }

    return 0;
toobig:
    errno = E2BIG;
    return -1;
}
