//-----------------------------------------------------------------------------
// Copyright 2019 Thiago Alves
// This file is part of the OpenPLC Software Stack.
//
// OpenPLC is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// OpenPLC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with OpenPLC.  If not, see <http://www.gnu.org/licenses/>.
//------
//
// This file has all the EtherNet/IP functions supported by the OpenPLC. If any
// other function is to be added to the project, it must be added here
// Hannah Hanback, Sep 2019
//-----------------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <string.h>

#include "ladder.h"
#include "enipStruct.h"	//This header file contains necessary structs for enip.cpp

#define ENIP_MIN_LENGTH     28

thread_local unsigned char enip_session[4];	// opens thread for session

using namespace std;


//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Unknown
//-----------------------------------------------------------------------------
int getEnipType(struct enip_data_Unknown *enipDataUnknown, struct enip_header *header)
{	

		if (header->command[0] == 65 || header->command[0] == 0x70)
		{
			// ENIP Type Unnecessary for command execution
			// 0x65 --- register session
			return 0;
		}
		else if (enipDataUnknown->item1_id[0] == 0x81)
		{
			// PCCC type 1 - Unknown
			return 1;
		}
		else if (enipDataUnknown->item1_data[0] == 0xb2 && enipDataUnknown->item2_length[1] == 0x4b) // There is an offset of the bytes within the 
		{																								   // Unconnected and Connected type data
			// PCCC type 2 - Unconnected Data Item														   // that is accounted for to check enipType														   // This means the labels "item1_data and item2_length
			return 2;																					   // do not contain what is stated but what it would be for the correct type
		}																								  
		else if (enipDataUnknown->item1_data[0] == 0xb2 && ( (enipDataUnknown->item2_length[1]==0x54) || (enipDataUnknown->item2_length[1]==0x4e) ) )	//0x54 opens connection
		{																																			    //0x4e closes connection
			// PCCC type 3 - Connected Data Item																										
			return 3;
		}
		else if (enipDataUnknown->item1_id[0] == 0xa1)
		{
			// PCCC type 3 - Connected for 0x70 command SEND UNIT DATA	
			return 3;
		}
		else
		{
			// Unknown type ID
			// Respond with error_code
			return -1;
		}
}


//-----------------------------------------------------------------------------
// Obtains the Length in the Header Length as variable type uint16_t
// used to know the length of CIP object data
//-----------------------------------------------------------------------------
uint16_t get_HeaderLength(struct enip_header *header)
{	
	uint16_t dataLength = ((uint16_t)header->length[1] << 8) | (uint16_t)header->length[0];
	return dataLength;
}


//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength(struct enip_data_Unknown *enipDataUnknown)
{	
	uint16_t dataLength = ((uint16_t)enipDataUnknown->item2_length[1] << 8) | (uint16_t)enipDataUnknown->item2_length[0];
	return dataLength;
}


//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Unconnected
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength_Unconnected(struct enip_data_Unconnected *enipDataUnconnected)
{	
	uint16_t dataLength = ((uint16_t)enipDataUnconnected->item2_length[1] << 8) | (uint16_t)enipDataUnconnected->item2_length[0];
	return dataLength;
}


