/*
 *  geohash.c
 *  libgeohash
 *
 *  Created by Derek Smith on 10/6/09.
 *  Copyright (c) 2010, SimpleGeo
 *      All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without 
 *  modification, are permitted provided that the following conditions are met:

 *  Redistributions of source code must retain the above copyright notice, this list
 *  of conditions and the following disclaimer. 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.
 *  Neither the name of the SimpleGeo 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 "geohash.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define MAX_LAT             90.0
#define MIN_LAT             -90.0

#define MAX_LONG            180.0
#define MIN_LONG            -180.0

#define NORTH               0
#define EAST                1
#define SOUTH               2
#define WEST                3

#define LENGTH_OF_DEGREE	111100				// meters

/*
 * From Elasticsearch Guide:
 * The highest-precision geohash of length 12 produces cells
 * that cover less than a square metre of land.
 */
#define MAX_GEOHASH_LENGTH	12

typedef struct IntervalStruct {
    
    double high;
    double low;
    
} Interval;


/* Normal 32 characer map used for geohashing */
static char char_map[32] =  "0123456789bcdefghjkmnpqrstuvwxyz";

/*
 *  The follow character maps were created by Dave Troy and used in his Javascript Geohashing
 *  library. http://github.com/davetroy/geohash-js
 */
static char *even_neighbors[] = {"p0r21436x8zb9dcf5h7kjnmqesgutwvy",
                                "bc01fg45238967deuvhjyznpkmstqrwx", 
                                "14365h7k9dcfesgujnmqp0r2twvyx8zb",
                                "238967debc01fg45kmstqrwxuvhjyznp"
                                };

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

static char *even_borders[] = {"prxz", "bcfguvyz", "028b", "0145hjnp"};
static char *odd_borders[] = {"bcfguvyz", "prxz", "0145hjnp", "028b"};

unsigned int index_for_char(char c, char *string) {
    
    int index = -1;
    int string_amount = strlen(string);
    int i;
    for(i = 0; i < string_amount; i++) {
        
        if(c == string[i]) {
            
            index = i; 
            break;
        }
        
    }
    
    return index;
}

char* get_neighbor(char *hash, int direction) {
    
    int hash_length = strlen(hash);
    
	char last_char = hash[hash_length - 1];
    
    int is_odd = hash_length % 2;
    char **border = is_odd ? odd_borders : even_borders;
    char **neighbor = is_odd ? odd_neighbors : even_neighbors; 
    
    size_t geohash_b_size = MAX_GEOHASH_LENGTH + 1;
    char *base = malloc(sizeof(char) * geohash_b_size);
    snprintf(base, geohash_b_size, "%s", hash);
    
	if(index_for_char(last_char, border[direction]) != -1)
		base = get_neighbor(base, direction);
    
    int neighbor_index = index_for_char(last_char, neighbor[direction]);
    last_char = char_map[neighbor_index];
        
    char *last_hash = malloc(sizeof(char) * 2);
    last_hash[0] = last_char;
    last_hash[1] = '\0';
    strcat(base, last_hash);
    free(last_hash);
    
	return base;
}

char* geohash_encode(double lat, double lng, int precision) {
    
    if(precision < 1 || precision > 12)
        precision = 6;
    
    char* hash = NULL;
    
    if(lat <= 90.0 && lat >= -90.0 && lng <= 180.0 && lng >= -180.0) {
        
        hash = (char*)malloc(sizeof(char) * (precision + 1));
        hash[precision] = '\0';
        
        precision *= 5.0;
        
        Interval lat_interval = {MAX_LAT, MIN_LAT};
        Interval lng_interval = {MAX_LONG, MIN_LONG};

        Interval *interval;
        double coord, mid;
        int is_even = 1;
        unsigned int hashChar = 0;
        int i;
        for(i = 1; i <= precision; i++) {
         
            if(is_even) {
            
                interval = &lng_interval;
                coord = lng;                
                
            } else {
                
                interval = &lat_interval;
                coord = lat;   
            }
            
            mid = (interval->low + interval->high) / 2.0;
            hashChar = hashChar << 1;
            
            if(coord > mid) {
                
                interval->low = mid;
                hashChar |= 0x01;
                
            } else
                interval->high = mid;
            
            if(!(i % 5)) {
                
                hash[(i - 1) / 5] = char_map[hashChar];
                hashChar = 0;

            }
            
            is_even = !is_even;
        }
     
        
    }
    
    return hash;
}

GeoCoord geohash_decode(char *hash) {
    
    GeoCoord coordinate = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
    
    if(hash) {
        
        int char_amount = strlen(hash);
        
        if(char_amount) {
            
            unsigned int char_mapIndex;
            Interval lat_interval = {MAX_LAT, MIN_LAT};
            Interval lng_interval = {MAX_LONG, MIN_LONG};
            Interval *interval;
        
            int is_even = 1;
            double delta;
            int i, j;
            for(i = 0; i < char_amount; i++) {
            
                char_mapIndex = index_for_char(hash[i], (char*)char_map);
                
                //JPM always false if(char_mapIndex < 0)
                //JPM    break;
            
                // Interpret the last 5 bits of the integer
                for(j = 0; j < 5; j++) {
                
                    interval = is_even ? &lng_interval : &lat_interval;
                
                    delta = (interval->high - interval->low) / 2.0;
                
                    if((char_mapIndex << j) & 0x0010)
                        interval->low += delta;
                    else
                        interval->high -= delta;
                
                    is_even = !is_even;
                }
            
            }
            
            coordinate.latitude = lat_interval.high - ((lat_interval.high - lat_interval.low) / 2.0);
            coordinate.longitude = lng_interval.high - ((lng_interval.high - lng_interval.low) / 2.0);
            
            coordinate.north = lat_interval.high;
            coordinate.east = lng_interval.high;
            coordinate.south = lat_interval.low;
            coordinate.west = lng_interval.low;
        }
    }
    
    return coordinate;
}


char** geohash_neighbors(char *hash) {

    char** neighbors = NULL;
    
    if(hash) {
        
        // N, NE, E, SE, S, SW, W, NW
        neighbors = (char**)malloc(sizeof(char*) * 8);
        
        neighbors[0] = get_neighbor(hash, NORTH);
        neighbors[1] = get_neighbor(neighbors[0], EAST);
        neighbors[2] = get_neighbor(hash, EAST);
        neighbors[3] = get_neighbor(neighbors[2], SOUTH);
        neighbors[4] = get_neighbor(hash, SOUTH);
        neighbors[5] = get_neighbor(neighbors[4], WEST);                
        neighbors[6] = get_neighbor(hash, WEST);
        neighbors[7] = get_neighbor(neighbors[6], NORTH);        

    }
    
    return neighbors;
}

GeoBoxDimension geohash_dimensions_for_precision(int precision) {
	
	GeoBoxDimension dimensions = {0.0, 0.0};
	
	if(precision > 0) {
	
		int lat_times_to_cut = precision * 5 / 2;
		int lng_times_to_cut = precision * 5 / 2 + (precision % 2 ? 1 : 0);
	
		double width = 360.0;
		double height = 180.0;
	
		int i;
		for(i = 0; i < lat_times_to_cut; i++)
			height /= 2.0;
		
		for(i = 0; i < lng_times_to_cut; i++)
			width /= 2.0;
		
		dimensions.width = width;
		dimensions.height = height;
		
	}
	
	return dimensions;
}
