#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#define NEED_sv_2pv_flags_GLOBAL
#include "ppport.h"
#include <math.h>
#include "xshelper.h"

static char PIECES[32] = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm',
    'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
};

STATIC_INLINE
void
encode(char *buf, STRLEN precision, NV lat, NV lon) {
    IV which = 0;
    STRLEN count = 0;
    NV 
        lat_min = -90,
        lat_max = 90,
        lon_min = -180,
        lon_max = 180
    ;

    while ( count < precision ) {
        IV i;
        IV bits = 0;
        for( i = 0; i < 5; i++ ) {
            IV bit;
            if (which) {
                NV mid = (lat_max + lat_min) / 2;
                bit = lat >= mid ? 1 : 0;
                if ( bit ) { lat_min = mid; }
                else       { lat_max = mid; }
            } else {
                NV mid = (lon_max + lon_min) / 2;
                bit = lon >= mid ? 1 : 0;
                if ( bit ) { lon_min = mid; }
                else       { lon_max = mid; }
            }
            bits = ( ( bits << 1 ) | bit );
            which ^= 1;
        }

        buf[count] = PIECES[bits];
        count++;
    }


    buf[count] = '\0';
}

STATIC_INLINE
void
decode_to_interval(char *hash, STRLEN len, NV *lat_min_out, NV *lat_max_out, NV *lon_min_out, NV *lon_max_out) {
    STRLEN i, j;
    IV which = 0, min_or_max;
    NV 
        lat_min = -90,
        lat_max = 90,
        lon_min = -180,
        lon_max = 180
    ;
    for (i = 0; i < len; i++ ) {
        IV bits;
        int x = (int) hash[i];
        if (x >= 48 && x <= 57) {
            bits = x - 48;
        } else if ( x >= 98 && x <= 104 ) {
            bits = x - 88;
        } else if ( x >= 106 && x <= 107 ) {
            bits = x - 89;
        } else if ( x >= 109 && x <= 110 ) {
            bits = x - 90;
        } else if ( x >= 112 && x <= 122 ) {
            bits = x - 91;
        } else {
            croak("Bad character '%c' in hash '%s'", hash[i], hash);
        }

        for (j = 0; j < 5; j++){ 
            min_or_max = ( bits & 16 ) >> 4;
            if (which) {
                NV mid = (lat_max + lat_min ) / 2;
                if (min_or_max) { /* max */
                    lat_min = mid;
                } else {
                    lat_max = mid;
                }
            } else {
                NV mid = (lon_max + lon_min ) / 2;
                if (min_or_max) { /* max */
                    lon_min = mid;
                } else {
                    lon_max = mid;
                }
            }

            which ^= 1;
            bits <<= 1;
        }
    }

    *lat_min_out = lat_min;
    *lat_max_out = lat_max;
    *lon_min_out = lon_min;
    *lon_max_out = lon_max;
}

STATIC_INLINE
void
decode(char *hash, IV len, NV *lat, NV *lon) {
    NV lat_min = 0, lat_max = 0, lon_min = 0, lon_max = 0;
    decode_to_interval(hash, len, &lat_min, &lat_max, &lon_min, &lon_max);
    *lat = (lat_max + lat_min) / 2;
    *lon = (lon_max + lon_min) / 2;
}

static char* NEIGHBORS[4][2] = {
    { "bc01fg45238967deuvhjyznpkmstqrwx", "p0r21436x8zb9dcf5h7kjnmqesgutwvy" },
    { "238967debc01fg45kmstqrwxuvhjyznp", "14365h7k9dcfesgujnmqp0r2twvyx8zb" },
    { "p0r21436x8zb9dcf5h7kjnmqesgutwvy", "bc01fg45238967deuvhjyznpkmstqrwx" },
    { "14365h7k9dcfesgujnmqp0r2twvyx8zb", "238967debc01fg45kmstqrwxuvhjyznp" }
};

static char* BORDERS[4][2] = {
    { "bcfguvyz", "prxz" },
    { "0145hjnp", "028b" },
    { "prxz", "bcfguvyz" },
    { "028b", "0145hjnp" }
};

