#include "lmdb-js.h"
#include <string.h>

using namespace Napi;

const int INCLUDE_VALUES = 0x100;
const int REVERSE = 0x400;
const int VALUES_FOR_KEY = 0x800;
const int ONLY_COUNT = 0x1000;
const int RENEW_CURSOR = 0x2000;
const int EXACT_MATCH = 0x4000;
const int INCLUSIVE_END = 0x8000;
const int EXCLUSIVE_START = 0x10000;

CursorWrap::CursorWrap(const CallbackInfo& info) : Napi::ObjectWrap<CursorWrap>(info) {
	this->keyType = LmdbKeyType::StringKey;
	this->freeKey = nullptr;
	this->endKey.mv_size = 0; // indicates no end key (yet)
	if (info.Length() < 1) {
		throwError(info.Env(), "Wrong number of arguments");
		return;
	}

	DbiWrap *dw;
	napi_unwrap(info.Env(), info[0], (void**)&dw);
	int64_t tw_address = 0;
	napi_get_value_int64(info.Env(), info[1], &tw_address);
	// Open the cursor
	MDB_cursor *cursor;
	MDB_txn *txn = dw->ew->getReadTxn(tw_address);
	int rc = mdb_cursor_open(txn, dw->dbi, &cursor);
	if (rc != 0) {
		throwLmdbError(info.Env(), rc);
		return;
	}
	info.This().As<Object>().Set("address", Number::New(info.Env(), (size_t) this));
	this->cursor = cursor;
	this->dw = dw;
	this->txn = txn;
	this->keyType = keyType;
}

CursorWrap::~CursorWrap() {
	if (this->cursor) {
		// Don't close cursor here, it is possible that the environment may already be closed, which causes it to crash
		//mdb_cursor_close(this->cursor);
	}
	if (this->freeKey) {
		this->freeKey(this->key);
	}
}

Value CursorWrap::close(const CallbackInfo& info) {
	if (!this->cursor) {
	  return throwError(info.Env(), "cursor.close: Attempt to close a closed cursor!");
	}
	mdb_cursor_close(this->cursor);
	this->cursor = nullptr;
	return info.Env().Undefined();
}

Value CursorWrap::del(const CallbackInfo& info) {
	int flags = 0;

	if (info.Length() == 1) {
		if (!info[0].IsObject()) {
			return throwError(info.Env(), "cursor.del: Invalid options argument. It should be an object.");
		}
		
		auto options = info[0].As<Object>();
		setFlagFromValue(&flags, MDB_NODUPDATA, "noDupData", false, options);
	}

	int rc = mdb_cursor_del(this->cursor, flags);
	if (rc != 0) {
		return throwLmdbError(info.Env(), rc);
	}
	return info.Env().Undefined();
}
int CursorWrap::returnEntry(int lastRC, MDB_val &key, MDB_val &data) {
	if (lastRC) {
		if (lastRC == MDB_NOTFOUND)
			return 0;
		else {
			return lastRC > 0 ? -lastRC : lastRC;
		}
	}
	if (endKey.mv_size > 0) {
		int comparison;
		if (flags & VALUES_FOR_KEY)
			comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data);
		else
			comparison = mdb_cmp(txn, dw->dbi, &endKey, &key);
		if ((flags & REVERSE) ? comparison >= 0 : (comparison <= 0)) {
			if (!((flags & INCLUSIVE_END) && comparison == 0))
				return 0;
		}
	}
	char* keyBuffer = dw->ew->keyBuffer;
	if (flags & INCLUDE_VALUES) {
		int result = getVersionAndUncompress(data, dw);
		bool fits = true;
		if (result) {
			fits = valToBinaryFast(data, dw); // it fit in the global/compression-target buffer
		}
		if (fits || result == 2 || data.mv_size < SHARED_BUFFER_THRESHOLD) {// if it was decompressed
			*((uint32_t*)keyBuffer) = data.mv_size;
			*((uint32_t*)(keyBuffer + 4)) = 0; // buffer id of 0
		} else {
			EnvWrap::toSharedBuffer(dw->ew->env, (uint32_t*) dw->ew->keyBuffer, data);
		}
	}
	if (!(flags & VALUES_FOR_KEY)) {
		memcpy(keyBuffer + 32, key.mv_data, key.mv_size);
		*(keyBuffer + 32 + key.mv_size) = 0; // make sure it is null terminated for the sake of better ordered-binary performance
	}

	return key.mv_size;
}

