#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

#include "opc_ua_common.h"

UA_Boolean running = true;

void inactivityCallback(UA_Client *client)
{
    dy_syslog(LOG_WARNING, "Server Inactivity");
}

static void opc_ua_browser_recursion(connect_config_t *connect_cfg, UA_NodeId parent)
{
    UA_Client *client = connect_cfg->client;
    UA_BrowseRequest bReq;
    UA_BrowseResponse bResp;

    if (connect_cfg->browsered)
    {
        dy_syslog(LOG_ERR, "already browsered");
        return;
    }

    UA_BrowseRequest_init(&bReq);
    bReq.requestedMaxReferencesPerNode = 0;
    bReq.nodesToBrowse = UA_BrowseDescription_new();
    bReq.nodesToBrowseSize = 1;
    bReq.nodesToBrowse[0].nodeId = parent;                      /* browse objects folder */
    bReq.nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL; /* return everything */
    bResp = UA_Client_Service_browse(client, bReq);
    for (size_t i = 0; i < bResp.resultsSize; ++i)
    {
        for (size_t j = 0; j < bResp.results[i].referencesSize; ++j)
        {
            UA_ReferenceDescription *ref = &(bResp.results[i].references[j]);

            if (ref->nodeClass == UA_NODECLASS_OBJECT)
            {
                UA_NodeId new;
                UA_NodeId_copy(&ref->nodeId.nodeId, &new);
                opc_ua_browser_recursion(connect_cfg, new);
            }
            else if (ref->nodeClass == UA_NODECLASS_VARIABLE || ref->nodeClass == UA_NODECLASS_METHOD)
            {
                dev_node_t *node = NULL;
                dev_node_t *tmp = NULL;
                pthread_rwlock_wrlock(&connect_cfg->data_lock);
                list_for_each_entry_safe(node, tmp, &connect_cfg->node_list, list2)
                {
                    int k = 0;

                    for (k = 0; k < node->tag_cnt; k++)
                    {
                        opc_ua_tag_t *tag = &node->tag[k];

                        if (tag->type == NODE_ID_NEED_BROWSER)
                        {
                            if ((int)ref->displayName.text.length == strlen(tag->display_name) &&
                                strncmp(ref->displayName.text.data, tag->display_name, (int)ref->displayName.text.length) == 0)
                            {
                                UA_NodeId_copy(&ref->nodeId.nodeId, &tag->nodeId);
                                tag->type = NODE_ID_BROWSER;
                                dy_syslog(LOG_DEBUG, "find node:%-16.*s\n", (int)ref->displayName.text.length, ref->displayName.text.data);
                                break;
                            }
                        }
                    }
                }
                pthread_rwlock_unlock(&connect_cfg->data_lock);
            }
        }
    }
    UA_BrowseRequest_clear(&bReq);
    UA_BrowseResponse_clear(&bResp);
}

static UA_StatusCode opc_ua_read_multi(connect_config_t *connect_cfg)
{
    UA_Client *client = connect_cfg->client;
    int arraySize = 0;
    static UA_ReadValueId *itemArray = NULL;
    static UA_StatusCode *retStatusArray = NULL;
    UA_StatusCode retval = UA_STATUSCODE_GOOD;
    int i;
    dev_node_t *node = NULL;
    dev_node_t *tmp = NULL;

    arraySize = connect_cfg->readable_tag_cnt;
    if (arraySize <= 0)
    {
        dy_syslog(LOG_WARNING, "size is invalid, %d", arraySize);
        goto out;
    }

    if (itemArray == NULL)
    {
        itemArray = calloc(arraySize, sizeof(UA_ReadValueId));
    }

    for (i = 0; i < connect_cfg->readable_tag_cnt; ++i)
    {
        UA_ReadValueId_init(&itemArray[i]);
        itemArray[i].attributeId = UA_ATTRIBUTEID_VALUE;
        itemArray[i].nodeId = connect_cfg->tags[i]->nodeId;
    }

    list_for_each_entry_safe(node, tmp, &connect_cfg->used_node_list, used_list)
    {
        flush_device_status_by_sn(connect_cfg->var->ds, ACTION_SND, node->sn);
    }
    UA_ReadRequest request;
    UA_ReadRequest_init(&request);
    request.nodesToRead = itemArray;
    request.nodesToReadSize = arraySize;
    UA_ReadResponse response = UA_Client_Service_read(client, request);

    if (retStatusArray == NULL)
    {
        retStatusArray = calloc(arraySize, sizeof(UA_StatusCode));
    }

    retval = response.responseHeader.serviceResult;
    if (retval == UA_STATUSCODE_GOOD)
    {
        if (response.resultsSize == arraySize)
        {
            for (i = 0; i < arraySize; ++i)
            {
                retStatusArray[i] = response.results[i].status;
            }
        }
        else
        {
            UA_ReadResponse_clear(&response);
            retval = UA_STATUSCODE_BADUNEXPECTEDERROR;
            goto out;
        }

        for (i = 0; i < arraySize; ++i)
        {
            if (retStatusArray[i] == UA_STATUSCODE_GOOD)
            {
                if (!response.results[i].hasValue)
                {
                    UA_ReadResponse_clear(&response);
                    retval = UA_STATUSCODE_BADUNEXPECTEDERROR;
                    goto out;
                }

                UA_Variant_clear(&connect_cfg->tags[i]->val);
                UA_Variant_copy(&response.results[i].value, &connect_cfg->tags[i]->val);
                connect_cfg->tags[i]->updated = true;
            }
            else
            {
                dy_syslog(LOG_ERR, "read failed:%s", connect_cfg->tags[i]->perperty->identifier);
            }
        }
    }
    UA_ReadResponse_clear(&response);

out:
    return retval;
}

