/*
 * SPDX-FileCopyrightText: 2002 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include <tango/client/Connection.h>

#include <tango/internal/telemetry/telemetry_kernel_macros.h>

#include <tango/client/ApiExcept.h>
#include <tango/client/ApiUtil.h>
#include <tango/client/Database.h>

namespace Tango
{

//-----------------------------------------------------------------------------
//
// method :         Connection::command_inout_asyn()
//
// description :     Send a command to a Tango device asynchronously.
//            The client is not blocked until the command is executed
//
// argin(s) :        command : The command name
//            data_in : Command input data
//            faf : Fire And Forget flag
//
// return :        The asynchronous call identifier
//
//-----------------------------------------------------------------------------

long Connection::command_inout_asynch(const std::string &command, const DeviceData &data_in, bool faf)
{
    TANGO_TELEMETRY_TRACE_BEGIN(({{"tango.operation.target", dev_name()}, {"tango.operation.argument", command}}));

    //
    // Reconnect to device in case it is needed
    //

    try
    {
        check_and_reconnect();
    }
    catch(Tango::ConnectionFailed &e)
    {
        TangoSys_OMemStream desc;
        desc << "Failed to execute command_inout on device " << dev_name();
        desc << ", command " << command << std::ends;
        TANGO_RETHROW_DETAILED_EXCEPTION(ApiConnExcept, e, API_CommandFailed, desc.str());
    }

    //
    // Throw exception if caller not allowed to do write action
    //

    if(access == ACCESS_READ)
    {
        int db_num;
        ApiUtil *au = ApiUtil::instance();

        if(get_from_env_var())
        {
            db_num = au->get_db_ind();
        }
        else
        {
            db_num = au->get_db_ind(get_db_host(), get_db_port_num());
        }

        std::vector<Database *> &v_d = au->get_db_vect();
        Database *db = v_d[db_num];

        //
        // If the command is not allowed, throw exception
        // Also throw exception if it was not possible to get the list
        // of allowed commands from the control access service
        //

        std::string d_name = dev_name();

        if(!db->is_command_allowed(d_name, command))
        {
            DevErrorList &e = db->get_access_except_errors();
            if(e.length() != 0)
            {
                DevFailed df(e);
                throw df;
            }

            TangoSys_OMemStream desc;
            desc << "Command_inout_asynch on device " << dev_name() << " for command ";
            desc << command << " is not authorized" << std::ends;

            TANGO_THROW_DETAILED_EXCEPTION(NotAllowedExcept, API_ReadOnlyMode, desc.str());
        }
    }

    //
    // Create the request object
    //

    CORBA::Request_ptr request;

    if(version >= 4)
    {
        request = device_4->_request("command_inout_4");
    }
    else if(version >= 2)
    {
        request = device_2->_request("command_inout_2");
    }
    else
    {
        request = device->_request("command_inout");
    }

    request->add_in_arg() <<= command.c_str();
    request->add_in_arg() <<= data_in.any.in();

    if(version >= 4)
    {
        request->add_in_arg() <<= source;
        request->add_in_arg() <<= get_client_identification();
    }
    else if(version >= 2)
    {
        request->add_in_arg() <<= source;
    }

    request->set_return_type(CORBA::_tc_any);
    request->exceptions()->add(Tango::_tc_DevFailed);

    //
    // If its a fire and forget (faf) call, send it and forget. Otherwise, send it
    // and store the request in the global asynchronous polling requests table
    //

    long id = 0;

    if(!faf)
    {
        id = add_asyn_request(request, TgRequest::CMD_INOUT);
        request->send_deferred();
    }
    else
    {
        request->send_oneway();
        CORBA::release(request);
    }

    return id;

    TANGO_TELEMETRY_TRACE_END();
}

//-----------------------------------------------------------------------------
//
// method :         Connection::command_inout_asyn()
//
// description :     Send a command to a Tango device asynchrnously.
//            The client is not blocked until the command is executed
//            These methods are only user helper methods which call
//            the official command_inout_asyn() method
//
// argin(s) :        command : The command name
//            data_in : Command input data
//            faf : Fire And Forget flag
//
// return :        The asynchronous call identifier
//
//-----------------------------------------------------------------------------

long Connection::command_inout_asynch(const std::string &command, bool faf)
{
    DeviceData data_in;
    return command_inout_asynch(command, data_in, faf);
}

//-----------------------------------------------------------------------------
//
// method :         Connection::command_inout_reply()
//
// description :     Try to obtain data returned by a command asynchronously
//            requested. This method does not block if the reply is
//            not yet arrived. An exception is thrown in this case.
//
// argin(s) :        id : The asynchrnous call identifier
//
// return :        The command output data
//
//-----------------------------------------------------------------------------

DeviceData Connection::command_inout_reply(long id)
{
    TANGO_TELEMETRY_TRACE_BEGIN(({{"tango.operation.target", dev_name()}}));

    DeviceData data_out;
    //
    // Retrieve request object
    //

    TgRequest &req = ApiUtil::instance()->get_pasyn_table()->get_request(id);

    //
    // Check request type
    //

    if(req.req_type != TgRequest::CMD_INOUT)
    {
        TANGO_THROW_DETAILED_EXCEPTION(ApiAsynExcept, API_BadAsynReqType, "Incompatible request type");
    }

    //
    // Reply arrived ? Throw exception if not yet arrived
    //

    if(!req.request->poll_response())
    {
        TangoSys_OMemStream desc;
        desc << "Device " << dev_name();
        desc << ": Reply for asynchronous call (id = " << id;
        desc << ") is not yet arrived" << std::ends;
        TANGO_THROW_DETAILED_EXCEPTION(ApiAsynNotThereExcept, API_AsynReplyNotArrived, desc.str());
    }

    //
    // Check if the reply is an exception
    // Due to a compatibility pb between omniORB 4.1 and omniORB 4.1 (at least 4.2.0),
    // we have to also handle CORBA::Request instance throwing exception in its env() and
    // other methods. This was not the case in omniORB 4.1!
    //

    CORBA::Environment_ptr env = nullptr;
    try
    {
        env = req.request->env();
    }
    catch(CORBA::TRANSIENT &tra)
    {
        char cb_excep_mess[256];
        Tango::Except::print_CORBA_SystemException_r(&tra, cb_excep_mess);
        if(tra.minor() == omni::TRANSIENT_CallTimedout)
        {
            omni420_timeout(id, cb_excep_mess);
        }
        else
        {
            set_connection_state(CONNECTION_NOTOK);
            return omni420_except(id, cb_excep_mess, req);
        }
    }
    catch(CORBA::SystemException &ex)
    {
        set_connection_state(CONNECTION_NOTOK);
        char cb_excep_mess[256];
        Tango::Except::print_CORBA_SystemException_r(&ex, cb_excep_mess);
        return omni420_except(id, cb_excep_mess, req);
    }

    if(!CORBA::is_nil(env) && (env->exception() == nullptr))
    {
        //
        // Get received value
        //

        const CORBA::Any *received = nullptr;
        CORBA::Any &dii_any = req.request->return_value();
        dii_any >>= received;
        CORBA::Any *server_any = new CORBA::Any(*received);

        data_out.any = server_any;
    }
    else
    {
        //
        // Retrieve exception and re-throw it.
        //

        CORBA::Exception *ex_ptr = env->exception();

        //
        // Special treatment for timeout exception (TRANSIENT with specific minor code)
        //

        CORBA::TRANSIENT *tra;
        if((tra = CORBA::TRANSIENT::_downcast(ex_ptr)) != nullptr)
        {
            if(tra->minor() == omni::TRANSIENT_CallTimedout)
            {
                bool need_reconnect = false;
                if(ext->has_alt_adr)
                {
                    try
                    {
                        Device_var dev = Device::_duplicate(device);
                        dev->ping();
                    }
                    catch(CORBA::TRANSIENT &trans_ping)
                    {
                        if(trans_ping.minor() == omni::TRANSIENT_ConnectFailed ||
                           trans_ping.minor() == omni::TRANSIENT_CallTimedout)
                        {
                            need_reconnect = true;
                        }
                    }
                    catch(...)
                    {
                    }
                }

                char cb_excep_mess[256];
                Tango::Except::print_CORBA_SystemException_r(tra, cb_excep_mess);

                if(!need_reconnect)
                {
                    CORBA::NVList_ptr req_arg = req.request->arguments();
                    const char *cmd = nullptr;
                    CORBA::NamedValue_ptr nv = req_arg->item(0);
                    *(nv->value()) >>= cmd;
                    char *tmp = Tango::string_dup(cmd);

                    TangoSys_OMemStream desc;
                    desc << "Timeout (" << timeout << " mS) exceeded on device " << dev_name();
                    desc << ", command " << tmp << std::ends;
                    Tango::string_free(tmp);

                    remove_asyn_request(id);

                    TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_DeviceTimedOut, desc.str());
                }
                else
                {
                    set_connection_state(CONNECTION_NOTOK);
                    remove_asyn_request(id);

                    std::stringstream ss;
                    ss << "Failed to execute command_inout_asynch on device " << dev_name();

                    TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_CommunicationFailed, ss.str());
                }
            }
        }

        CORBA::UnknownUserException *unk_ex;
        if((unk_ex = CORBA::UnknownUserException::_downcast(ex_ptr)) != nullptr)
        {
            //
            // It is a UserUnknownException exception. This means that the
            // server has sent a DevFailed exception
            //

            const Tango::DevFailed *serv_ex;
            unk_ex->exception() >>= serv_ex;
            Tango::DevFailed ex(*serv_ex);

            CORBA::NVList_ptr req_arg = req.request->arguments();
            const char *cmd = nullptr;
            CORBA::NamedValue_ptr nv = req_arg->item(0);
            *(nv->value()) >>= cmd;
            char *tmp = Tango::string_dup(cmd);

            TangoSys_OMemStream desc;
            desc << "Failed to execute command_inout_asynch on device " << dev_name();
            desc << ", command " << tmp << std::ends;
            Tango::string_free(tmp);

            remove_asyn_request(id);

            TANGO_RETHROW_EXCEPTION(ex, API_CommandFailed, desc.str());
        }

        CORBA::SystemException *sys_ex;
        if((sys_ex = CORBA::SystemException::_downcast(ex_ptr)) != nullptr)
        {
            set_connection_state(CONNECTION_NOTOK);

            //
            // Re-throw nearly all CORBA system exceptions
            //

            CORBA::NVList_ptr req_arg = req.request->arguments();
            const char *cmd = nullptr;
            CORBA::NamedValue_ptr nv = req_arg->item(0);
            *(nv->value()) >>= cmd;
            char *tmp = Tango::string_dup(cmd);

            char cb_excep_mess[256];
            Tango::Except::print_CORBA_SystemException_r(sys_ex, cb_excep_mess);

            //
            // Check if the exception was a connection exception
            // If so, execute the command synchronously (tries to reconnect)
            // If successful just return, otherwise throw the first exception
            //

            std::string ex(cb_excep_mess);
            std::string::size_type pos_con = ex.find("TRANSIENT_ConnectFailed");
            std::string::size_type pos_one = ex.find("EXIST_NoMatch");
            if(pos_con != std::string::npos || pos_one != std::string::npos)
            {
                try
                {
                    DeviceData dd_out = redo_synch_cmd(req);

                    //
                    // Remove request from request global table.
                    //

                    Tango::string_free(tmp);

                    remove_asyn_request(id);

                    return dd_out;
                }
                catch(Tango::DevFailed &)
                {
                }
            }

            TangoSys_OMemStream desc;
            desc << "Failed to execute command_inout_asynch on device " << dev_name();
            desc << ", command " << tmp << std::ends;
            Tango::string_free(tmp);

            remove_asyn_request(id);

            TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_CommunicationFailed, desc.str());
        }
    }

    //
    // Remove request from request global table.
    //

    remove_asyn_request(id);

    return data_out;

    TANGO_TELEMETRY_TRACE_END();
}

//-----------------------------------------------------------------------------
//
// method :         Connection::command_inout_reply()
//
// description :     Try to obtain data returned by a command asynchronously
//            requested. This method blocks if the reply is
//            not yet arrived for a max of timeout mS.
//            An exception is thrown in case of reply not yet arrived.
//
// argin(s) :        call_timeout : The max time to block the calling process
//            if timeout is 0, this means wait until the reply is
//            there
//            id : The asynchrnous call identifier
//
// return :        The command output data
//
//-----------------------------------------------------------------------------

DeviceData Connection::command_inout_reply(long id, long call_timeout)
{
    TANGO_TELEMETRY_TRACE_BEGIN(({{"tango.operation.target", dev_name()}}));

    //
    // Retrieve request object
    //

    TgRequest &req = ApiUtil::instance()->get_pasyn_table()->get_request(id);

    //
    // Check request type
    //

    if(req.req_type != TgRequest::CMD_INOUT)
    {
        TANGO_THROW_DETAILED_EXCEPTION(ApiAsynExcept, API_BadAsynReqType, "Incompatible request type");
    }

    //
    // Use CORBA get_response call if the call timeout is specified as 0
    // (If the response is not already there).
    // Otherwise, uses a loop. Switch process to sleeping state for 20 ms
    // between each test to check if the replies has arrived.
    //

    if(call_timeout == 0)
    {
        if(!req.request->poll_response())
        {
            try
            {
                req.request->get_response();
            }
            catch(...)
            {
            }
        }
    }
    else
    {
        long nb = call_timeout / 20;
        int i;

        for(i = 0; i < nb; i++)
        {
            if(req.request->poll_response())
            {
                break;
            }

            std::this_thread::sleep_for(std::chrono::milliseconds(20));
        }

        if(i == nb)
        {
            if(!req.request->poll_response())
            {
                TangoSys_OMemStream desc;
                desc << "Device " << dev_name();
                desc << ": Reply for asynchronous call (id = " << id;
                desc << ") is not yet arrived" << std::ends;
                TANGO_THROW_DETAILED_EXCEPTION(ApiAsynNotThereExcept, API_AsynReplyNotArrived, desc.str());
            }
        }
    }

    //
    // Check if the reply is an exception
    // Due to a compatibility pb between omniORB 4.1 and omniORB 4.1 (at least 4.2.0),
    // we have to also handle CORBA::Request instance throwing exception in its env() and
    // other methods. This was not the case in omniORB 4.1!
    //

    DeviceData data_out;
    CORBA::Environment_ptr env = nullptr;

    try
    {
        env = req.request->env();
    }
    catch(CORBA::TRANSIENT &tra)
    {
        char cb_excep_mess[256];
        Tango::Except::print_CORBA_SystemException_r(&tra, cb_excep_mess);
        if(tra.minor() == omni::TRANSIENT_CallTimedout)
        {
            omni420_timeout(id, cb_excep_mess);
        }
        else
        {
            set_connection_state(CONNECTION_NOTOK);
            return omni420_except(id, cb_excep_mess, req);
        }
    }
    catch(CORBA::SystemException &ex)
    {
        set_connection_state(CONNECTION_NOTOK);
        char cb_excep_mess[256];
        Tango::Except::print_CORBA_SystemException_r(&ex, cb_excep_mess);
        return omni420_except(id, cb_excep_mess, req);
    }

    if(!CORBA::is_nil(env) && (env->exception() == nullptr))
    {
        //
        // It's not an exception, therefore get received value
        //

        const CORBA::Any *received;
        CORBA::Any &dii_any = req.request->return_value();
        dii_any >>= received;
        CORBA::Any *server_any = new CORBA::Any(*received);

        data_out.any = server_any;
    }
    else
    {
        //
        // Retrieve exception and re-throw it.
        //

        CORBA::Exception *ex_ptr = env->exception();

        //
        // Special treatement for timeout exception (TRANSIENT with specific minor code)
        //

        CORBA::TRANSIENT *tra;
        if((tra = CORBA::TRANSIENT::_downcast(ex_ptr)) != nullptr)
        {
            if(tra->minor() == omni::TRANSIENT_CallTimedout)
            {
                bool need_reconnect = false;
                if(ext->has_alt_adr)
                {
                    try
                    {
                        Device_var dev = Device::_duplicate(device);
                        dev->ping();
                    }
                    catch(CORBA::TRANSIENT &trans_ping)
                    {
                        if(trans_ping.minor() == omni::TRANSIENT_ConnectFailed ||
                           trans_ping.minor() == omni::TRANSIENT_CallTimedout)
                        {
                            need_reconnect = true;
                        }
                    }
                    catch(...)
                    {
                    }
                }

                char cb_excep_mess[256];
                Tango::Except::print_CORBA_SystemException_r(tra, cb_excep_mess);

                if(!need_reconnect)
                {
                    CORBA::NVList_ptr req_arg = req.request->arguments();
                    const char *cmd = nullptr;
                    CORBA::NamedValue_ptr nv = req_arg->item(0);
                    *(nv->value()) >>= cmd;
                    char *tmp = Tango::string_dup(cmd);

                    TangoSys_OMemStream desc;
                    desc << "Timeout (" << timeout << " mS) exceeded on device " << dev_name();
                    desc << ", command " << tmp << std::ends;
                    Tango::string_free(tmp);

                    remove_asyn_request(id);

                    TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_DeviceTimedOut, desc.str());
                }
                else
                {
                    set_connection_state(CONNECTION_NOTOK);
                    remove_asyn_request(id);

                    std::stringstream ss;
                    ss << "Failed to execute command_inout_asynch on device " << dev_name();

                    TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_CommunicationFailed, ss.str());
                }
            }
        }

        CORBA::UnknownUserException *unk_ex;
        if((unk_ex = CORBA::UnknownUserException::_downcast(ex_ptr)) != nullptr)
        {
            //
            // It is a UserUnknownException exception. This means that the
            // server has sent a DevFailed exception
            //

            const Tango::DevFailed *serv_ex;
            unk_ex->exception() >>= serv_ex;
            Tango::DevFailed ex(*serv_ex);

            CORBA::NVList_ptr req_arg = req.request->arguments();
            const char *cmd = nullptr;
            CORBA::NamedValue_ptr nv = req_arg->item(0);
            *(nv->value()) >>= cmd;
            char *tmp = Tango::string_dup(cmd);

            TangoSys_OMemStream desc;
            desc << "Failed to execute command_inout_asynch on device " << dev_name();
            desc << ", command " << tmp << std::ends;
            Tango::string_free(tmp);

            remove_asyn_request(id);
            ;

            TANGO_RETHROW_EXCEPTION(ex, API_CommandFailed, desc.str());
        }

        CORBA::SystemException *sys_ex;
        if((sys_ex = CORBA::SystemException::_downcast(ex_ptr)) != nullptr)
        {
            set_connection_state(CONNECTION_NOTOK);

            //
            // Re-throw nearly all CORBA system exceptions
            //

            CORBA::NVList_ptr req_arg = req.request->arguments();
            const char *cmd = nullptr;
            CORBA::NamedValue_ptr nv = req_arg->item(0);
            *(nv->value()) >>= cmd;
            char *tmp = Tango::string_dup(cmd);

            char cb_excep_mess[256];
            Tango::Except::print_CORBA_SystemException_r(sys_ex, cb_excep_mess);

            //
            // Check if the exception was a connection exception
            // If so, execute the command synchronously (tries to reconnect)
            // If successful just return, otherwise throw the first exception
            //

            std::string ex(cb_excep_mess);
            std::string::size_type pos_con = ex.find("TRANSIENT_ConnectFailed");
            std::string::size_type pos_one = ex.find("EXIST_NoMatch");
            if(pos_con != std::string::npos || pos_one != std::string::npos)
            {
                try
                {
                    DeviceData dd_out = redo_synch_cmd(req);

                    //
                    // Remove request from request global table.
                    //

                    Tango::string_free(tmp);

                    remove_asyn_request(id);

                    return dd_out;
                }
                catch(Tango::DevFailed &)
                {
                }
            }

            TangoSys_OMemStream desc;
            desc << "Failed to execute command_inout_asynch on device " << dev_name();
            desc << ", command " << tmp << std::ends;
            Tango::string_free(tmp);

            remove_asyn_request(id);

            TANGO_RETHROW_DETAILED_EXCEPTION(ApiCommExcept, cb_excep_mess, API_CommunicationFailed, desc.str());
        }
    }

    //
    // Remove request from request global table.
    //

    remove_asyn_request(id);

    return data_out;

    TANGO_TELEMETRY_TRACE_END();
}

} // namespace Tango