const int START_ADDRESS_POSITION = 4064;
int32_t CursorWrap::doPosition(uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) {
	//char* keyBuffer = dw->ew->keyBuffer;
	MDB_val key, data;
	int rc;
	if (dw->ew->env == nullptr) {
		return MDB_BAD_TXN;
	}
	if (flags & RENEW_CURSOR) { // TODO: check the txn_id to determine if we need to renew
		rc = mdb_cursor_renew(txn = dw->ew->getReadTxn(), cursor);
		if (rc) {
			if (rc > 0)
				rc = -rc;
			return rc;
		}
	}
	if (endKeyAddress) {
		uint32_t* keyBuffer = (uint32_t*) endKeyAddress;
		endKey.mv_size = *keyBuffer;
		endKey.mv_data = (char*)(keyBuffer + 1);
	} else
		endKey.mv_size = 0;
	iteratingOp = (flags & REVERSE) ?
		(flags & INCLUDE_VALUES) ?
			(flags & VALUES_FOR_KEY) ? MDB_PREV_DUP : MDB_PREV :
			MDB_PREV_NODUP :
		(flags & INCLUDE_VALUES) ?
			(flags & VALUES_FOR_KEY) ? MDB_NEXT_DUP : MDB_NEXT :
			MDB_NEXT_NODUP;
	key.mv_size = keySize;
	key.mv_data = dw->ew->keyBuffer;
	if (keySize == 0) {
		rc = mdb_cursor_get(cursor, &key, &data, flags & REVERSE ? MDB_LAST : MDB_FIRST);  
	} else {
		if (flags & VALUES_FOR_KEY) { // only values for this key
			// take the next part of the key buffer as a pointer to starting data
			uint32_t* startValueBuffer = (uint32_t*)(size_t)(*(double*)(dw->ew->keyBuffer + START_ADDRESS_POSITION));
			data.mv_size = endKeyAddress ? *((uint32_t*)startValueBuffer) : 0;
			data.mv_data = startValueBuffer + 1;
			MDB_val startValue;
			if (flags & EXCLUSIVE_START)
				startValue = data; // save it for comparison
			if (flags & REVERSE) {// reverse through values
				startValue = data; // save it for comparison
				rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ? MDB_GET_BOTH_RANGE : MDB_SET_KEY);
				if (rc) {
					if (startValue.mv_size) {
						// value specified, but not found, so find key and go to last item
						rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_KEY);
						if (!rc)
							rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
					} // else just couldn't find the key
				} else { // found entry
					if (startValue.mv_size == 0) // no value specified, so go to last value
						rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
					else if (mdb_dcmp(txn, dw->dbi, &startValue, &data)) // the range found the next value *after* the start
						rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV_DUP);
				}
			} else // forward, just do a get by range
				rc = mdb_cursor_get(cursor, &key, &data, data.mv_size ?
					(flags & EXACT_MATCH) ? MDB_GET_BOTH : MDB_GET_BOTH_RANGE : MDB_SET_KEY);

			if (rc == MDB_NOTFOUND)
				return 0;
			if (flags & ONLY_COUNT && (!endKeyAddress || (flags & EXACT_MATCH))) {
				size_t count;
				rc = mdb_cursor_count(cursor, &count);
				if (rc)
					return rc > 0 ? -rc : rc;
				return count;
			}
			if (flags & EXCLUSIVE_START) {
				while(!rc) {
					if (mdb_dcmp(txn, dw->dbi, &startValue, &data))
						break;
					rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
				}
			}
		} else {
			MDB_val firstKey;
			if (flags & EXCLUSIVE_START)
				firstKey = key; // save it for comparison
			if (flags & REVERSE) {// reverse
				firstKey = key; // save it for comparison
				rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE);
				if (rc)
					rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST);
				else if (mdb_cmp(txn, dw->dbi, &firstKey, &key)) // the range found the next entry *after* the start
					rc = mdb_cursor_get(cursor, &key, &data, MDB_PREV);
				else if (dw->flags & MDB_DUPSORT)
					// we need to go to the last value of this key
					rc = mdb_cursor_get(cursor, &key, &data, MDB_LAST_DUP);
			} else // forward, just do a get by range
				rc = mdb_cursor_get(cursor, &key, &data, (flags & EXACT_MATCH) ? MDB_SET_KEY : MDB_SET_RANGE);
			if (flags & EXCLUSIVE_START) {
				while(!rc) {
					if (mdb_cmp(txn, dw->dbi, &firstKey, &key))
						break;
					rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
				}
			}
		}
	}

	while (offset-- > 0 && !rc) {
		rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
	}
	if (flags & ONLY_COUNT) {
		uint32_t count = 0;
		bool useCursorCount = false;
		// if we are in a dupsort database, and we are iterating over all entries, we can just count all the values for each key
		if (dw->flags & MDB_DUPSORT) {
			if (iteratingOp == MDB_PREV) {
				iteratingOp = MDB_PREV_NODUP;
				useCursorCount = true;
			}
			if (iteratingOp == MDB_NEXT) {
				iteratingOp = MDB_NEXT_NODUP;
				useCursorCount = true;
			}
		}

		while (!rc) {
			if (endKey.mv_size > 0) {
				int comparison;
				if (flags & VALUES_FOR_KEY)
					comparison = mdb_dcmp(txn, dw->dbi, &endKey, &data);
				else
					comparison = mdb_cmp(txn, dw->dbi, &endKey, &key);
				if ((flags & REVERSE) ? comparison >= 0 : (comparison <=0)) {
					if (!((flags & INCLUSIVE_END) && comparison == 0))
						return count;
				}
			}
			if (useCursorCount) {
				size_t countForKey;
				rc = mdb_cursor_count(cursor, &countForKey);
				if (rc) {
					if (rc > 0)
						rc = -rc;
					return rc;
				}
				count += countForKey;
			} else
				count++;
			rc = mdb_cursor_get(cursor, &key, &data, iteratingOp);
		}
		return count;
	}
	// TODO: Handle count?
	return returnEntry(rc, key, data);
}
NAPI_FUNCTION(position) {
	ARGS(5)
    GET_INT64_ARG(0);
    CursorWrap* cw = (CursorWrap*) i64;
	GET_UINT32_ARG(cw->flags, 1);
	uint32_t offset;
	GET_UINT32_ARG(offset, 2);
	uint32_t keySize;
	GET_UINT32_ARG(keySize, 3);
    napi_get_value_int64(env, args[4], &i64);
    int64_t endKeyAddress = i64;
	int32_t result = cw->doPosition(offset, keySize, endKeyAddress);
	RETURN_INT32(result);
}
int32_t positionFFI(double cwPointer, uint32_t flags, uint32_t offset, uint32_t keySize, uint64_t endKeyAddress) {
	CursorWrap* cw = (CursorWrap*) (size_t) cwPointer;
	DbiWrap* dw = cw->dw;
	dw->getFast = true;
	cw->flags = flags;
	return cw->doPosition(offset, keySize, endKeyAddress);
}

