/*
 * Copyright (c) 2015 ARM Limited. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 * 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.
 */
#include "mbed-client-classic/m2mconnectionhandlerpimpl.h"
#include "mbed-client/m2mconnectionobserver.h"
#include "mbed-client/m2mconstants.h"
#include "mbed-client/m2msecurity.h"
#include "mbed-client/m2mconnectionhandler.h"

#include "NetworkInterface.h"
#include "UDPSocket.h"
#include "TCPSocket.h"

#include "eventOS_event.h"
#include "eventOS_scheduler.h"

#include "mbed-trace/mbed_trace.h"
#include "mbed.h"

#define TRACE_GROUP "mClt"

#ifdef MBED_CONF_MBED_CLIENT_EVENT_LOOP_SIZE
#define MBED_CLIENT_EVENT_LOOP_SIZE MBED_CONF_MBED_CLIENT_EVENT_LOOP_SIZE
#else
#define MBED_CLIENT_EVENT_LOOP_SIZE 1024
#endif

int8_t M2MConnectionHandlerPimpl::_tasklet_id = -1;

static MemoryPool<M2MConnectionHandlerPimpl::TaskIdentifier, MBED_CLIENT_EVENT_LOOP_SIZE/64> memory_pool;

extern "C" void connection_tasklet_event_handler(arm_event_s *event)
{
    tr_debug("M2MConnectionHandlerPimpl::connection_tasklet_event_handler");
    M2MConnectionHandlerPimpl::TaskIdentifier *task_id = (M2MConnectionHandlerPimpl::TaskIdentifier*)event->data_ptr;
    M2MConnectionHandlerPimpl* pimpl = (M2MConnectionHandlerPimpl*)task_id->pimpl;
    if(pimpl) {
        eventOS_scheduler_set_active_tasklet(pimpl->connection_tasklet_handler());
    }
    switch (event->event_type) {
        case M2MConnectionHandlerPimpl::ESocketIdle:
            tr_debug("Connection Tasklet Generated");
            break;
        case M2MConnectionHandlerPimpl::ESocketReadytoRead:
            tr_debug("connection_tasklet_event_handler - ESocketReadytoRead");
            if(pimpl) {
                if(pimpl->is_handshake_ongoing()) {
                    pimpl->receive_handshake_handler();
                } else {
                    pimpl->receive_handler();
                }
            }
            break;
        case M2MConnectionHandlerPimpl::ESocketDnsHandler:
            tr_debug("connection_tasklet_event_handler - ESocketDnsHandler");
            if(pimpl) {
                pimpl->dns_handler();
            }
            break;
        case M2MConnectionHandlerPimpl::ESocketSend:
            tr_debug("connection_tasklet_event_handler - ESocketSend");
            if(pimpl) {
                pimpl->send_socket_data((uint8_t*)task_id->data_ptr,(uint16_t)event->event_data);
                if (task_id->data_ptr) {
                    free(task_id->data_ptr);
                }
            }
            break;
        default:
            break;
    }
    if (task_id) {
        memory_pool.free(task_id);
    }
}

M2MConnectionHandlerPimpl::M2MConnectionHandlerPimpl(M2MConnectionHandler* base, M2MConnectionObserver &observer,
                                                     M2MConnectionSecurity* sec,
                                                     M2MInterface::BindingMode mode,
                                                     M2MInterface::NetworkStack stack)
:_base(base),
 _observer(observer),
 _security_impl(sec),
 _use_secure_connection(false),
 _binding_mode(mode),
 _network_stack(stack),
 _socket(0),
 _is_handshaking(false),
 _listening(true),
 _server_type(M2MConnectionObserver::LWM2MServer),
 _server_port(0),
 _listen_port(0),
 _running(false),
 _net_iface(0),
 _socket_address(0)
{
    memset(&_address_buffer, 0, sizeof _address_buffer);
    memset(&_address, 0, sizeof _address);
    _address._address = _address_buffer;

    if (_network_stack != M2MInterface::LwIP_IPv4) {
        tr_error("ConnectionHandler: Unsupported network stack, only IPv4 is currently supported");
    }
    _running = true;
    tr_debug("M2MConnectionHandlerPimpl::M2MConnectionHandlerPimpl() - Initializing thread");
    eventOS_scheduler_mutex_wait();
    if (M2MConnectionHandlerPimpl::_tasklet_id == -1) {
        M2MConnectionHandlerPimpl::_tasklet_id = eventOS_event_handler_create(&connection_tasklet_event_handler, ESocketIdle);
    }
    eventOS_scheduler_mutex_release();
}

