/*******************************************************************************
 * Copyright (c) 2018 - 2019 fortiss GmbH
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Jose Cabral, Kirill Dorofeev - initial implementation
 *******************************************************************************/

#include "opcua_remote_handler.h"
#include "../../core/devexec.h"
#include "../../core/iec61131_functions.h"
#include "../../core/cominfra/basecommfb.h"
#include <criticalregion.h>
#include <forte_printer.h>

using namespace forte::com_infra;

DEFINE_HANDLER(COPC_UA_Remote_Handler);

COPC_UA_Client_IterationList::COPC_UA_Client_IterationList() :
    mNewClientsPresent(false) {
}

COPC_UA_Client_IterationList::~COPC_UA_Client_IterationList() {
}

void COPC_UA_Client_IterationList::startIterationThread() {
  if(!isAlive()) {
    start();
    mClientsThreadStarted.waitIndefinitely();
  }
}

void COPC_UA_Client_IterationList::stopIterationThread() {
  setAlive(false);
  resumeIterationLoop();
  end();
}

void COPC_UA_Client_IterationList::addClient(CUA_ClientInformation& paClientInformation) {
  CCriticalRegion iterationListRegion(mNewClientsMutex);
  addClientToList(paClientInformation, mNewClients);
  mNeedsIteration.inc();
  mNewClientsPresent = true;
}

void COPC_UA_Client_IterationList::removeClient(CUA_ClientInformation& paClientInformation) {
  CCriticalRegion iterationListRegion(mIterationClientsMutex);
  CCriticalRegion newClientsRegion(mNewClientsMutex);
  mNewClients.erase(&paClientInformation); //client could still be in the newList
  mIterationClients.erase(&paClientInformation);
}

void COPC_UA_Client_IterationList::run() {
  mClientsThreadStarted.inc();
  while(isAlive()) {

    if(mNewClientsPresent) {
      updateClientList();
    }
    bool needsRetry = handleClients();
    if(isAlive()) {
      if(needsRetry) {
        mNeedsIteration.timedWait(scmNanosecondsToSleep);
      } else {
        mNeedsIteration.waitIndefinitely();
      }
    }
  }
}

void COPC_UA_Client_IterationList::resumeIterationLoop() {
  mNeedsIteration.inc();
}

void COPC_UA_Client_IterationList::addClientToList(CUA_ClientInformation& paClientInformation, CSinglyLinkedList<CUA_ClientInformation*>& paList) {
  bool elementAlreadyPresent = false;
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = paList.begin(); itClientInformation != paList.end(); ++itClientInformation) {
    if(&paClientInformation == (*itClientInformation)) {
      elementAlreadyPresent = true;
      break;
    }
  }

  if(!elementAlreadyPresent) {
    paList.pushBack(&paClientInformation);
  }
}

void COPC_UA_Client_IterationList::updateClientList() {
  CCriticalRegion iterationListRegion(mIterationClientsMutex);
  CCriticalRegion newClientsRegion(mNewClientsMutex);
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mNewClients.begin(); itClientInformation != mNewClients.end();
      ++itClientInformation) {
    addClientToList(**itClientInformation, mIterationClients);
  }
  mNewClients.clearAll();
  mNewClientsPresent = false;
}

// ***************** CLIENT HANDLER ************* //

COPC_UA_Remote_Handler::COPC_UA_Remote_Handler(CDeviceExecution& paDeviceExecution) :
    COPC_UA_HandlerAbstract(paDeviceExecution), mConnectionHandler(*this) {
}

COPC_UA_Remote_Handler::~COPC_UA_Remote_Handler() {
  disableHandler();
  cleanResources();
}

void COPC_UA_Remote_Handler::enableHandler(void) {
  startIterationThread();
  mConnectionHandler.startIterationThread();
}

void COPC_UA_Remote_Handler::disableHandler(void) {
  mConnectionHandler.stopIterationThread();
  stopIterationThread();
}

UA_StatusCode COPC_UA_Remote_Handler::initializeAction(CActionInfo& paActionInfo) {
  enableHandler();
  UA_StatusCode retVal = UA_STATUSCODE_BADINTERNALERROR;
  switch(paActionInfo.getAction()){
    case CActionInfo::eRead:
    case CActionInfo::eWrite:
    case CActionInfo::eCallMethod:
    case CActionInfo::eSubscribe:
      retVal = getClientAndAddAction(paActionInfo);
      break;
    case CActionInfo::eCreateMethod:
    case CActionInfo::eCreateObject:
    case CActionInfo::eDeleteObject:
      DEVLOG_ERROR("[OPC UA REMOTE]: Cannot perform action %s remotely. Initialization failed\n", CActionInfo::mActionNames[paActionInfo.getAction()]);
      break;
    default:
      DEVLOG_ERROR("[OPC UA REMOTE]: Unknown action %d to be initialized\n", paActionInfo.getAction());
      break;
  }
  return retVal;
}