NAPI_FUNCTION(iterate) {
	ARGS(1)
    GET_INT64_ARG(0);
    CursorWrap* cw = (CursorWrap*) i64;
	MDB_val key, data;
	int rc;
	if (cw->dw->ew->env == nullptr) rc = MDB_BAD_TXN;
	else
		rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp);
	RETURN_INT32(cw->returnEntry(rc, key, data));
}

int32_t iterateFFI(double cwPointer) {
	CursorWrap* cw = (CursorWrap*) (size_t) cwPointer;
	DbiWrap* dw = cw->dw;
	dw->getFast = true;
	MDB_val key, data;
	if (cw->dw->ew->env == nullptr)
		return MDB_BAD_TXN;
	int rc = mdb_cursor_get(cw->cursor, &key, &data, cw->iteratingOp);
	return cw->returnEntry(rc, key, data);
}


NAPI_FUNCTION(getCurrentValue) {
	ARGS(1)
    GET_INT64_ARG(0);
    CursorWrap* cw = (CursorWrap*) i64;
	MDB_val key, data;
	int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT);
	RETURN_INT32(cw->returnEntry(rc, key, data));
}

napi_finalize noopCursor = [](napi_env, void *, void *) {
	// Data belongs to LMDB, we shouldn't free it here
};
NAPI_FUNCTION(getCurrentShared) {
	ARGS(1)
    GET_INT64_ARG(0);
    CursorWrap* cw = (CursorWrap*) i64;
	MDB_val key, data;
	int rc = mdb_cursor_get(cw->cursor, &key, &data, MDB_GET_CURRENT);
	if (rc)
		RETURN_INT32(cw->returnEntry(rc, key, data));
	getVersionAndUncompress(data, cw->dw);
	napi_create_external_buffer(env, data.mv_size,
		(char*) data.mv_data, noopCursor, nullptr, &returnValue);
	return returnValue;
}

NAPI_FUNCTION(renew) {
	ARGS(1)
    GET_INT64_ARG(0);
    CursorWrap* cw = (CursorWrap*) i64;
	mdb_cursor_renew(cw->txn = cw->dw->ew->getReadTxn(), cw->cursor);
	RETURN_UNDEFINED;
}

void CursorWrap::setupExports(Napi::Env env, Object exports) {
	// CursorWrap: Prepare constructor template
	Function CursorClass = DefineClass(env, "Cursor", {
	// CursorWrap: Add functions to the prototype
		CursorWrap::InstanceMethod("close", &CursorWrap::close),
		CursorWrap::InstanceMethod("del", &CursorWrap::del),
	});
	EXPORT_NAPI_FUNCTION("position", position);
	EXPORT_NAPI_FUNCTION("iterate", iterate);
	EXPORT_NAPI_FUNCTION("getCurrentValue", getCurrentValue);
	EXPORT_NAPI_FUNCTION("getCurrentShared", getCurrentShared);
	EXPORT_NAPI_FUNCTION("renew", renew);
	EXPORT_FUNCTION_ADDRESS("positionPtr", positionFFI);
	EXPORT_FUNCTION_ADDRESS("iteratePtr", iterateFFI);

	exports.Set("Cursor", CursorClass);

//	cursorTpl->InstanceTemplate()->SetInternalFieldCount(1);
}

// This file contains code from the node-lmdb project
// Copyright (c) 2013-2017 Timur Kristóf
// Copyright (c) 2021 Kristopher Tate
// Licensed to you under the terms of the MIT license
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

