/*
Copyright (c) 2017 European Organization for Nuclear Research (CERN).
All rights reserved. This program and the accompanying materials
are made available under the terms of the GNU Public License v3.0
which accompanies this distribution, and is available at
http://www.gnu.org/licenses/gpl.html

Contributors:
    .European Organization for Nuclear Research    (CERN) - initial API and implementation
    .GSI Helmholtzzentrum für Schwerionenforschung (GSI)  - features and bugfixes
*/


#include <silecs-communication/interface/core/SilecsService.h>
#include <silecs-communication/interface/utility/XMLParser.h>
#include <silecs-communication/interface/equipment/SilecsPLC.h>
#include <silecs-communication/interface/equipment/SilecsBlock.h>
#include <silecs-communication/interface/equipment/SilecsRegister.h>
#include <silecs-communication/interface/core/SilecsAction.h>
#include <silecs-communication/interface/utility/SilecsException.h>
#include <silecs-communication/interface/utility/SilecsLog.h>
#include <silecs-communication/interface/utility/StringUtilities.h>
#include <silecs-communication/interface/equipment/PLCBlock.h>
#include <silecs-communication/interface/communication/SilecsConnection.h>
#include <silecs-communication/interface/equipment/PLCRegister.h>

namespace Silecs
{

PLCBlock::PLCBlock(const ElementXML& blockNode, long deviceInputAddress, long deviceOutputAddress, Connection& plcConnection_, const SilecsParamConfig& paramConfig) :
                Block(blockNode, plcConnection_),
                deviceInputAddress_(deviceInputAddress),
                deviceOutputAddress_(deviceOutputAddress),
                paramConfig_(paramConfig)
{
    auto& registerNodes = blockNode.getChildNodes();
    for (auto registerIter = registerNodes.begin(); registerIter != registerNodes.end(); registerIter++)
    {
        std::string registerName = registerIter->getAttribute("name");
        std::unique_ptr<Register> reg;
        switch (paramConfig.brandID)
        {
            // SIEMENS PLCs use Motorola memory convention (BigEndianRegister)
            case Siemens:
                reg = std::unique_ptr<Register>(new S7Register(*registerIter, paramConfig));
                break;
                // SCHNEIDER PLCs use Intel memory convention (LittleEndianRegister)
                // RABBIT microcontrollers use Intel memory convention (LittleEndianRegister)
            case Schneider:
            case Digi:
                reg = std::unique_ptr<Register>(new UnityRegister(*registerIter, paramConfig));
                break;
                // BECKHOFF PLCs use different memory convention depending on PLC type:
                // . CX model: same convention than Unity
                // . BC model: special convention for 64 bits data (float, ..): swap only the two 16bits words together (no bytes swap).
            case Beckhoff:
                reg = std::unique_ptr<Register>(new TwinCATRegister(*registerIter, paramConfig));
                break;
            default:
                throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PLC_MANUFACTURER, paramConfig.brand);

        }

        AccessArea accessArea = AccessArea::Memory;
        if (blockNode.hasAttribute("ioType")) // only IO-Blocks have this attribute
        {
            accessArea = Block::whichAccessArea(blockNode.getAttribute("ioType"));
        }

        reg->setAccessArea(accessArea);
        reg->setAccessType(Block::whichAccessType(blockNode.getName()));
        reg->setBlockName(blockNode.getAttribute("name"));
        registers_.emplace_back(std::move(reg));
    }

    // Create buffer for the block exchanges
    bufferSize_ = memSize_; //size of one block type (including alignements)
    pBuffer_ = (unsigned char*)calloc(bufferSize_, sizeof(unsigned char));
}

PLCBlock::~PLCBlock()
{
    //Remove the buffer
    if (pBuffer_ != NULL)
        free(pBuffer_);
    pBuffer_ = NULL;
}

int PLCBlock::recvBlockMode()
{
    unsigned long usedAddress = address_;
    unsigned long usedSize = memSize_;
    unsigned long usedDeviceOffset = deviceInputAddress_ * usedSize;

    // Overwrite device-block address, offset & size in case user wants to resize the block dynamically
    if (withCustomAttributes() == true)
    {
        usedAddress = customAddress_;
        usedSize = customSize_;
        usedDeviceOffset = customOffset_;
    }

    return readBlock(usedAddress, usedDeviceOffset, usedSize, (unsigned char*)pBuffer_);
}

int PLCBlock::recvDeviceMode()
{
    unsigned long usedDeviceAddress = deviceInputAddress_;
    unsigned long usedBlockAddress = address_;
    unsigned long usedSize = memSize_;

    // Overwrite device-block address, offset & size in case user wants to resize the block dynamically
    if (withCustomAttributes() == true)
    {
        usedDeviceAddress = customAddress_;
        usedBlockAddress = customOffset_;
        usedSize = customSize_;
    }
    return readBlock(usedDeviceAddress, usedBlockAddress, usedSize, (unsigned char*)pBuffer_);
}