//-----------------------------------------------------------------------------
// Obtains the Length in Item2_Length as variable type uint16_t
// used to know the length of pccc data within Item2_Data
// ENIP Type: Connected_0x70
//-----------------------------------------------------------------------------
uint16_t get_Item2_DataLength_Connected_0x70(struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{	
	uint16_t dataLength = ((uint16_t)enipDataConnected_0x70->item2_length[1] << 8) | (uint16_t)enipDataConnected_0x70->item2_length[0];
	return dataLength;
}


//-----------------------------------------------------------------------------
// Parses the Header information into struct
//-----------------------------------------------------------------------------  
int parseEnipHeader(unsigned char *buffer, int buffer_size, struct enip_header *header, struct enip_data_Unknown *enipDataUnknown)
{	
    //verify if message is big enough
    if (buffer_size < ENIP_MIN_LENGTH)
        return -1;
    
    header->command = &buffer[0];
    header->length = &buffer[2];
    header->session_handle = &buffer[4];
    header->status = &buffer[8];
    header->sender_context = &buffer[12];
    header->options = &buffer[20];
    header->data = &buffer[24];
    
    uint16_t enip_data_size = ((uint16_t)header->length[1] << 8) | (uint16_t)header->length[0];
	
    //verify if buffer_size matches enip_data_size
    if (buffer_size - 24 < enip_data_size)
        return -1;

    return enip_data_size;
}


//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: UNKNOWN
//-----------------------------------------------------------------------------
void parseEnipUnknown(unsigned char *buffer, struct enip_data_Unknown *enipDataUnknown)
{
	enipDataUnknown->interface_handle = &buffer[24];
	enipDataUnknown->timeout = &buffer[28];
	enipDataUnknown->item_count = &buffer[30];
	
	enipDataUnknown->item1_id = &buffer[32];
	enipDataUnknown->item1_length = &buffer[34];
	enipDataUnknown->item1_data = &buffer[36];
	
	enipDataUnknown->item2_id = &buffer[37];
	enipDataUnknown->item2_length = &buffer[39];
	enipDataUnknown->item2_data = &buffer[41];
}


//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: Unconnected
//-----------------------------------------------------------------------------
void parseEnipUnconnected(unsigned char *buffer, struct enip_data_Unconnected *enipDataUnconnected)
{
	enipDataUnconnected->interface_handle = &buffer[24];
    enipDataUnconnected->timeout = &buffer[28];
    enipDataUnconnected->item_count = &buffer[30];
	
	enipDataUnconnected->item1_id = &buffer[32];
	enipDataUnconnected->item1_length = &buffer[34];
	
	enipDataUnconnected->item2_id = &buffer[36];
	enipDataUnconnected->item2_length = &buffer[38];
	
	enipDataUnconnected->service = &buffer[40];   //0x4b (Request)
	enipDataUnconnected->request_pathSize = &buffer[41];//[1]
	enipDataUnconnected->request_path = &buffer[42];//[4]
	enipDataUnconnected->requestor_idLength = &buffer[46];//[1]
	enipDataUnconnected->vendor_id = &buffer[47];//[2]
	enipDataUnconnected->serial_number = &buffer[49];//[4]
	enipDataUnconnected->data = &buffer[53];
}


//-----------------------------------------------------------------------------
// Parses the ENIP data from the buffer
// ENIP Type: Connected
//-----------------------------------------------------------------------------
void parseEnipConnected(unsigned char *buffer, struct enip_data_Connected *enipDataConnected)
{
	enipDataConnected->interface_handle = &buffer[24];//[4]
    enipDataConnected->timeout = &buffer[28];//[2]
    enipDataConnected->item_count = &buffer[30];//[2]
	
	enipDataConnected->item1_id = &buffer[32];//[2]
	enipDataConnected->item1_length = &buffer[34];//[2]
	
	enipDataConnected->item2_id = &buffer[36];//[2]
	enipDataConnected->item2_length = &buffer[38];//[2]
	
	enipDataConnected->service = &buffer[40];//[1]   0x4b (Request)
	enipDataConnected->request_pathSize = &buffer[41];//[1] -----------size in words
	enipDataConnected->request_path = &buffer[42];//[4]
	enipDataConnected->actual_timeout = &buffer[46];//[2]
	enipDataConnected->o2t_netConnectID = &buffer[48];//[4]
	enipDataConnected->t2o_netConnectID = &buffer[52];//[4]
	enipDataConnected->connect_serialNo = &buffer[56];//[2]
	enipDataConnected->orig_vendorNo = &buffer[58];//[2]
	enipDataConnected->orig_serialNo = &buffer[60];//[4]
	enipDataConnected->timeout_multiplier = &buffer[64];//[1]
	enipDataConnected->reserved = &buffer[65];//[3]
	enipDataConnected->o2t_rpi = &buffer[68];//[4]
	enipDataConnected->o2t_netConnectParam = &buffer[72];//[2]
	enipDataConnected->t2o_rpi = &buffer[74];//[4]
	enipDataConnected->t2o_netConnectParam = &buffer[78];//[2]
	enipDataConnected->transport_trigger = &buffer[80];//[1]
	enipDataConnected->connection_pathSize = &buffer[81];//[1] ----- size in words
	enipDataConnected->connection_path = &buffer[82];//[?]
}


void parseEnipDataConnected_0x70(unsigned char *buffer, struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
	enipDataConnected_0x70->interface_handle = &buffer[24];
	enipDataConnected_0x70->timeout = &buffer[28];
	enipDataConnected_0x70->item_count = &buffer[30];
	
	enipDataConnected_0x70->item1_id = &buffer[32];
	enipDataConnected_0x70->item1_length = &buffer[34];
	enipDataConnected_0x70->connection_id = &buffer[36];
	
	enipDataConnected_0x70->item2_id = &buffer[40];
	enipDataConnected_0x70->item2_length = &buffer[42];
	enipDataConnected_0x70->sequence_count = &buffer[44];
	
	enipDataConnected_0x70->service = &buffer[46];
	enipDataConnected_0x70->request_pathSize = &buffer[47];
	enipDataConnected_0x70->request_path = &buffer[48];
	enipDataConnected_0x70->requestor_id = &buffer[52];
	enipDataConnected_0x70->pcccData = &buffer[59];
}


//-----------------------------------------------------------------------------
// Registers a ENIP Session
// Command Code: 0x65
//-----------------------------------------------------------------------------  
int registerEnipSession(struct enip_header *header)
{	
    unsigned char r[4];
    srand((unsigned)time(NULL));

    for (int i = 0; i < 4; ++i) 
        r[i] = rand();
    
    memcpy(header->session_handle, &r[0], 4);
    memcpy(&enip_session[0], &r[0], 4);
    
    return ENIP_MIN_LENGTH;
}


//-----------------------------------------------------------------------------
// SendRRData
// Receives a PCCC msg and Responds
// Command Code: 0x65
//-----------------------------------------------------------------------------  
int sendRRData(int enipType, struct enip_header *header, struct enip_data_Unknown *enipDataUnknown, struct enip_data_Unconnected *enipDataUnconnected, struct enip_data_Connected *enipDataConnected)
{
	if (enipType == 1)
	{	

		//writeDataContents(enipDataUnknown);

		uint16_t currentHeaderLength = get_HeaderLength(header); // get length of current stored size
		uint16_t currentPcccSize = get_Item2_DataLength(enipDataUnknown); // get length of stored pccc size
	
		//change timeout value
		enipDataUnknown->timeout[0] = 0x00;
		enipDataUnknown->timeout[1] = 0x04;
		
		//get pointer to beginning of pccc data to be passed
		unsigned char* pcccData = enipDataUnknown->item2_data;
	
		//send pccc Data to pccc.cpp to be parsed and craft response
		// returns the new PCCC data size
		uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
		if (newPcccSize == -1)
			return -1;	//error in PCCC.cpp
	
		//change enipDataUnknown->item2_length to match new pccc data size
		enipDataUnknown->item2_length[0] = newPcccSize & 0xFF;
		enipDataUnknown->item2_length[1] = newPcccSize >> 8;
		
		//calculate new header length size
		uint16_t len = currentHeaderLength - (currentPcccSize - newPcccSize);
	
		//change header->length to match new enip data size
		header->length[0] = len & 0xFF;
		header->length[1] = len >> 8;
		
		//calculate total size of enip response message in bytes
		uint16_t messageSize = len + 24;
	
		return messageSize; // total message size in bytes
	}
	else if (enipType == 2)
	{	
		//change timeout value
		enipDataUnconnected->timeout[0] = 0x00;
		enipDataUnconnected->timeout[1] = 0x04;
		
		uint16_t currentHeaderLength = get_HeaderLength(header); // get length of current stored size
		uint16_t currentItem2Size = get_Item2_DataLength_Unconnected(enipDataUnconnected); // get length of stored pccc size
	
		//get pointer to beginning of pccc data to be passed
		unsigned char* pcccData = enipDataUnconnected->data;
	
		//send pccc Data to pccc.cpp to be parsed and craft response
		// returns the new PCCC data size
		uint16_t newPcccSize = processPCCCMessage(pcccData, currentItem2Size - 13); // get length of new pccc size
		if (newPcccSize == -1)
			return -1;	//error in PCCC.cpp
		
		//item2_length is the length of the PCCC Data + 11 (11 for number of bytes after item2_length excluding PCCC Data)
		uint16_t newItem2Size = 11 + newPcccSize;
		
		//change enipDataUnconnected->item2_length to match new pccc data size
		enipDataUnconnected->item2_length[0] = newItem2Size & 0xFF;
		enipDataUnconnected->item2_length[1] = newItem2Size >> 8;
		
		//calculate new header length size
		uint16_t newHeaderLength = currentHeaderLength - (currentItem2Size - newItem2Size);
		
		//change header->length to match new enip data size
		header->length[0] = newHeaderLength & 0xFF;
		header->length[1] = newHeaderLength >> 8;
		
		//change service 0x4b to 0xcb
		enipDataUnconnected->service[0] = 0xcb;
		
		//change request path to 0
		enipDataUnconnected->request_pathSize[0] = 0x00;
		enipDataUnconnected->request_path[0] = 0x00;
		enipDataUnconnected->request_path[1] = 0x00;
		
		//move data forward
		memmove(&enipDataUnconnected->request_path[2], enipDataUnconnected->requestor_idLength, newPcccSize + 7);//11);
		
		//obtain total size of response message in bytes
		uint16_t messageSize = newHeaderLength + 24; // 24 is the static header size
		
		return messageSize; // total message size in bytes
	}
	else if (enipType == 3)
	{
		//change timeout value
		enipDataConnected->timeout[0] = 0x00;
		enipDataConnected->timeout[1] = 0x04;
		
		//change item2_length value (always 30?)
		enipDataConnected->item2_length[0] = 0x1e;
		enipDataConnected->item2_length[1] = 0x00;
		
		//change service response  0x54->0xd4
		enipDataConnected->service[0] = 0xd4;
		
		//change request path to 0
		enipDataConnected->request_pathSize[0] = 0x00;
		enipDataConnected->request_path[0] = 0x00;
		enipDataConnected->request_path[1] = 0x00;
		
		// change o2t_netConnectID
		enipDataConnected->request_path[2] = 0x5a;
		enipDataConnected->request_path[3] = 0xf0;
		enipDataConnected->actual_timeout[0] = 0xb8;
		enipDataConnected->actual_timeout[1] = 0x01;
		
		// start at the back and move up forward
		
		// overwrite t2o_netConnectParam with response of reserved 0x00 00
		enipDataConnected->t2o_netConnectParam[0] = 0x00;
		enipDataConnected->t2o_netConnectParam[1] = 0x00;
		
		// move up to overwrite o2t_netConnectParam
		memmove(&enipDataConnected->o2t_netConnectParam[0], enipDataConnected->t2o_rpi, 6); //6 = 80841e00 00 00 
		
		// move to overwrite timeout multiplier
		memmove(&enipDataConnected->timeout_multiplier[0], enipDataConnected->o2t_rpi, 10);//10 = 80841e00 80841e00 0000
		
		// move to overwrite o2t_netConnectID
		memmove(&enipDataConnected->o2t_netConnectID[0], enipDataConnected->t2o_netConnectID, 22);
		
		//change length inside header
		header->length[0] = 0x2e;
		header->length[1] = 0x00;
	
		//calculate total size of response message in bytes
			// this will be length from header +24
			// uint16_t enip_dataSize = len + 24;
		uint16_t messageSize = 70;
		
		return messageSize;
		
	}
	else
	{
		// log error to openPLC
		return -1;
	}
	
	
}


//-----------------------------------------------------------------------------
// SendUnitData
// Receives a PCCC msg and Responds
// Command Code: 0x70
//-----------------------------------------------------------------------------  
int sendUnitData(struct enip_header *header, struct enip_data_Connected_0x70 *enipDataConnected_0x70)
{
	//change the service response 0x4b -> 0xcb
	enipDataConnected_0x70->service[0] = 0xcb;
	
	//overwrite request path
	enipDataConnected_0x70->request_pathSize[0] = 0x00;
	enipDataConnected_0x70->request_path[0] = 0x00;
	enipDataConnected_0x70->request_path[1] = 0x00;
	
	//get pointer to beginning of pccc data to be passed
	unsigned char* pcccData = enipDataConnected_0x70->pcccData;
	
	int masked_write_check = 0;
	int write_check = 0;
	int write_check_2 = 0;
	if(pcccData[4] == 0xab)
	{
		masked_write_check = pcccData[5]; //Byte Size
	}
	
	if(pcccData[4] == 0xaa)
	{
		//write_check = 2;
		write_check = 2;
	}
	if(pcccData[4] == 0xa2 && pcccData[7] == 0x8a)
	{
		write_check_2 = -6;
	}
	//if(pcccData[4] == 0xa2 && pcccData[7] == 0x89)
	//{
		//write_check_2 = -2;
	//}
	
	//get the current Item2_Length
	uint16_t currentItem2Size = get_Item2_DataLength_Connected_0x70(enipDataConnected_0x70);
	
	//calculate the currentPcccSize
	uint16_t currentPcccSize = abs(currentItem2Size - 15);
	
	//send pccc Data to pccc.cpp to be parsed and craft response
	// returns the new PCCC data size
	uint16_t newPcccSize = processPCCCMessage(pcccData, currentPcccSize);
	if (newPcccSize == -1)
		return -1;	//error in PCCC.cpp
		
	//calculate Data Sizes
	uint16_t newItem2Size = newPcccSize + 13;
	uint16_t newHeaderSize = newPcccSize + newItem2Size + 14 + masked_write_check + write_check + write_check_2; // Use to be '+ 16' uint16_t newHeaderSize = newPcccSize + newItem2Size + 16;
	
	//change item2_length to match new cip data size
	enipDataConnected_0x70->item2_length[0] = newItem2Size & 0xFF;
	enipDataConnected_0x70->item2_length[1] = newItem2Size >> 8;
	
	//change header->length to match new enip data size
	header->length[0] = newHeaderSize & 0xFF;
	header->length[1] = newHeaderSize >> 8;
	
	//move data forward
	memmove(&enipDataConnected_0x70->request_path[2], enipDataConnected_0x70->requestor_id, newPcccSize + 7);
	
	//calculate total size of enip response message in bytes
	uint16_t messageSize = newHeaderSize + 24;
	
	return messageSize; // total message size in bytes
}


//-----------------------------------------------------------------------------
// This function must parse and process the client request and write back the
// response for it. The return value is the size of the response message in
// bytes.
//-----------------------------------------------------------------------------
int processEnipMessage(unsigned char *buffer, int buffer_size)
{	
	// initialize logging system
	unsigned char log_msg[1000];
    unsigned char *p = log_msg;
	
	// initailize structs
    struct enip_header header;
	struct enip_data_Unknown enipDataUnknown;
	struct enip_data_Unconnected enipDataUnconnected;
	struct enip_data_Connected enipDataConnected;
	struct enip_data_Connected_0x70 enipDataConnected_0x70;

    if (parseEnipHeader(buffer, buffer_size, &header, &enipDataUnknown) < 0)
	{
        return -1;
	}
	
	parseEnipUnknown(buffer, &enipDataUnknown);

	// Register a Session
    if (header.command[0] == 0x65)	
        return registerEnipSession(&header);

	if (header.command[0] == 0x70)	// Send Unit Data ---> works with Connected Type
	{
		parseEnipDataConnected_0x70(buffer, &enipDataConnected_0x70);
		uint16_t size = sendUnitData(&header, &enipDataConnected_0x70);
		return size; //sendUnitData()
	}


	//writeDataContents(&enipDataUnknown);
    
	// select Enip type-----------------------------
	// 1 = UNKNOWN
	// 2 = Unconnected
	// 3 = Connected
	// -1 = ERROR: unsupported enip type------------
	int enipType = getEnipType(&enipDataUnknown, &header);
	
	if (enipType == 2)
	{
		parseEnipUnconnected(buffer, &enipDataUnconnected);
	}
	else if (enipType == 3)
	{
		parseEnipConnected(buffer, &enipDataConnected);
	}
	else if (enipType < 0)
	{
		// log UNKNOWN Enip Type message to open plc 
		sprintf(log_msg, "ENIP: Received unsupported EtherNet/IP Type\n");
		log(log_msg);
	}
	
	//writeDataContents(&enipDataUnknown);
	
    //if (header.command[0] == 0x65)	// Register a Session
      //  return registerEnipSession(&header);
	
    if (header.command[0] == 0x6f)	// Send RR Data
	{
		//writeDataContents(&enipDataUnknown);
		uint16_t size = sendRRData(enipType, &header, &enipDataUnknown, &enipDataUnconnected, &enipDataConnected);
		return size;
	}
	/*else if (header.command[0] == 0x70)	// Send Unit Data ---> works with Connected Type
	{
		parseEnipDataConnected_0x70(buffer, &enipDataConnected_0x70);
		uint16_t size = sendUnitData(&header, &enipDataConnected_0x70);
		return size; //sendUnitData()
	}*/
    else
    {
        p += sprintf(p, "Unknown EtherNet/IP request: ");
        for (int i = 0; i < buffer_size; i++)
        {
            p += sprintf(p, "%02x ", (unsigned char)buffer[i]);
        }
        p += sprintf(p, "\n");
        log(log_msg);
        
        return -1;
    }
}
