#include <node.h>
#include <v8.h>
#include <string>
#include <errno.h>
#include <string.h> //for strerror
#include <nan.h>
extern "C" {
#include <maxminddb.h>
}

using namespace v8;
MMDB_s mmdb;
static bool loaded = false;

#define MY_THROW_EXCEP(msg) {                                \
    Nan::ThrowError((std::string("mmgeodb: ")+msg).c_str()); \
    info.GetReturnValue().SetUndefined();                    \
    return;                                                  \
}

static inline const char* nonNull(const char* msg)
{
    if (!msg)
        return "(Unknown error)";
    else
        return msg;
}
enum { MMGEODB_REQUIRE_ALL = 1 };

NAN_METHOD(loadDb)
{
//    NanScope();
    if ((info.Length() < 1) || !info[0]->IsString())
        MY_THROW_EXCEP("No filename specified");
    String::Utf8Value fname(info[0]->ToString());
    int status = MMDB_open(*fname, MMDB_MODE_MMAP, &mmdb);

    if (status != MMDB_SUCCESS)
    {
        if (status == MMDB_IO_ERROR)
            MY_THROW_EXCEP((std::string("I/O error opeining file '")+*fname+"': "+
                            nonNull(strerror(errno))).c_str())
    else
            MY_THROW_EXCEP((std::string("Can't open database file '")+*fname+"': "+
                            nonNull(MMDB_strerror(status))).c_str());
    }
    loaded = true;
}
void lookupIp(const Nan::FunctionCallbackInfo<v8::Value>& info)
{
    if (!loaded)
        MY_THROW_EXCEP("No GeoIP database has been loaded");
    if ((info.Length() < 1) || !info[0]->IsString())
        MY_THROW_EXCEP("No ip address specified");
    String::Utf8Value ip(info[0]->ToString());
    if (!*ip)
        MY_THROW_EXCEP("Empty ip address string provided");
    int flags = 0;
    if (info.Length() > 1)
    {
        if (!info[1]->IsInt32())
            MY_THROW_EXCEP("Argument 2 must be an integer");
        flags = info[1]->Int32Value();
    }
    int gai_error, mmdb_error;
    MMDB_lookup_result_s result = MMDB_lookup_string(&mmdb, *ip, &gai_error, &mmdb_error);

    if (gai_error)
        MY_THROW_EXCEP((std::string("Error from getaddrinfo for ")+*ip+
                        nonNull(gai_strerror(gai_error))));

    if (mmdb_error != MMDB_SUCCESS)
        MY_THROW_EXCEP((std::string("Error looking up ip: ")+nonNull(MMDB_strerror(mmdb_error))));

    if (!result.found_entry)
    {
        info.GetReturnValue().SetNull();
        return;
    }

    MMDB_entry_data_s ccode;
    int status = MMDB_get_value(&result.entry, &ccode, "country", "iso_code", NULL);
    if (status != MMDB_SUCCESS)
        MY_THROW_EXCEP((std::string("Error getting country code:")+nonNull(MMDB_strerror(status))));

    if (!ccode.has_data)
        info.GetReturnValue().SetNull();
    if (ccode.type != MMDB_DATA_TYPE_UTF8_STRING)
       MY_THROW_EXCEP("Unexpected data type of result for country code");

    Local<Object> ret = Nan::New<Object>();
    ret->Set(Nan::New<String>("ccode").ToLocalChecked(),
             Nan::New<String>(ccode.utf8_string, ccode.data_size).ToLocalChecked());

    MMDB_entry_data_s continent;
    status = MMDB_get_value(&result.entry, &continent, "continent", "code", NULL);
    //in some records it is missing
    if (status != MMDB_SUCCESS)
    {
        if (flags & MMGEODB_REQUIRE_ALL)
            MY_THROW_EXCEP((std::string("Error getting continent: ")+nonNull(MMDB_strerror(status))));
    }
    else if (!continent.has_data)
    {
        if (flags & MMGEODB_REQUIRE_ALL)
            MY_THROW_EXCEP("Empty continent in db record");
    }
    else if (continent.type != MMDB_DATA_TYPE_UTF8_STRING)
    {
        MY_THROW_EXCEP("Unexpected data type of result for continent");
    }
    else
    {
        ret->Set(Nan::New<String>("continent").ToLocalChecked(),
                 Nan::New<String>(continent.utf8_string, continent.data_size).ToLocalChecked());
    }
    info.GetReturnValue().Set(ret);
}

NAN_METHOD(unload)
{
    if (!loaded)
        MY_THROW_EXCEP("No database is loaded");
    MMDB_close(&mmdb);
    loaded = false;
    info.GetReturnValue().SetUndefined();
}

void init(Handle<Object> exports)
{
  exports->Set(Nan::New<String>("load").ToLocalChecked(), Nan::New<FunctionTemplate>(loadDb)->GetFunction());
  exports->Set(Nan::New<String>("lookupIp").ToLocalChecked(), Nan::New<FunctionTemplate>(lookupIp)->GetFunction());
  exports->Set(Nan::New<String>("unload").ToLocalChecked(), Nan::New<FunctionTemplate>(unload)->GetFunction());
  exports->Set(Nan::New<String>("REQUIRE_ALL").ToLocalChecked(), Nan::New<Number>((int)MMGEODB_REQUIRE_ALL));
}

NODE_MODULE(mmgeodb, init)