#define LOG2_OF_10 3.32192809488736
#define LOG2_OF_180 7.49185309632967
#define LOG2_OF_360 8.49185309632968

STATIC_INLINE
IV
bits_for_number(char *number) {
    for(; *number != '\0'; number++){
        if(*number == '.'){
            number++; /* skip dot */
            return (IV) (strlen(number)) * LOG2_OF_10 + 1;
        }
    }
    return 0;
}

STATIC_INLINE
IV
precision(SV *lat, SV *lon) {
    IV lab = bits_for_number(SvPV_nolen(lat)) + 8;  /* 8 > log_2(180) */
    IV lob = bits_for_number(SvPV_nolen(lon)) + 9;  /* 9 > log_2(360) */

    /* Though it seems I should use ceil(), I copied the logic from Geo::Hash */
    return (IV) ( ( ( lab > lob ? lab : lob ) + 1 ) / 2.5 );
}

enum GH_DIRECTION {
    ADJ_RIGHT = 0,
    ADJ_LEFT = 1,
    ADJ_TOP = 2,
    ADJ_BOTTOM = 3
};

/* need to free this return value! */
#define HASHBASE_BUFSIZ 8192
STATIC_INLINE
char *
adjacent(char *hash, STRLEN hashlen, enum GH_DIRECTION direction) {
    char base[HASHBASE_BUFSIZ];
    char last_ch = hash[ hashlen - 1 ];
    char *pos, *ret;
    IV type = hashlen % 2;
    IV base_len;

    if (hashlen < 1)
        croak("PANIC: hash too short!");
    if (hashlen > HASHBASE_BUFSIZ)
        croak("PANIC: hash too big!");

    memcpy(base, hash, hashlen - 1);
    *(base + hashlen - 1) = '\0';

    if (hashlen > 1) {
        pos = strchr(BORDERS[direction][type], last_ch);
        if (pos != NULL) {
           char *tmp = adjacent(base, strlen(base), direction);
           strcpy(base, tmp);
           Safefree(tmp);
        }
    }
    base_len = strlen(base);
    Newxz( ret, base_len + 2, char );
    strcpy( ret, base );
    ret[ base_len ] = PIECES[ strchr(NEIGHBORS[direction][type], last_ch) - NEIGHBORS[direction][type] ]; 
    *(ret + base_len + 1) = '\0';
    return ret;
}

STATIC_INLINE
void
neighbors(char *hash, STRLEN hashlen, int around, int offset, char ***neighbors, int *nsize) {
    char *xhash;
    STRLEN xhashlen = hashlen;
    int i = 1;

    Newxz( xhash, hashlen + 1, char );
    Copy( hash, xhash, hashlen, char );
    *(xhash + hashlen) = '\0';

    *nsize = ( (around + offset) * 2 + 1 ) * ( (around + offset) * 2 + 1 )
             - (offset * 2 + 1) * (offset * 2 + 1);
    Newxz( *neighbors, *nsize, char *);

    while ( offset > 0 ) {
        char *top = adjacent( xhash, xhashlen, ADJ_TOP );
        char *left = adjacent( top, strlen(top), ADJ_LEFT );
        Safefree(top);
        Safefree(xhash);
        xhash = left;
        xhashlen = strlen(left);

        offset--;
        i++;
    }

    {
    int m = 0;
    char *tmp = xhash;
    while (around-- > 0) {
        int j;
        /* going to insert this many neighbors */
        (*neighbors)[m++] = adjacent(xhash, xhashlen, ADJ_TOP);

        for ( j = 0; j < 2 * i - 1; j ++ ) {
            xhash = (*neighbors)[m - 1];
            xhashlen = strlen( xhash );
            (*neighbors)[m++] = adjacent(xhash, xhashlen, ADJ_RIGHT);
        }
        for ( j = 0; j < 2 * i; j ++ ) {
            xhash = (*neighbors)[m - 1];
            xhashlen = strlen( xhash );
            (*neighbors)[m++] = adjacent(xhash, xhashlen, ADJ_BOTTOM);
        }
        for ( j = 0; j < 2 * i; j ++ ) {
            xhash = (*neighbors)[m - 1];
            xhashlen = strlen( xhash );
            (*neighbors)[m++] = adjacent(xhash, xhashlen, ADJ_LEFT);
        }
        for ( j = 0; j < 2 * i; j ++ ) {
            xhash = (*neighbors)[m - 1];
            xhashlen = strlen( xhash );
            (*neighbors)[m++] = adjacent(xhash, xhashlen, ADJ_TOP);
        }
        i++;
        xhash = (*neighbors)[m - 1];
        xhashlen = strlen( xhash );
    }
        Safefree(tmp);
    }
}