M2MConnectionHandlerPimpl::~M2MConnectionHandlerPimpl()
{
    tr_debug("M2MConnectionHandlerPimpl::~M2MConnectionHandlerPimpl()");
    if(_socket_address) {
        delete _socket_address;
        _socket_address = NULL;
    }
    if (_socket) {
        delete _socket;
        _socket = 0;
    }
    _net_iface = 0;
    delete _security_impl;
    tr_debug("M2MConnectionHandlerPimpl::~M2MConnectionHandlerPimpl() - OUT");
}

bool M2MConnectionHandlerPimpl::bind_connection(const uint16_t listen_port)
{
    _listen_port = listen_port;
    return true;
}

bool M2MConnectionHandlerPimpl::resolve_server_address(const String& server_address,
                                                       const uint16_t server_port,
                                                       M2MConnectionObserver::ServerType server_type,
                                                       const M2MSecurity* security)
{
    tr_debug("M2MConnectionHandlerPimpl::resolve_server_address()");
    if (!_net_iface) {
        return false;
    }
    _security = security;
    _server_port = server_port;
    _server_type = server_type;
    _server_address = server_address;
    TaskIdentifier* task = memory_pool.alloc();
    if (!task) {
        return false;
    }
    task->pimpl = this;

    arm_event_s event;
    event.receiver = M2MConnectionHandlerPimpl::_tasklet_id;
    event.sender = 0;
    event.event_type = ESocketDnsHandler;
    event.data_ptr = task;
    event.priority = ARM_LIB_HIGH_PRIORITY_EVENT;
    return eventOS_event_send(&event) == 0 ? true : false;
}

void M2MConnectionHandlerPimpl::dns_handler()
{
    tr_debug("M2MConnectionHandlerPimpl::dns_handler()");
    if(_socket_address) {
        delete _socket_address;
       _socket_address = NULL;
    }
    _socket_address = new SocketAddress(_net_iface,_server_address.c_str(), _server_port);
    if(*_socket_address) {
        _address._address = (void*)_socket_address->get_ip_address();
        tr_debug("IP Address %s",_socket_address->get_ip_address());
        tr_debug("Port %d",_socket_address->get_port());
        _address._length = strlen((char*)_address._address);
        _address._port = _socket_address->get_port();
        _address._stack = _network_stack;
    } else {
        _observer.socket_error(M2MConnectionHandler::DNS_RESOLVING_ERROR, true);
        close_socket();
        return;
    }

    close_socket();
    init_socket();

    if(is_tcp_connection()) {
       tr_debug("M2MConnectionHandlerPimpl::resolve_server_address - Using TCP");
        if (((TCPSocket*)_socket)->connect(*_socket_address) < 0) {
            _observer.socket_error(M2MConnectionHandler::SOCKET_ABORT);
            return;
        }
    }

    _running = true;

    if (_security) {
        if (_security->resource_value_int(M2MSecurity::SecurityMode) == M2MSecurity::Certificate ||
            _security->resource_value_int(M2MSecurity::SecurityMode) == M2MSecurity::Psk) {

            if( _security_impl != NULL ){
                _security_impl->reset();
                if (_security_impl->init(_security) == 0) {
                    _is_handshaking = true;
                    tr_debug("M2MConnectionHandlerPimpl::resolve_server_address - connect DTLS");
                    if(_security_impl->start_connecting_non_blocking(_base) < 0 ){
                        tr_debug("M2MConnectionHandlerPimpl::dns_handler - handshake failed");
                        _is_handshaking = false;
                        _observer.socket_error(M2MConnectionHandler::SSL_CONNECTION_ERROR);
                        close_socket();
                        return;
                    }
                } else {
                    tr_error("M2MConnectionHandlerPimpl::resolve_server_address - init failed");
                    _observer.socket_error(M2MConnectionHandler::SSL_CONNECTION_ERROR, false);
                    close_socket();
                    return;
                }
            } else {
                tr_error("M2MConnectionHandlerPimpl::dns_handler - sec is null");
                _observer.socket_error(M2MConnectionHandler::SSL_CONNECTION_ERROR, false);
                close_socket();
                return;
            }
        }
    }
    if(!_is_handshaking) {
        enable_keepalive();
        _observer.address_ready(_address,
                                _server_type,
                                _address._port);
    }
}

