/*
 *  call-seq:
 *     arr.pack ( aTemplateString ) -> aBinaryString
 *  
 *  Packs the contents of <i>arr</i> into a binary sequence according to
 *  the directives in <i>aTemplateString</i> (see the table below)
 *  Directives ``A,'' ``a,'' and ``Z'' may be followed by a count,
 *  which gives the width of the resulting field. The remaining
 *  directives also may take a count, indicating the number of array
 *  elements to convert. If the count is an asterisk
 *  (``<code>*</code>''), all remaining array elements will be
 *  converted. Any of the directives ``<code>sSiIlL</code>'' may be
 *  followed by an underscore (``<code>_</code>'') to use the underlying
 *  platform's native size for the specified type; otherwise, they use a
 *  platform-independent size. Spaces are ignored in the template
 *  string. See also <code>String#unpack</code>.
 *     
 *     a = [ "a", "b", "c" ]
 *     n = [ 65, 66, 67 ]
 *     a.pack("A3A3A3")   #=> "a  b  c  "
 *     a.pack("a3a3a3")   #=> "a\000\000b\000\000c\000\000"
 *     n.pack("ccc")      #=> "ABC"
 *     
 *  Directives for +pack+.
 *
 *   Directive    Meaning
 *   ---------------------------------------------------------------
 *       @     |  Moves to absolute position
 *       A     |  ASCII string (space padded, count is width)
 *       a     |  ASCII string (null padded, count is width)
 *       B     |  Bit string (descending bit order)
 *       b     |  Bit string (ascending bit order)
 *       C     |  Unsigned char
 *       c     |  Char
 *       D, d  |  Double-precision float, native format
 *       E     |  Double-precision float, little-endian byte order
 *       e     |  Single-precision float, little-endian byte order
 *       F, f  |  Single-precision float, native format
 *       G     |  Double-precision float, network (big-endian) byte order
 *       g     |  Single-precision float, network (big-endian) byte order
 *       H     |  Hex string (high nibble first)
 *       h     |  Hex string (low nibble first)
 *       I     |  Unsigned integer
 *       i     |  Integer
 *       L     |  Unsigned long
 *       l     |  Long
 *       M     |  Quoted printable, MIME encoding (see RFC2045)
 *       m     |  Base64 encoded string
 *       N     |  Long, network (big-endian) byte order
 *       n     |  Short, network (big-endian) byte-order
 *       P     |  Pointer to a structure (fixed-length string)
 *       p     |  Pointer to a null-terminated string
 *       Q, q  |  64-bit number
 *       S     |  Unsigned short
 *       s     |  Short
 *       U     |  UTF-8
 *       u     |  UU-encoded string
 *       V     |  Long, little-endian byte order
 *       v     |  Short, little-endian byte order
 *       w     |  BER-compressed integer\fnm
 *       X     |  Back up a byte
 *       x     |  Null byte
 *       Z     |  Same as ``a'', except that null is added with *
 */

