/*************************************************************************/
/*  wsl_peer.cpp                                                         */
/*************************************************************************/
/*                       This file is part of:                           */
/*                           GODOT ENGINE                                */
/*                      https://godotengine.org                          */
/*************************************************************************/
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
/*                                                                       */
/* 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.                */
/*************************************************************************/

#ifndef JAVASCRIPT_ENABLED

#include "wsl_peer.h"

#include "wsl_client.h"
#include "wsl_server.h"

#include "core/crypto/crypto_core.h"
#include "core/math/random_number_generator.h"
#include "core/os/os.h"

String WSLPeer::generate_key() {
	// Random key
	RandomNumberGenerator rng;
	rng.set_seed(OS::get_singleton()->get_unix_time());
	Vector<uint8_t> bkey;
	int len = 16; // 16 bytes, as per RFC
	bkey.resize(len);
	uint8_t *w = bkey.ptrw();
	for (int i = 0; i < len; i++) {
		w[i] = (uint8_t)rng.randi_range(0, 255);
	}
	return CryptoCore::b64_encode_str(&w[0], len);
}

String WSLPeer::compute_key_response(String p_key) {
	String key = p_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // Magic UUID as per RFC
	Vector<uint8_t> sha = key.sha1_buffer();
	return CryptoCore::b64_encode_str(sha.ptr(), sha.size());
}

void WSLPeer::_wsl_destroy(struct PeerData **p_data) {
	if (!p_data || !(*p_data))
		return;
	struct PeerData *data = *p_data;
	if (data->polling) {
		data->destroy = true;
		return;
	}
	wslay_event_context_free(data->ctx);
	memdelete(data);
	*p_data = nullptr;
}

bool WSLPeer::_wsl_poll(struct PeerData *p_data) {
	p_data->polling = true;
	int err = 0;
	if ((err = wslay_event_recv(p_data->ctx)) != 0 || (err = wslay_event_send(p_data->ctx)) != 0) {
		print_verbose("Websocket (wslay) poll error: " + itos(err));
		p_data->destroy = true;
	}
	p_data->polling = false;

	if (p_data->destroy || (wslay_event_get_close_sent(p_data->ctx) && wslay_event_get_close_received(p_data->ctx))) {
		bool valid = p_data->valid;
		_wsl_destroy(&p_data);
		return valid;
	}
	return false;
}

ssize_t wsl_recv_callback(wslay_event_context_ptr ctx, uint8_t *data, size_t len, int flags, void *user_data) {
	struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data;
	if (!peer_data->valid) {
		wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
		return -1;
	}
	Ref<StreamPeer> conn = peer_data->conn;
	int read = 0;
	Error err = conn->get_partial_data(data, len, read);
	if (err != OK) {
		print_verbose("Websocket get data error: " + itos(err) + ", read (should be 0!): " + itos(read));
		wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
		return -1;
	}
	if (read == 0) {
		wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
		return -1;
	}
	return read;
}

ssize_t wsl_send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) {
	struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data;
	if (!peer_data->valid) {
		wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
		return -1;
	}
	Ref<StreamPeer> conn = peer_data->conn;
	int sent = 0;
	Error err = conn->put_partial_data(data, len, sent);
	if (err != OK) {
		wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
		return -1;
	}
	if (sent == 0) {
		wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
		return -1;
	}
	return sent;
}

int wsl_genmask_callback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, void *user_data) {
	RandomNumberGenerator rng;
	// TODO maybe use crypto in the future?
	rng.set_seed(OS::get_singleton()->get_unix_time());
	for (unsigned int i = 0; i < len; i++) {
		buf[i] = (uint8_t)rng.randi_range(0, 255);
	}
	return 0;
}

void wsl_msg_recv_callback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
	struct WSLPeer::PeerData *peer_data = (struct WSLPeer::PeerData *)user_data;
	if (!peer_data->valid || peer_data->closing) {
		return;
	}
	WSLPeer *peer = (WSLPeer *)peer_data->peer;

	if (peer->parse_message(arg) != OK)
		return;

	if (peer_data->is_server) {
		WSLServer *helper = (WSLServer *)peer_data->obj;
		helper->_on_peer_packet(peer_data->id);
	} else {
		WSLClient *helper = (WSLClient *)peer_data->obj;
		helper->_on_peer_packet();
	}
}

wslay_event_callbacks wsl_callbacks = {
	wsl_recv_callback,
	wsl_send_callback,
	wsl_genmask_callback,
	nullptr, /* on_frame_recv_start_callback */
	nullptr, /* on_frame_recv_callback */
	nullptr, /* on_frame_recv_end_callback */
	wsl_msg_recv_callback
};