void M2MConnectionHandlerPimpl::send_handler()
{
    tr_debug("M2MConnectionHandlerPimpl::send_handler()");
    _observer.data_sent();
}

bool M2MConnectionHandlerPimpl::send_data(uint8_t *data,
                                          uint16_t data_len,
                                          sn_nsdl_addr_s *address)
{
    tr_debug("M2MConnectionHandlerPimpl::send_data()");
    if (address == NULL || data == NULL) {
        return false;
    }

    uint8_t *buffer = (uint8_t*)malloc(data_len);
    if(!buffer) {
        return false;
    }

    TaskIdentifier* task = memory_pool.alloc();
    if (!task) {
        free(buffer);
        return false;
    }
    task->pimpl = this;
    memcpy(buffer, data, data_len);
    task->data_ptr = buffer;
    arm_event_s event;
    event.receiver = M2MConnectionHandlerPimpl::_tasklet_id;
    event.sender = 0;
    event.event_type = ESocketSend;
    event.data_ptr = task;
    event.event_data = data_len;
    event.priority = ARM_LIB_HIGH_PRIORITY_EVENT;

    return eventOS_event_send(&event) == 0 ? true : false;
}

void M2MConnectionHandlerPimpl::send_socket_data(uint8_t *data,
                                                 uint16_t data_len)
{
    bool success = false;
    if( _use_secure_connection ){
        if( _security_impl->send_message(data, data_len) > 0){
            success = true;
        }
    } else {
        int32_t ret = -1;
        if(is_tcp_connection()){
            //We need to "shim" the length in front
            uint16_t d_len = data_len+4;
            uint8_t* d = (uint8_t*)malloc(data_len+4);

            d[0] = (data_len >> 24 )& 0xff;
            d[1] = (data_len >> 16 )& 0xff;
            d[2] = (data_len >> 8 )& 0xff;
            d[3] = data_len & 0xff;
            memmove(d+4, data, data_len);
            ret = ((TCPSocket*)_socket)->send(d,d_len);
            free(d);
        }else {
            ret = ((UDPSocket*)_socket)->sendto(*_socket_address,data, data_len);
        }
        if (ret > 0) {
            success = true;
        }
    }

    if (!success) {
        _observer.socket_error(M2MConnectionHandler::SOCKET_SEND_ERROR, true);
        close_socket();
    }
}

int8_t M2MConnectionHandlerPimpl::connection_tasklet_handler()
{
    return M2MConnectionHandlerPimpl::_tasklet_id;
}

void M2MConnectionHandlerPimpl::socket_event()
{
    TaskIdentifier* task = memory_pool.alloc();
    if (!task) {
    	_observer.socket_error(M2MConnectionHandler::SOCKET_READ_ERROR, true);
        return;
    }
    task->pimpl = this;

    arm_event_s event;
    event.receiver = M2MConnectionHandlerPimpl::_tasklet_id;
    event.sender = 0;
    event.event_type = ESocketReadytoRead;
    event.data_ptr = task;
    event.priority = ARM_LIB_HIGH_PRIORITY_EVENT;
    int8_t error = eventOS_event_send(&event);
    if(error != 0) {
    	_observer.socket_error(M2MConnectionHandler::SOCKET_READ_ERROR, true);
    }
}

bool M2MConnectionHandlerPimpl::start_listening_for_data()
{
    tr_debug("M2MConnectionHandlerPimpl::start_listening_for_data()");
    // Boolean return required for other platforms,
    // not needed in mbed OS Socket.
    _listening = true;
    _running = true;
    return _listening;
}

void M2MConnectionHandlerPimpl::stop_listening()
{
    tr_debug("M2MConnectionHandlerPimpl::stop_listening()");
    _listening = false;
    if(_security_impl) {
        _security_impl->reset();
    }
}