static VALUE
pack_pack(ary, fmt)
    VALUE ary, fmt;
{
    static char *nul10 = "\0\0\0\0\0\0\0\0\0\0";
    static char *spc10 = "          ";
    char *p, *pend;
    VALUE res, from, associates = 0;
    char type;
    long items, len, idx, plen;
    char *ptr;
#ifdef NATINT_PACK
    int natint;         /* native integer */
#endif

    StringValue(fmt);
    p = RSTRING(fmt)->ptr;
    pend = p + RSTRING(fmt)->len;
    res = rb_str_buf_new(0);

    items = RARRAY(ary)->len;
    idx = 0;

#define THISFROM RARRAY(ary)->ptr[idx]
#define NEXTFROM (items-- > 0 ? RARRAY(ary)->ptr[idx++] : (rb_raise(rb_eArgError, toofew),0))

    while (p < pend) {
        if (RSTRING(fmt)->ptr + RSTRING(fmt)->len != pend) {
            rb_raise(rb_eRuntimeError, "format string modified");
        }
        type = *p++;           /* get data type */
#ifdef NATINT_PACK
        natint = 0;
#endif

        if (ISSPACE(type)) continue;
        if (type == '#') {
            while ((p < pend) && (*p != '\n')) {
                p++;
            }
            continue;
        }
        if (*p == '_' || *p == '!') {
            const char *natstr = "sSiIlL";

            if (strchr(natstr, type)) {
#ifdef NATINT_PACK
                natint = 1;
#endif
                p++;
            }
            else {
                rb_raise(rb_eArgError, "'%c' allowed only after types %s", *p, natstr);
            }
        }
        if (*p == '*') {       /* set data length */
            len = strchr("@Xxu", type) ? 0 : items;
            p++;
        }
        else if (ISDIGIT(*p)) {
            len = strtoul(p, (char**)&p, 10);
        }
        else {
            len = 1;
        }

        switch (type) {
          case 'A': case 'a': case 'Z':
          case 'B': case 'b':
          case 'H': case 'h':
            from = NEXTFROM;
            if (NIL_P(from)) {
                ptr = "";
                plen = 0;
            }
            else {
                StringValue(from);
                ptr = RSTRING(from)->ptr;
                plen = RSTRING(from)->len;
                OBJ_INFECT(res, from);
            }

            if (p[-1] == '*')
                len = plen;

            switch (type) {
              case 'a':                /* arbitrary binary string (null padded)  */
              case 'A':                /* ASCII string (space padded) */
              case 'Z':                /* null terminated ASCII string  */
                if (plen >= len) {
                    rb_str_buf_cat(res, ptr, len);
                    if (p[-1] == '*' && type == 'Z')
                        rb_str_buf_cat(res, nul10, 1);
                }
                else {
                    rb_str_buf_cat(res, ptr, plen);
                    len -= plen;
                    while (len >= 10) {
                        rb_str_buf_cat(res, (type == 'A')?spc10:nul10, 10);
                        len -= 10;
                    }
                    rb_str_buf_cat(res, (type == 'A')?spc10:nul10, len);
                }
                break;

              case 'b':                /* bit string (ascending) */
                {
                    int byte = 0;
                    long i, j = 0;

                    if (len > plen) {
                        j = (len - plen + 1)/2;
                        len = plen;
                    }
                    for (i=0; i++ < len; ptr++) {
                        if (*ptr & 1)
                            byte |= 128;
                        if (i & 7)
                            byte >>= 1;
                        else {
                            char c = byte & 0xff;
                            rb_str_buf_cat(res, &c, 1);
                            byte = 0;
                        }
                    }
                    if (len & 7) {
                        char c;
                        byte >>= 7 - (len & 7);
                        c = byte & 0xff;
                        rb_str_buf_cat(res, &c, 1);
                    }
                    len = j;
                    goto grow;
                }
                break;

              case 'B':                /* bit string (descending) */
                {
                    int byte = 0;
                    long i, j = 0;

                    if (len > plen) {
                        j = (len - plen + 1)/2;
                        len = plen;
                    }
                    for (i=0; i++ < len; ptr++) {
                        byte |= *ptr & 1;
                        if (i & 7)
                            byte <<= 1;
                        else {
                            char c = byte & 0xff;
                            rb_str_buf_cat(res, &c, 1);
                            byte = 0;
                        }
                    }
                    if (len & 7) {
                        char c;
                        byte <<= 7 - (len & 7);
                        c = byte & 0xff;
                        rb_str_buf_cat(res, &c, 1);
                    }
                    len = j;
                    goto grow;
                }
                break;

              case 'h':                /* hex string (low nibble first) */
                {
                    int byte = 0;
                    long i, j = 0;

                    if (len > plen) {
                        j = (len - plen + 1)/2;
                        len = plen;
                    }
                    for (i=0; i++ < len; ptr++) {
                        if (ISALPHA(*ptr))
                            byte |= (((*ptr & 15) + 9) & 15) << 4;
                        else
                            byte |= (*ptr & 15) << 4;
                        if (i & 1)
                            byte >>= 4;
                        else {
                            char c = byte & 0xff;
                            rb_str_buf_cat(res, &c, 1);
                            byte = 0;
                        }
                    }
                    if (len & 1) {
                        char c = byte & 0xff;
                        rb_str_buf_cat(res, &c, 1);
                    }
                    len = j;
                    goto grow;
                }
                break;

              case 'H':                /* hex string (high nibble first) */
                {
                    int byte = 0;
                    long i, j = 0;

                    if (len > plen) {
                        j = (len - plen + 1)/2;
                        len = plen;
                    }
                    for (i=0; i++ < len; ptr++) {
                        if (ISALPHA(*ptr))
                            byte |= ((*ptr & 15) + 9) & 15;
                        else
                            byte |= *ptr & 15;
                        if (i & 1)
                            byte <<= 4;
                        else {
                            char c = byte & 0xff;
                            rb_str_buf_cat(res, &c, 1);
                            byte = 0;
                        }
                    }
                    if (len & 1) {
                        char c = byte & 0xff;
                        rb_str_buf_cat(res, &c, 1);
                    }
                    len = j;
                    goto grow;
                }
                break;
            }
            break;

          case 'c':            /* signed char */
          case 'C':            /* unsigned char */
            while (len-- > 0) {
                char c;

                from = NEXTFROM;
                c = num2i32(from);
                rb_str_buf_cat(res, &c, sizeof(char));
            }
            break;

          case 's':            /* signed short */
          case 'S':            /* unsigned short */
            while (len-- > 0) {
                short s;

                from = NEXTFROM;
                s = num2i32(from);
                rb_str_buf_cat(res, OFF16(&s), NATINT_LEN(short,2));
            }
            break;

          case 'i':            /* signed int */
          case 'I':            /* unsigned int */
            while (len-- > 0) {
                long i;

                from = NEXTFROM;
                i = num2i32(from);
                rb_str_buf_cat(res, OFF32(&i), NATINT_LEN(int,4));
            }
            break;

          case 'l':            /* signed long */
          case 'L':            /* unsigned long */
            while (len-- > 0) {
                long l;

                from = NEXTFROM;
                l = num2i32(from);
                rb_str_buf_cat(res, OFF32(&l), NATINT_LEN(long,4));
            }
            break;

          case 'q':            /* signed quad (64bit) int */
          case 'Q':            /* unsigned quad (64bit) int */
            while (len-- > 0) {
                char tmp[QUAD_SIZE];

                from = NEXTFROM;
                rb_quad_pack(tmp, from);
                rb_str_buf_cat(res, (char*)&tmp, QUAD_SIZE);
            }
            break;

          case 'n':            /* unsigned short (network byte-order)  */
            while (len-- > 0) {
                unsigned short s;

                from = NEXTFROM;
                s = num2i32(from);
                s = NATINT_HTONS(s);
                rb_str_buf_cat(res, OFF16(&s), NATINT_LEN(short,2));
            }
            break;

          case 'N':            /* unsigned long (network byte-order) */
            while (len-- > 0) {
                unsigned long l;

                from = NEXTFROM;
                l = num2i32(from);
                l = NATINT_HTONL(l);
                rb_str_buf_cat(res, OFF32(&l), NATINT_LEN(long,4));
            }
            break;

          case 'v':            /* unsigned short (VAX byte-order) */
            while (len-- > 0) {
                unsigned short s;

                from = NEXTFROM;
                s = num2i32(from);
                s = NATINT_HTOVS(s);
                rb_str_buf_cat(res, OFF16(&s), NATINT_LEN(short,2));
            }
            break;

          case 'V':            /* unsigned long (VAX byte-order) */
            while (len-- > 0) {
                unsigned long l;

                from = NEXTFROM;
                l = num2i32(from);
                l = NATINT_HTOVL(l);
                rb_str_buf_cat(res, OFF32(&l), NATINT_LEN(long,4));
            }
            break;

          case 'f':            /* single precision float in native format */
          case 'F':            /* ditto */
            while (len-- > 0) {
                float f;

                from = NEXTFROM;
                f = RFLOAT(rb_Float(from))->value;
                rb_str_buf_cat(res, (char*)&f, sizeof(float));
            }
            break;

          case 'e':            /* single precision float in VAX byte-order */
            while (len-- > 0) {
                float f;
                FLOAT_CONVWITH(ftmp);

                from = NEXTFROM;
                f = RFLOAT(rb_Float(from))->value;
                f = HTOVF(f,ftmp);
                rb_str_buf_cat(res, (char*)&f, sizeof(float));
            }
            break;

          case 'E':            /* double precision float in VAX byte-order */
            while (len-- > 0) {
                double d;
                DOUBLE_CONVWITH(dtmp);

                from = NEXTFROM;
                d = RFLOAT(rb_Float(from))->value;
                d = HTOVD(d,dtmp);
                rb_str_buf_cat(res, (char*)&d, sizeof(double));
            }
            break;

          case 'd':            /* double precision float in native format */
          case 'D':            /* ditto */
            while (len-- > 0) {
                double d;

                from = NEXTFROM;
                d = RFLOAT(rb_Float(from))->value;
                rb_str_buf_cat(res, (char*)&d, sizeof(double));
            }
            break;

          case 'g':            /* single precision float in network byte-order */
            while (len-- > 0) {
                float f;
                FLOAT_CONVWITH(ftmp);

                from = NEXTFROM;
                f = RFLOAT(rb_Float(from))->value;
                f = HTONF(f,ftmp);
                rb_str_buf_cat(res, (char*)&f, sizeof(float));
            }
            break;

          case 'G':            /* double precision float in network byte-order */
            while (len-- > 0) {
                double d;
                DOUBLE_CONVWITH(dtmp);

                from = NEXTFROM;
                d = RFLOAT(rb_Float(from))->value;
                d = HTOND(d,dtmp);
                rb_str_buf_cat(res, (char*)&d, sizeof(double));
            }
            break;

          case 'x':            /* null byte */
          grow:
            while (len >= 10) {
                rb_str_buf_cat(res, nul10, 10);
                len -= 10;
            }
            rb_str_buf_cat(res, nul10, len);
            break;

          case 'X':            /* back up byte */
          shrink:
            plen = RSTRING(res)->len;
            if (plen < len)
                rb_raise(rb_eArgError, "X outside of string");
            RSTRING(res)->len = plen - len;
            RSTRING(res)->ptr[plen - len] = '\0';
            break;

          case '@':            /* null fill to absolute position */
            len -= RSTRING(res)->len;
            if (len > 0) goto grow;
            len = -len;
            if (len > 0) goto shrink;
            break;

          case '%':
            rb_raise(rb_eArgError, "%% is not supported");
            break;

          case 'U':            /* Unicode character */
            while (len-- > 0) {
                long l;
                char buf[8];
                int le;

                from = NEXTFROM;
                from = rb_to_int(from);
                l = NUM2INT(from);
                if (l < 0) {
                    rb_raise(rb_eRangeError, "pack(U): value out of range");
                }
                le = uv_to_utf8(buf, l);
                rb_str_buf_cat(res, (char*)buf, le);
            }
            break;

          case 'u':            /* uuencoded string */
          case 'm':            /* base64 encoded string */
            from = NEXTFROM;
            StringValue(from);
            ptr = RSTRING(from)->ptr;
            plen = RSTRING(from)->len;

            if (len <= 2)
                len = 45;
            else
                len = len / 3 * 3;
            while (plen > 0) {
                long todo;

                if (plen > len)
                    todo = len;
                else
                    todo = plen;
                encodes(res, ptr, todo, type);
                plen -= todo;
                ptr += todo;
            }
            break;

          case 'M':            /* quoted-printable encoded string */
            from = rb_obj_as_string(NEXTFROM);
            if (len <= 1)
                len = 72;
            qpencode(res, from, len);
            break;

          case 'P':            /* pointer to packed byte string */
            from = THISFROM;
            if (!NIL_P(from)) {
                StringValue(from);
                if (RSTRING(from)->len < len) {
                    rb_raise(rb_eArgError, "too short buffer for P(%ld for %ld)",
                             RSTRING(from)->len, len);
                }
            }
            len = 1;
            /* FALL THROUGH */
          case 'p':            /* pointer to string */
            while (len-- > 0) {
                char *t;
                from = NEXTFROM;
                if (NIL_P(from)) {
                    t = 0;
                }
                else {
                    t = StringValuePtr(from);
                }
                if (!associates) {
                    associates = rb_ary_new();
                }
                rb_ary_push(associates, from);
                rb_str_buf_cat(res, (char*)&t, sizeof(char*));
            }
            break;

          case 'w':            /* BER compressed integer  */
            while (len-- > 0) {
                unsigned long ul;
                VALUE buf = rb_str_new(0, 0);
                char c, *bufs, *bufe;

                from = NEXTFROM;
                if (TYPE(from) == T_BIGNUM) {
                    VALUE big128 = rb_uint2big(128);
                    while (TYPE(from) == T_BIGNUM) {
                        from = rb_big_divmod(from, big128);
                        c = NUM2INT(RARRAY(from)->ptr[1]) | 0x80; /* mod */
                        rb_str_buf_cat(buf, &c, sizeof(char));
                        from = RARRAY(from)->ptr[0]; /* div */
                    }
                }

                {
                    long l = NUM2LONG(from);
                    if (l < 0) {
                        rb_raise(rb_eArgError, "can't compress negative numbers");
                    }
                    ul = l;
                }

                while (ul) {
                    c = ((ul & 0x7f) | 0x80);
                    rb_str_buf_cat(buf, &c, sizeof(char));
                    ul >>=  7;
                }

                if (RSTRING(buf)->len) {
                    bufs = RSTRING(buf)->ptr;
                    bufe = bufs + RSTRING(buf)->len - 1;
                    *bufs &= 0x7f; /* clear continue bit */
                    while (bufs < bufe) { /* reverse */
                        c = *bufs;
                        *bufs++ = *bufe;
                        *bufe-- = c;
                    }
                    rb_str_buf_cat(res, RSTRING(buf)->ptr, RSTRING(buf)->len);
                }
                else {
                    c = 0;
                    rb_str_buf_cat(res, &c, sizeof(char));
                }
            }
            break;

          default:
            break;
        }
    }

    if (associates) {
        rb_str_associate(res, associates);
    }
    return res;
}