MODULE = Geo::Hash::XS PACKAGE = Geo::Hash::XS

PROTOTYPES: DISABLE

SV *
encode(self, lat, lon, p = 0)
        SV *self;
        SV *lat;
        SV *lon;
        IV p;
    PREINIT:
        char *encoded;
    CODE:
        if (! looks_like_number(lat) || ! looks_like_number(lon) ) {
            croak("encode() only works on degrees, not dms values");
        }  

        if (p <= 0) {
            p = precision(lat, lon);
        }
        PERL_UNUSED_VAR(self);

        Newxz(encoded, p + 1, char);
        encode(encoded, p, SvNV(lat), SvNV(lon));

        RETVAL = newSV(0);
        sv_setpv( RETVAL, encoded );
        Safefree( encoded );
    OUTPUT:
        RETVAL

void
decode_to_interval(self, hash)
        SV *self;
        char *hash;
    PREINIT:
        NV lat_min = 0, lat_max = 0, lon_min = 0, lon_max = 0;
        STRLEN len = strlen(hash);
        AV *lat_range = (AV *)sv_2mortal((SV *)newAV());
        AV *lon_range = (AV *)sv_2mortal((SV *)newAV());
    PPCODE:
        PERL_UNUSED_VAR(self);
        decode_to_interval(hash, len, &lat_min, &lat_max, &lon_min, &lon_max);

        av_push(lat_range, newSVnv(lat_max));
        av_push(lat_range, newSVnv(lat_min));
        av_push(lon_range, newSVnv(lon_max));
        av_push(lon_range, newSVnv(lon_min));

        XPUSHs(sv_2mortal(newRV_inc((SV *)lat_range)));
        XPUSHs(sv_2mortal(newRV_inc((SV *)lon_range)));

void
decode(self, hash)
        SV *self;
        char *hash;
    PREINIT:
        NV lat = 0, lon = 0;
        STRLEN len = strlen(hash);
    PPCODE:
        PERL_UNUSED_VAR(self);
        decode(hash, len, &lat, &lon);
        mXPUSHn(lat);
        mXPUSHn(lon);

IV
precision(self, lat, lon)
        SV *self
        SV *lat
        SV *lon;
    CODE:
        PERL_UNUSED_VAR(self);
        RETVAL = precision(lat, lon);
    OUTPUT:
        RETVAL

SV *
adjacent(self, hash, direction)
        SV *self;
        char *hash;
        int direction;
    PREINIT:
        char *adj;
    CODE:
        PERL_UNUSED_VAR(self);
        adj = adjacent(hash, strlen(hash), direction);

        RETVAL = newSV(0);
        sv_setpv(RETVAL, adj);
        Safefree(adj);
    OUTPUT:
        RETVAL

void
neighbors(self, hash, around = 1, offset = 0)
        SV *self;
        char *hash;
        int around;
        int offset;
    PREINIT:
        int i;
        int nsize;
        char **list;
    PPCODE:
        PERL_UNUSED_VAR(self);
        neighbors(hash, strlen(hash), around, offset, &list, &nsize);

        for( i = 0; i < nsize; i++ ) {
            mXPUSHp( list[i], strlen(list[i]) );
        }
        for( i = 0; i < nsize; i++ ) {
            Safefree(list[i]);
        }
        Safefree(list);

IV
_constant()
    ALIAS:
        ADJ_TOP = ADJ_TOP
        ADJ_RIGHT = ADJ_RIGHT
        ADJ_LEFT = ADJ_LEFT
        ADJ_BOTTOM = ADJ_BOTTOM
    CODE:
        RETVAL = ix;
    OUTPUT:
        RETVAL