int M2MConnectionHandlerPimpl::send_to_socket(const unsigned char *buf, size_t len)
{
    tr_debug("M2MConnectionHandlerPimpl::send_to_socket len - %d", len);
    int size = -1;
    if(is_tcp_connection()) {
        size = ((TCPSocket*)_socket)->send(buf,len);
    } else {
        size = ((UDPSocket*)_socket)->sendto(*_socket_address,buf,len);
    }
    tr_debug("M2MConnectionHandlerPimpl::send_to_socket size - %d", size);
    if(NSAPI_ERROR_WOULD_BLOCK == size){
        if(_is_handshaking) {
            return M2MConnectionHandler::CONNECTION_ERROR_WANTS_WRITE;
        } else {
            return len;
        }
    }else if(size < 0){
        return -1;
    }else{
        if(!_is_handshaking) {
            _observer.data_sent();
        }
        return size;
    }
}

int M2MConnectionHandlerPimpl::receive_from_socket(unsigned char *buf, size_t len)
{
    tr_debug("M2MConnectionHandlerPimpl::receive_from_socket");
    int recv = -1;
    if(is_tcp_connection()) {
        recv = ((TCPSocket*)_socket)->recv(buf, len);
    } else {
        recv = ((UDPSocket*)_socket)->recvfrom(NULL,buf, len);
    }
    tr_debug("M2MConnectionHandlerPimpl::receive_from_socket recv size %d", recv);
    if(NSAPI_ERROR_WOULD_BLOCK == recv){
        return M2MConnectionHandler::CONNECTION_ERROR_WANTS_READ;
    }else if(recv < 0){
        return -1;
    }else{
        return recv;
    }
}

void M2MConnectionHandlerPimpl::handle_connection_error(int error)
{
    tr_debug("M2MConnectionHandlerPimpl::handle_connection_error");
    _observer.socket_error(error);
}

void M2MConnectionHandlerPimpl::set_platform_network_handler(void *handler)
{
    tr_debug("M2MConnectionHandlerPimpl::set_platform_network_handler");
    _net_iface = (NetworkInterface*)handler;
}

void M2MConnectionHandlerPimpl::receive_handshake_handler()
{
    tr_debug("M2MConnectionHandlerPimpl::receive_handshake_handler()");
    if( _is_handshaking ){
        int ret = _security_impl->continue_connecting();
        tr_debug("M2MConnectionHandlerPimpl::receive_handshake_handler() - ret %d", ret);
        if( ret == M2MConnectionHandler::CONNECTION_ERROR_WANTS_READ ){ //We wait for next readable event
            tr_debug("M2MConnectionHandlerPimpl::receive_handshake_handler() - We wait for next readable event");
            return;
        } else if( ret == 0 ){
            _is_handshaking = false;
            _use_secure_connection = true;
            enable_keepalive();
            _observer.address_ready(_address,
                                    _server_type,
                                    _server_port);
        }else if( ret < 0 ){
            _is_handshaking = false;
            _observer.socket_error(M2MConnectionHandler::SSL_CONNECTION_ERROR, true);
            close_socket();
        }
    }
}

bool M2MConnectionHandlerPimpl::is_handshake_ongoing()
{
    return _is_handshaking;
}

void M2MConnectionHandlerPimpl::receive_handler()
{
    tr_debug("M2MConnectionHandlerPimpl::receive_handler()");
    memset(_recv_buffer, 0, 1024);
    size_t receive_length = sizeof(_recv_buffer);

    if(_listening) {
        if( _use_secure_connection ){
            int rcv_size = _security_impl->read(_recv_buffer, receive_length);

            if(rcv_size >= 0){
                _observer.data_available((uint8_t*)_recv_buffer,
                                         rcv_size, _address);
            } else if (M2MConnectionHandler::CONNECTION_ERROR_WANTS_READ != rcv_size) {
                _observer.socket_error(M2MConnectionHandler::SOCKET_READ_ERROR, true);
                close_socket();
                return;
            }
        }else{
            int recv = -1;
            if(is_tcp_connection()){
                recv = ((TCPSocket*)_socket)->recv(_recv_buffer, receive_length);

            }else{
                recv = ((UDPSocket*)_socket)->recvfrom(NULL,_recv_buffer, receive_length);
            }
            if (recv > 0) {
                // Send data for processing.
                if(is_tcp_connection()){
                    //We need to "shim" out the length from the front
                    if( receive_length > 4 ){
                        uint64_t len = (_recv_buffer[0] << 24 & 0xFF000000) + (_recv_buffer[1] << 16 & 0xFF0000);
                        len += (_recv_buffer[2] << 8 & 0xFF00) + (_recv_buffer[3] & 0xFF);
                        if(len > 0) {
                            uint8_t* buf = (uint8_t*)malloc(len);
                            if(buf) {
                                memmove(buf, _recv_buffer+4, len);
                                // Observer for TCP plain mode
                                _observer.data_available(buf,len,_address);
                                free(buf);
                            }
                        }
                    }else{
                        _observer.socket_error(M2MConnectionHandler::SOCKET_READ_ERROR, true);
                        close_socket();
                    }
                } else { // Observer for UDP plain mode
                    tr_debug("M2MConnectionHandlerPimpl::receive_handler - data received %d", recv);
                    _observer.data_available((uint8_t*)_recv_buffer,
                                             recv, _address);
                }
            } else if(NSAPI_ERROR_WOULD_BLOCK != recv) {
                // Socket error in receiving
                _observer.socket_error(M2MConnectionHandler::SOCKET_READ_ERROR, true);
                close_socket();
            }
        }
    }
}

