/************************************************************************************
 *
 * D++, A Lightweight C++ library for Discord
 *
 * Copyright 2021 Craig Edwards and D++ contributors
 * (https://github.com/brainboxdotcc/DPP/graphs/contributors)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ************************************************************************************/
#define _XOPEN_SOURCE
#include <string>
#include <iostream>
#include <fstream>
#include <time.h>
#include <stdlib.h>
#include <dpp/discordclient.h>
#include <dpp/discord.h>
#include <dpp/event.h>
#include <dpp/cache.h>
#include <dpp/stringops.h>
#include <dpp/nlohmann/json.hpp>
#include <fmt/format.h>

#ifdef _WIN32
#include <time.h>
#include <iomanip>
#include <sstream>

char* strptime(const char* s, const char* f, struct tm* tm) {
	std::istringstream input(s);
	input.imbue(std::locale(setlocale(LC_ALL, nullptr)));
	input >> std::get_time(tm, f);
	if (input.fail()) {
		return const_cast< char* >("");
	}
	return (char*)(s + input.tellg());
}
#endif

namespace dpp {

uint64_t SnowflakeNotNull(const json* j, const char *keyname) {
	/* Snowflakes are a special case. Pun intended.
	 * Because discord drinks the javascript kool-aid, they have to send 64 bit integers as strings as js can't deal with them
	 * even though we can. So, all snowflakes are sent and received wrapped as string values and must be read by nlohmann::json
	 * as string types, then converted from string to uint64_t. Checks for existence of the value, and that it is a string containing
	 * a number. If not, then this function returns 0.
	 */
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && k->is_string() ? strtoull(k->get<std::string>().c_str(), nullptr, 10) : 0;
	} else {
		return 0;
	}
}

void SetSnowflakeNotNull(const json* j, const char *keyname, uint64_t &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && k->is_string() ? strtoull(k->get<std::string>().c_str(), nullptr, 10) : 0;
	}
}


std::string StringNotNull(const json* j, const char *keyname) {
	/* Returns empty string if the value is not a string, or is null or not defined */
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && k->is_string() ? k->get<std::string>() : "";
	} else {
		return "";
	}
}

void SetStringNotNull(const json* j, const char *keyname, std::string &v) {
	/* Returns empty string if the value is not a string, or is null or not defined */
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && k->is_string() ? k->get<std::string>() : "";
	}
}

uint64_t Int64NotNull(const json* j, const char *keyname) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && !k->is_string() ? k->get<uint64_t>() : 0;
	} else {
		return 0;
	}
}

void SetInt64NotNull(const json* j, const char *keyname, uint64_t &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && !k->is_string() ? k->get<uint64_t>() : 0;
	}
}


uint32_t Int32NotNull(const json* j, const char *keyname) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && !k->is_string() ? k->get<uint32_t>() : 0;
	} else {
		return 0;
	}
}

void SetInt32NotNull(const json* j, const char *keyname, uint32_t &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && !k->is_string() ? k->get<uint32_t>() : 0;
	}
}

uint16_t Int16NotNull(const json* j, const char *keyname) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && !k->is_string() ? k->get<uint16_t>() : 0;
	} else {
		return 0;
	}
}

void SetInt16NotNull(const json* j, const char *keyname, uint16_t &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && !k->is_string() ? k->get<uint16_t>() : 0;
	}
}

uint8_t Int8NotNull(const json* j, const char *keyname) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() && !k->is_string() ? k->get<uint8_t>() : 0;
	} else {
		return 0;
	}
}

void SetInt8NotNull(const json* j, const char *keyname, uint8_t &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() && !k->is_string() ? k->get<uint8_t>() : 0;
	}
}

bool BoolNotNull(const json* j, const char *keyname) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		return !k->is_null() ? (k->get<bool>() == true) : false;
	} else {
		return false;
	}
}

void SetBoolNotNull(const json* j, const char *keyname, bool &v) {
	auto k = j->find(keyname);
	if (k != j->end()) {
		v = !k->is_null() ? (k->get<bool>() == true) : false;
	}
}