UA_StatusCode COPC_UA_Remote_Handler::executeAction(CActionInfo& paActionInfo) {

  CUA_ClientInformation *clientInfo = getClient(paActionInfo.getEndpoint());
  UA_StatusCode retVal = UA_STATUSCODE_BADINTERNALERROR;

  if(clientInfo && clientInfo->isActionInitialized(paActionInfo)) {
    switch(paActionInfo.getAction()){
      case CActionInfo::eRead:
        retVal = clientInfo->executeRead(paActionInfo);
        break;
      case CActionInfo::eWrite:
        retVal = clientInfo->executeWrite(paActionInfo);
        break;
      case CActionInfo::eCallMethod:
        retVal = clientInfo->executeCallMethod(paActionInfo);
        break;
      default: //eCreateMethod, eCreateObject, eDeleteObject will never reach here since they weren't initialized. eSubscribe is a Subscribe FB
        DEVLOG_ERROR("[OPC UA REMOTE]: Action %d to be executed is unknown or invalid\n", paActionInfo.getAction());
        break;
    }
  } else {
    DEVLOG_ERROR("[OPC UA REMOTE]: Cannot execute action from FB %s. It was not properly initialized\n",
      paActionInfo.getLayer().getCommFB()->getInstanceName());
  }

  if(UA_STATUSCODE_GOOD == retVal) {
    resumeIterationLoop();
  }

  return retVal;
}

UA_StatusCode COPC_UA_Remote_Handler::uninitializeAction(CActionInfo& paActionInfo) {
  UA_StatusCode retVal = UA_STATUSCODE_BADINTERNALERROR;
  switch(paActionInfo.getAction()){
    case CActionInfo::eRead:
    case CActionInfo::eWrite:
    case CActionInfo::eCallMethod:
    case CActionInfo::eSubscribe:
      removeActionFromClient(paActionInfo);
      retVal = UA_STATUSCODE_GOOD;
      break;
    default:
      DEVLOG_ERROR("[OPC UA REMOTE]: Action %d to be uninitialized is unknown or invalid\n", paActionInfo.getAction());
      break;
  }
  return retVal;
}

void COPC_UA_Remote_Handler::cleanResources() {
  CCriticalRegion criticalRegion(mAllClientListMutex);
  for(CSinglyLinkedList<CUA_ClientInformation*>::Iterator itClientInformation = mAllClients.begin(); itClientInformation != mAllClients.end();
      ++itClientInformation) {
    mConnectionHandler.removeClient(**itClientInformation);
    removeClient(**itClientInformation);
    delete *itClientInformation;
  }
  mAllClients.clearAll();
}

UA_StatusCode COPC_UA_Remote_Handler::getClientAndAddAction(CActionInfo& paActionInfo) {
  UA_StatusCode retVal = UA_STATUSCODE_BADINTERNALERROR;
  CUA_ClientInformation *clientInfo = getClient(paActionInfo.getEndpoint());
  if(clientInfo) {
    addActionToClient(paActionInfo);
    retVal = UA_STATUSCODE_GOOD;
  }
  return retVal;
}

CUA_ClientInformation* COPC_UA_Remote_Handler::getClient(const CIEC_STRING &paEndpoint) {
  CCriticalRegion allClientsRegion(mAllClientListMutex);

  CUA_ClientInformation *client = 0;
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mAllClients.begin(); itClientInformation != mAllClients.end();
      ++itClientInformation) {
    if((*itClientInformation)->getEndpoint() == paEndpoint) {
      client = (*itClientInformation);
      break;
    }
  }
  if(!client) {
    client = new CUA_ClientInformation(paEndpoint);
    if(client->configureClient()) {
      mAllClients.pushBack(client);
    } else {
      delete client;
      client = 0;
    }
  }

  return client;
}

void COPC_UA_Remote_Handler::addActionToClient(CActionInfo& paActionInfo) {
  CCriticalRegion allClientsRegion(mAllClientListMutex);
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mAllClients.begin(); itClientInformation != mAllClients.end();
      ++itClientInformation) {
    CCriticalRegion clientRegion((*itClientInformation)->getMutex());
    if((*itClientInformation)->getEndpoint() == paActionInfo.getEndpoint()) {
      (*itClientInformation)->addAction(paActionInfo);
      addClientToConnectionHandler(**itClientInformation);
      break;
    }
  }
}