Error WSLPeer::parse_message(const wslay_event_on_msg_recv_arg *arg) {
	uint8_t is_string = 0;
	if (arg->opcode == WSLAY_TEXT_FRAME) {
		is_string = 1;
	} else if (arg->opcode == WSLAY_CONNECTION_CLOSE) {
		close_code = arg->status_code;
		size_t len = arg->msg_length;
		close_reason = "";
		if (len > 2 /* first 2 bytes = close code */) {
			close_reason.parse_utf8((char *)arg->msg + 2, len - 2);
		}
		if (!wslay_event_get_close_sent(_data->ctx)) {
			if (_data->is_server) {
				WSLServer *helper = (WSLServer *)_data->obj;
				helper->_on_close_request(_data->id, close_code, close_reason);
			} else {
				WSLClient *helper = (WSLClient *)_data->obj;
				helper->_on_close_request(close_code, close_reason);
			}
		}
		return ERR_FILE_EOF;
	} else if (arg->opcode != WSLAY_BINARY_FRAME) {
		// Ping or pong
		return ERR_SKIP;
	}
	_in_buffer.write_packet(arg->msg, arg->msg_length, &is_string);
	return OK;
}

void WSLPeer::make_context(PeerData *p_data, unsigned int p_in_buf_size, unsigned int p_in_pkt_size, unsigned int p_out_buf_size, unsigned int p_out_pkt_size) {
	ERR_FAIL_COND(_data != nullptr);
	ERR_FAIL_COND(p_data == nullptr);

	_in_buffer.resize(p_in_pkt_size, p_in_buf_size);
	_packet_buffer.resize((1 << MAX(p_in_buf_size, p_out_buf_size)));

	_data = p_data;
	_data->peer = this;
	_data->valid = true;

	if (_data->is_server)
		wslay_event_context_server_init(&(_data->ctx), &wsl_callbacks, _data);
	else
		wslay_event_context_client_init(&(_data->ctx), &wsl_callbacks, _data);
	wslay_event_config_set_max_recv_msg_length(_data->ctx, (1ULL << p_in_buf_size));
}

void WSLPeer::set_write_mode(WriteMode p_mode) {
	write_mode = p_mode;
}

WSLPeer::WriteMode WSLPeer::get_write_mode() const {
	return write_mode;
}

void WSLPeer::poll() {
	if (!_data)
		return;

	if (_wsl_poll(_data)) {
		_data = nullptr;
	}
}

Error WSLPeer::put_packet(const uint8_t *p_buffer, int p_buffer_size) {

	ERR_FAIL_COND_V(!is_connected_to_host(), FAILED);

	struct wslay_event_msg msg; // Should I use fragmented?
	msg.opcode = write_mode == WRITE_MODE_TEXT ? WSLAY_TEXT_FRAME : WSLAY_BINARY_FRAME;
	msg.msg = p_buffer;
	msg.msg_length = p_buffer_size;

	wslay_event_queue_msg(_data->ctx, &msg);
	if (wslay_event_send(_data->ctx) < 0) {
		close_now();
		return FAILED;
	}
	return OK;
}

Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) {

	r_buffer_size = 0;

	ERR_FAIL_COND_V(!is_connected_to_host(), FAILED);

	if (_in_buffer.packets_left() == 0)
		return ERR_UNAVAILABLE;

	int read = 0;
	uint8_t *rw = _packet_buffer.ptrw();
	_in_buffer.read_packet(rw, _packet_buffer.size(), &_is_string, read);

	*r_buffer = rw;
	r_buffer_size = read;

	return OK;
}

int WSLPeer::get_available_packet_count() const {

	if (!is_connected_to_host())
		return 0;

	return _in_buffer.packets_left();
}

bool WSLPeer::was_string_packet() const {

	return _is_string;
}

bool WSLPeer::is_connected_to_host() const {

	return _data != nullptr;
}

void WSLPeer::close_now() {
	close(1000, "");
	_wsl_destroy(&_data);
}

void WSLPeer::close(int p_code, String p_reason) {
	if (_data && !wslay_event_get_close_sent(_data->ctx)) {
		CharString cs = p_reason.utf8();
		wslay_event_queue_close(_data->ctx, p_code, (uint8_t *)cs.ptr(), cs.size());
		wslay_event_send(_data->ctx);
		_data->closing = true;
	}

	_in_buffer.clear();
	_packet_buffer.resize(0);
}

IP_Address WSLPeer::get_connected_host() const {

	ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), IP_Address());

	return _data->tcp->get_connected_host();
}

uint16_t WSLPeer::get_connected_port() const {

	ERR_FAIL_COND_V(!is_connected_to_host() || _data->tcp.is_null(), 0);

	return _data->tcp->get_connected_port();
}

void WSLPeer::set_no_delay(bool p_enabled) {

	ERR_FAIL_COND(!is_connected_to_host() || _data->tcp.is_null());
	_data->tcp->set_no_delay(p_enabled);
}

void WSLPeer::invalidate() {
	if (_data)
		_data->valid = false;
}

WSLPeer::WSLPeer() {
	_data = nullptr;
	_is_string = 0;
	close_code = -1;
	write_mode = WRITE_MODE_BINARY;
}

WSLPeer::~WSLPeer() {

	close();
	invalidate();
	_wsl_destroy(&_data);
	_data = nullptr;
}

#endif // JAVASCRIPT_ENABLED