std::string base64_encode(unsigned char const* buf, unsigned int buffer_length) {
	/* Quick and dirty base64 encode */
	static const char to_base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	size_t ret_size = buffer_length + 2;

	ret_size = 4 * ret_size / 3;

	std::string ret;
	ret.reserve(ret_size);

	for (unsigned int i=0; i<ret_size/4; ++i)
	{
		size_t index = i*3;
		unsigned char b3[3];
		b3[0] = buf[index+0];
		b3[1] = buf[index+1];
		b3[2] = buf[index+2];

		ret.push_back(to_base64[ ((b3[0] & 0xfc) >> 2) ]);
		ret.push_back(to_base64[ ((b3[0] & 0x03) << 4) + ((b3[1] & 0xf0) >> 4) ]);
		ret.push_back(to_base64[ ((b3[1] & 0x0f) << 2) + ((b3[2] & 0xc0) >> 6) ]);
		ret.push_back(to_base64[ ((b3[2] & 0x3f)) ]);
	}

	return ret;
}

time_t TimestampNotNull(const json* j, const char* keyname)
{
	/* Parses discord ISO 8061 timestamps to time_t, accounting for local time adjustment.
	 * Note that discord timestamps contain a decimal seconds part, which time_t and struct tm
	 * can't handle. We strip these out.
	 */
	time_t retval = 0;
	if (j->find(keyname) != j->end() && !(*j)[keyname].is_null() && (*j)[keyname].is_string()) {
		tm timestamp = {};
		std::string timedate = (*j)[keyname].get<std::string>();
		if (timedate.find('+') != std::string::npos && timedate.find('.') != std::string::npos) {
			std::string tzpart = timedate.substr(timedate.find('+'), timedate.length());
			timedate = timedate.substr(0, timedate.find('.')) + tzpart ;
			strptime(timedate.substr(0, 19).c_str(), "%FT%TZ%z", &timestamp);
			timestamp.tm_isdst = 0;
			retval = mktime(&timestamp);
		} else {
			strptime(timedate.substr(0, 19).c_str(), "%F %T", &timestamp);
			retval = mktime(&timestamp);
		}
	}
	return retval;
}

void SetTimestampNotNull(const json* j, const char* keyname, time_t &v)
{
	/* Parses discord ISO 8061 timestamps to time_t, accounting for local time adjustment.
	 * Note that discord timestamps contain a decimal seconds part, which time_t and struct tm
	 * can't handle. We strip these out.
	 */
	time_t retval = 0;
	if (j->find(keyname) != j->end() && !(*j)[keyname].is_null() && (*j)[keyname].is_string()) {
		tm timestamp = {};
		std::string timedate = (*j)[keyname].get<std::string>();
		if (timedate.find('+') != std::string::npos && timedate.find('.') != std::string::npos) {
			std::string tzpart = timedate.substr(timedate.find('+'), timedate.length());
			timedate = timedate.substr(0, timedate.find('.')) + tzpart ;
			strptime(timedate.substr(0, 19).c_str(), "%FT%TZ%z", &timestamp);
			timestamp.tm_isdst = 0;
			retval = mktime(&timestamp);
		} else {
			strptime(timedate.substr(0, 19).c_str(), "%F %T", &timestamp);
			retval = mktime(&timestamp);
		}
		v = retval;
	}
}