void COPC_UA_Remote_Handler::removeActionFromClient(CActionInfo& paActionInfo) {
  CCriticalRegion allClientsRegion(mAllClientListMutex);

  CUA_ClientInformation *clientToDelete = 0;
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mAllClients.begin(); itClientInformation != mAllClients.end();
      ++itClientInformation) {
    CCriticalRegion clientRegion((*itClientInformation)->getMutex());
    if((*itClientInformation)->getEndpoint() == paActionInfo.getEndpoint()) {
      (*itClientInformation)->removeAction(paActionInfo);
      clientToDelete = *itClientInformation;
      break;
    }
  }

  if(clientToDelete && !clientToDelete->hasActions()) {
    removeClientFromAllLists(*clientToDelete);
  }
}

void COPC_UA_Remote_Handler::removeClientFromAllLists(CUA_ClientInformation& paClientInformation) {
  mConnectionHandler.removeClient(paClientInformation);
  removeClient(paClientInformation);
  mAllClients.erase(&paClientInformation);
  delete &paClientInformation;
}

void COPC_UA_Remote_Handler::addClientToConnectionHandler(CUA_ClientInformation& paClientInformation) {
  mConnectionHandler.addClient(paClientInformation);
}

bool COPC_UA_Remote_Handler::handleClients() {
  CCriticalRegion iterationCriticalRegion(mIterationClientsMutex); //this is needed because removing a client from the list could cause trouble
  CSinglyLinkedList<CUA_ClientInformation *> failedClients;
  bool asyncIsNeeded = false;
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mIterationClients.begin(); itClientInformation != mIterationClients.end();
      ++itClientInformation) {
    CCriticalRegion criticalRegionClienMutex((*itClientInformation)->getMutex());
    if((*itClientInformation)->isAsyncNeeded()) {
      if(!(*itClientInformation)->executeAsyncCalls()) {
        failedClients.pushBack(*itClientInformation);
      } else {
        asyncIsNeeded = (*itClientInformation)->isAsyncNeeded();
      }
    }
    if(!isAlive()) {
      break;
    }
  }

  if(isAlive() && !failedClients.isEmpty()) {
    for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = failedClients.begin(); itClientInformation != failedClients.end();
        ++itClientInformation) {
      DEVLOG_ERROR("[OPC UA REMOTE]: There was a problem checking remote %s.\n", (*itClientInformation)->getEndpoint().getValue());

      //we cannot use COPC_UA_Client_IterationList::remove here because it locks mIterationClientsMutex
      CCriticalRegion newClientsRegion(mNewClientsMutex);
      CCriticalRegion criticalRegionClienMutex((*itClientInformation)->getMutex());
      mIterationClients.erase(*itClientInformation);
      mNewClients.erase(*itClientInformation); //client could still be in the newList
      (*itClientInformation)->uninitializeClient(); //reset all in client and pass it back to the connection handler
      (*itClientInformation)->configureClient();
      addClientToConnectionHandler(**itClientInformation);
    }
  }
  return asyncIsNeeded;
}

//************************** CONECTION HANDLER ************** //

COPC_UA_Remote_Handler::UA_ConnectionHandler::UA_ConnectionHandler(COPC_UA_Remote_Handler& paClientHandler) :
    mClientHandler(paClientHandler) {
}

COPC_UA_Remote_Handler::UA_ConnectionHandler::~UA_ConnectionHandler() {
}

bool COPC_UA_Remote_Handler::UA_ConnectionHandler::handleClients() {
  CCriticalRegion clientsClientsRegion(mIterationClientsMutex);
  CSinglyLinkedList<CUA_ClientInformation *> clientsToRemove;
  bool needsRetry = false;
  for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = mIterationClients.begin(); itClientInformation != mIterationClients.end();
      ++itClientInformation) {
    CCriticalRegion clientRegion((*itClientInformation)->getMutex());
    if((*itClientInformation)->handleClientState()) {
      clientsToRemove.pushBack(*itClientInformation);
    } else {
      needsRetry = true;
    }
    if((*itClientInformation)->someActionWasInitialized()) {
      mClientHandler.addClient(**itClientInformation);
    }
    if(!isAlive()) {
      break;
    }
  }

  if(isAlive() && !clientsToRemove.isEmpty()) {
    for(CSinglyLinkedList<CUA_ClientInformation *>::Iterator itClientInformation = clientsToRemove.begin(); itClientInformation != clientsToRemove.end();
        ++itClientInformation) {
      mIterationClients.erase(*itClientInformation);
    }
  }
  return needsRetry;
}