void M2MConnectionHandlerPimpl::claim_mutex()
{
    eventOS_scheduler_mutex_wait();
}

void M2MConnectionHandlerPimpl::release_mutex()
{
    eventOS_scheduler_mutex_release();
}

void M2MConnectionHandlerPimpl::init_socket()
{
    tr_debug("M2MConnectionHandlerPimpl::init_socket - IN");
    _is_handshaking = false;
    _running = true;

    if(is_tcp_connection()) {
       tr_debug("M2MConnectionHandlerPimpl::init_socket - Using TCP");
        _socket = new TCPSocket(_net_iface);
        if(_socket) {
            _socket->attach(this, &M2MConnectionHandlerPimpl::socket_event);
        } else {
            _observer.socket_error(M2MConnectionHandler::SOCKET_ABORT);
            return;
        }
    } else {
       tr_debug("M2MConnectionHandlerPimpl::init_socket - Using UDP - port %d", _listen_port);
        _socket = new UDPSocket(_net_iface);
        if(_socket) {
            _socket->bind(_listen_port);
            _socket->attach(this, &M2MConnectionHandlerPimpl::socket_event);
        } else {
            _observer.socket_error(M2MConnectionHandler::SOCKET_ABORT);
            return;
        }
    }
    _socket->set_blocking(false);
    tr_debug("M2MConnectionHandlerPimpl::init_socket - OUT");
}

bool M2MConnectionHandlerPimpl::is_tcp_connection()
{
    return _binding_mode == M2MInterface::TCP ||
            _binding_mode == M2MInterface::TCP_QUEUE ? true : false;
}

void M2MConnectionHandlerPimpl::close_socket()
{
    tr_debug("M2MConnectionHandlerPimpl::close_socket() - IN");
    if(_socket) {
       _running = false;
        _socket->close();
        delete _socket;
        _socket = NULL;
    }
    tr_debug("M2MConnectionHandlerPimpl::close_socket() - OUT");
}

void M2MConnectionHandlerPimpl::enable_keepalive()
{
#if MBED_CLIENT_TCP_KEEPALIVE_TIME
    if(is_tcp_connection()) {
        int keepalive = MBED_CLIENT_TCP_KEEPALIVE_TIME;
        int enable = 1;
        tr_debug("M2MConnectionHandlerPimpl::resolve_hostname - keepalive %d s\n", keepalive);
        if(_socket->setsockopt(1,NSAPI_KEEPALIVE,&enable,sizeof(enable)) != 0) {
            tr_error("M2MConnectionHandlerPimpl::enable_keepalive - setsockopt fail to Set Keepalive\n");
        }
        if(_socket->setsockopt(1,NSAPI_KEEPINTVL,&keepalive,sizeof(keepalive)) != 0) {
            tr_error("M2MConnectionHandlerPimpl::enable_keepalive - setsockopt fail to Set Keepalive TimeInterval\n");
        }
        if(_socket->setsockopt(1,NSAPI_KEEPIDLE,&keepalive,sizeof(keepalive)) != 0) {
            tr_error("M2MConnectionHandlerPimpl::enable_keepalive - setsockopt fail to Set Keepalive Time\n");
        }
    }
#endif
}