std::map<std::string, dpp::events::event*> eventmap = {
	{ "__LOG__", new dpp::events::logger() },
	{ "GUILD_CREATE", new dpp::events::guild_create() },
	{ "GUILD_UPDATE", new dpp::events::guild_update() },
	{ "GUILD_DELETE", new dpp::events::guild_delete() },
	{ "GUILD_MEMBER_UPDATE", new dpp::events::guild_member_update() },
	{ "RESUMED", new dpp::events::resumed() },
	{ "READY", new dpp::events::ready() },
	{ "CHANNEL_CREATE", new dpp::events::channel_create() },
	{ "CHANNEL_UPDATE", new dpp::events::channel_update() },
	{ "CHANNEL_DELETE", new dpp::events::channel_delete() },
	{ "PRESENCE_UPDATE", new dpp::events::presence_update() },
	{ "TYPING_START", new dpp::events::typing_start() },
	{ "MESSAGE_CREATE", new dpp::events::message_create() },
	{ "MESSAGE_UPDATE", new dpp::events::message_update() },
	{ "MESSAGE_DELETE", new dpp::events::message_delete() },
	{ "MESSAGE_DELETE_BULK", new dpp::events::message_delete_bulk() },
	{ "MESSAGE_REACTION_ADD", new dpp::events::message_reaction_add() },
	{ "MESSAGE_REACTION_REMOVE", new dpp::events::message_reaction_remove() },
	{ "MESSAGE_REACTION_REMOVE_ALL", new dpp::events::message_reaction_remove_all() },
	{ "MESSAGE_REACTION_REMOVE_EMOJI", new dpp::events::message_reaction_remove_emoji() },
	{ "CHANNEL_PINS_UPDATE", new dpp::events::channel_pins_update() },
	{ "GUILD_BAN_ADD", new dpp::events::guild_ban_add() },
	{ "GUILD_BAN_REMOVE", new dpp::events::guild_ban_remove() },
	{ "GUILD_EMOJIS_UPDATE", new dpp::events::guild_emojis_update() },
	{ "GUILD_INTEGRATIONS_UPDATE", new dpp::events::guild_integrations_update() },
	{ "INTEGRATION_CREATE", new dpp::events::integration_create() },
	{ "INTEGRATION_UPDATE", new dpp::events::integration_update() },
	{ "INTEGRATION_DELETE", new dpp::events::integration_delete() },
	{ "GUILD_MEMBER_ADD", new dpp::events::guild_member_add() },
	{ "GUILD_MEMBER_REMOVE", new dpp::events::guild_member_remove() },
	{ "GUILD_MEMBERS_CHUNK", new dpp::events::guild_members_chunk() },
	{ "GUILD_ROLE_CREATE", new dpp::events::guild_role_create() },
	{ "GUILD_ROLE_UPDATE", new dpp::events::guild_role_update() },
	{ "GUILD_ROLE_DELETE", new dpp::events::guild_role_delete() },
	{ "VOICE_STATE_UPDATE", new dpp::events::voice_state_update() },
	{ "VOICE_SERVER_UPDATE", new dpp::events::voice_server_update() },
	{ "WEBHOOKS_UPDATE", new dpp::events::webhooks_update() },
	{ "INVITE_CREATE", new dpp::events::invite_create() },
	{ "INVITE_DELETE", new dpp::events::invite_delete() },
	{ "APPLICATION_COMMAND_CREATE", new dpp::events::application_command_create() },
	{ "APPLICATION_COMMAND_UPDATE", new dpp::events::application_command_update() },
	{ "APPLICATION_COMMAND_DELETE", new dpp::events::application_command_delete() },
	{ "INTERACTION_CREATE", new dpp::events::interaction_create() },
	{ "USER_UPDATE", new dpp::events::user_update() },
	{ "GUILD_JOIN_REQUEST_DELETE", new dpp::events::guild_join_request_delete() },
	{ "STAGE_INSTANCE_CREATE", new dpp::events::stage_instance_create() },
	{ "STAGE_INSTANCE_DELETE", new dpp::events::stage_instance_delete() },
	{ "THREAD_CREATE", new dpp::events::thread_create() },
	{ "THREAD_UPDATE", new dpp::events::thread_update() },
	{ "THREAD_DELETE", new dpp::events::thread_delete() },
	{ "THREAD_LIST_SYNC", new dpp::events::thread_list_sync() },
	{ "THREAD_MEMBER_UPDATE", new dpp::events::thread_member_update() },
	{ "THREAD_MEMBERS_UPDATE", new dpp::events::thread_members_update() },
	{ "GUILD_APPLICATION_COMMAND_COUNTS_UPDATE", nullptr },
	{ "GUILD_STICKERS_UPDATE", new dpp::events::guild_stickers_update() },
	{ "APPLICATION_COMMAND_PERMISSIONS_UPDATE", nullptr },
};

void discord_client::HandleEvent(const std::string &event, json &j, const std::string &raw)
{
	auto ev_iter = eventmap.find(event);
	if (ev_iter != eventmap.end()) {
		/* A handler with nullptr is silently ignored. We don't plan to make a handler for it
		 * so this usually some user-only thing thats crept into the API and shown to bots
		 * that we dont care about.
		 */
		if (ev_iter->second != nullptr) {
			ev_iter->second->handle(this, j, raw);
		}
	} else {
		log(dpp::ll_debug, fmt::format("Unhandled event: {}, {}", event, j.dump()));
	}
}

};