int PLCBlock::sendBlockMode()
{
    unsigned long usedAddress = address_;
    unsigned long usedSize = memSize_;
    unsigned long usedDeviceOffset = deviceOutputAddress_ * usedSize;

    // Overwrite device-block address, offset & size in case user wants to resize the block dynamically
    if (withCustomAttributes() == true)
    {
        usedAddress = customAddress_;
        usedSize = customSize_;
        usedDeviceOffset = customOffset_;
    }
    return sendBlock(usedAddress, usedDeviceOffset, usedSize, (unsigned char*)pBuffer_);
}

int PLCBlock::sendDeviceMode()
{
    unsigned long usedDeviceAddress = deviceOutputAddress_;
    unsigned long usedBlockAddress = address_;
    unsigned long usedSize = memSize_;

    // Overwrite device-block address, offset & size in case user wants to resize the block dynamically
    if (withCustomAttributes() == true)
    {
        usedDeviceAddress = customAddress_;
        usedBlockAddress = customOffset_;
        usedSize = customSize_;
    }
    return sendBlock(usedDeviceAddress, usedBlockAddress, usedSize, (unsigned char*)pBuffer_);
}

int PLCBlock::readBlock(long address, unsigned long offset, unsigned long size, unsigned char* pBuffer)
{
    timeval tod; //Time-of-day for register time-stamping
    int errorCode = 0;

    try
    {
        // Try to open the connection, if it fails. Throw an exception. On success all the
        // PLC blocks will be updated this function will be called recursively within doOpen.
        // Once all blocks are updated continue from here.
        if (!plcConnection_.doOpen())
        {
            throw SilecsException{__FILE__, __LINE__, COMM_CONNECT_FAILURE};
        }
        switch (accessArea_)
        {
            case AccessArea::Digital:
                errorCode = plcConnection_.readDIO(address, offset, size, pBuffer);
                break;
            case AccessArea::Analog:
            {
                errorCode = plcConnection_.readAIO(address, offset, size, pBuffer);
                break;
            }
            case AccessArea::Memory:
            default:
            {
                errorCode = plcConnection_.readMemory(address, offset, size, pBuffer);
                break;
            }
        }

        if (plcConnection_.isConnected() && (errorCode == 0))
        { //Data have just been received: get time-of-day to time-stamp the registers
            gettimeofday(&tod, 0);

            importRegisters(pBuffer, tod);

            LOG_DELAY(RECV) << "done for block: " << name_;
        }
    }
    catch(const SilecsException& ex)
    {
        LOG(ERROR) << ex.what();
        LOG(ERROR) << "RecvAction (execute) for block " << name_ << " has failed.";
        return ex.getCode();
    }
    return errorCode;
}

int PLCBlock::sendBlock(long address, unsigned long offset, unsigned long size, unsigned char* pBuffer)
{
    int errorCode = 0;
    try
    {
        // Try to open the connection, if it fails. Throw an exception. On success all the
        // PLC blocks will be updated this function will be called recursively within doOpen.
        // Once all blocks are updated continue from here.
        if (!plcConnection_.doOpen())
        {
            throw SilecsException{__FILE__, __LINE__, COMM_CONNECT_FAILURE};
        }

        if (plcConnection_.isEnabled())
        {
            exportRegisters(pBuffer);
        }

        switch (accessArea_)
        {
            case AccessArea::Digital:
                errorCode = plcConnection_.writeDIO(address, offset, size, pBuffer);
                break;
            case AccessArea::Analog:
            {
                errorCode = plcConnection_.writeAIO(address, offset, size, pBuffer);
                break;
            }
            case AccessArea::Memory:
            default:
                errorCode = plcConnection_.writeMemory(address, offset, size, pBuffer);
                break;
        }

        if (SEND & Log::topics_)
        {
            if (plcConnection_.isEnabled())
            {
                Log(SEND).getLogDelay() << "done for block: " << name_;
            }
        }
    }
    catch(const SilecsException& ex)
    {
        LOG(ERROR) << ex.what();
        LOG(ERROR) << "SendAction (execute) failed";
        return ex.getCode();
    }
    return errorCode;
}

int PLCBlock::send()
{
    if (accessType_ == AccessType::Acquisition)
    {
        throw SilecsException(__FILE__, __LINE__, DATA_WRITE_ACCESS_TYPE_MISMATCH);
    }
    
    if (paramConfig_.protocolModeID == BlockMode)
    {
        return sendBlockMode();
    }
    else
    {
        return sendDeviceMode();
    }
}

int PLCBlock::receive()
{
    if (accessType_ == AccessType::Command)
    {
        throw SilecsException(__FILE__, __LINE__, DATA_READ_ACCESS_TYPE_MISMATCH);
    }

    if (paramConfig_.protocolModeID == BlockMode)
    {
        return recvBlockMode();
    }
    else
    {
        return recvDeviceMode();
    }
}

}