static int opc_ua_update_connect_tags(connect_config_t *connect_cfg)
{
    dev_node_t *node = NULL;
    dev_node_t *tmp = NULL;
    int i = 0, j = 0;
    int used_node = 0;

    j = 0;
    list_for_each_entry_safe(node, tmp, &connect_cfg->node_list, list2)
    {
        used_node = 0;
        for (i = 0; i < node->tag_cnt; i++)
        {
            if (node->tag[i].type == NODE_ID_BROWSER || node->tag[i].type == NODE_ID_USER_DEFINE)
            {
                j++;
                used_node = 1;
            }
        }

        if (used_node == 1)
        {
            list_add_tail(&node->used_list, &connect_cfg->used_node_list);
        }
    }

    connect_cfg->readable_tag_cnt = j;
    connect_cfg->tags = calloc(j, sizeof(opc_ua_tag_t *));
    j = 0;
    list_for_each_entry_safe(node, tmp, &connect_cfg->node_list, list2)
    {
        for (i = 0; i < node->tag_cnt; i++)
        {
            if (node->tag[i].type == NODE_ID_BROWSER || node->tag[i].type == NODE_ID_USER_DEFINE)
            {
                connect_cfg->tags[j] = &node->tag[i];
                j++;
            }
        }
    }

    return 0;
}

static void opc_ua_rcv(connect_config_t *connect_cfg)
{
    UA_SessionState ss;
    dev_node_t *node = NULL;
    dev_node_t *tmp = NULL;

    UA_Client_getState(connect_cfg->client, NULL, &ss, NULL);
    if (ss == UA_SESSIONSTATE_ACTIVATED)
    {
        if (!connect_cfg->browsered)
        {
            opc_ua_browser_recursion(connect_cfg, UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER));
            dy_syslog(LOG_DEBUG, "browser done");
            connect_cfg->browsered = true;
            opc_ua_update_connect_tags(connect_cfg);
        }
        pthread_rwlock_wrlock(&connect_cfg->data_lock);
        if (opc_ua_read_multi(connect_cfg) != UA_STATUSCODE_GOOD)
        {
            dy_syslog(LOG_ERR, "read multi failed");
        }
        else
        {
            list_for_each_entry_safe(node, tmp, &connect_cfg->used_node_list, used_list)
            {
                flush_device_status_by_sn(connect_cfg->var->ds, ACTION_RCV, node->sn);
            }
        }
        pthread_rwlock_unlock(&connect_cfg->data_lock);
    }
    else
    {
        if (connect_cfg->username && connect_cfg->password)
        {
            UA_Client_connectUsername(connect_cfg->client, connect_cfg->url, connect_cfg->username, connect_cfg->password);
        }
        else
        {
            UA_Client_connect(connect_cfg->client, connect_cfg->url);
        }
    }
    UA_Client_run_iterate(connect_cfg->client, 100);
}

static void opc_ua_rcv_loop(connect_config_t *connect_cfg)
{
    int ret = -1, maxfd, i;
    fd_set rset;
    struct timeval timeout;

    while (1)
    {
        SELECT_INIT();
        SELECT_ADD_FD(connect_cfg->report_timer);

        timeout.tv_usec = 0;
        timeout.tv_sec = 5;

        ret = select(maxfd + 1, &rset, 0, 0, &timeout);
        if (ret < 0)
        {
            int error = errno;
            dy_syslog(LOG_INFO, "errno %d\n", error);
            if (error == EINTR)
            {
                continue;
            }
            else
            {
                break;
            }
        }
        else if (ret > 0)
        {
            if (connect_cfg->report_timer > 0 && FD_ISSET(connect_cfg->report_timer, &rset))
            {
                FD_CLR(connect_cfg->report_timer, &rset);
                opc_ua_rcv(connect_cfg);
            }
        }
    }
}

static void *opc_ua_thread_loop(void *param)
{
    connect_config_t *connect_cfg = (connect_config_t *)param;

    connect_cfg->report_timer = my_timer_create();
    if (connect_cfg->report_timer > 0)
    {
        my_timer_set(connect_cfg->report_timer, 1, 1000);
    }

    opc_ua_rcv_loop(connect_cfg);
}

int opc_ua_start_thread(opc_ua_var_t *var, connect_config_t *connect_cfg)
{
    pthread_t thread_opc_ua;
    pthread_create(&thread_opc_ua, NULL, opc_ua_thread_loop, (void *)connect_cfg);
}
