// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package contracts defines all channel messages structure.
package contracts

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/framework/docparser"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	logger "github.com/aws/amazon-ssm-agent/agent/log"
	messageContracts "github.com/aws/amazon-ssm-agent/agent/messageservice/contracts"
	"github.com/aws/amazon-ssm-agent/agent/messageservice/utils"
	"github.com/aws/amazon-ssm-agent/agent/times"
)

type IAgentMessage interface {
	Deserialize(log logger.T, input []byte) (err error)
	Serialize(log logger.T) (result []byte, err error)
	Validate() error
	ParseAgentMessage(context context.T, messagesOrchestrationRootDir string, instanceId string) (*contracts.DocumentState, error)
}

// AgentMessage represents a message for agent to send/receive. AgentMessage Message in MGS is equivalent to MDS' InstanceMessage.
// All agent messages are sent in this form to the MGS service.
type AgentMessage struct {
	HeaderLength   uint32
	MessageType    string
	SchemaVersion  uint32
	CreatedDate    uint64
	SequenceNumber int64
	Flags          uint64
	MessageId      uuid.UUID
	PayloadDigest  []byte
	PayloadType    uint32
	PayloadLength  uint32
	Payload        []byte
}

// HL - HeaderLength is a 4 byte integer that represents the header length.
// MessageType is a 32 byte UTF-8 string containing the message type.
// SchemaVersion is a 4 byte integer containing the message schema version number.
// CreatedDate is an 8 byte integer containing the message create epoch millis in UTC.
// SequenceNumber is an 8 byte integer containing the message sequence number for serialized message streams.
// Flags is an 8 byte unsigned integer containing a packed array of control flags:
//   Bit 0 is SYN - SYN is set (1) when the recipient should consider Seq to be the first message number in the stream
//   Bit 1 is FIN - FIN is set (1) when this message is the final message in the sequence.
// MessageId is a 40 byte UTF-8 string containing a random UUID identifying this message.
// Payload digest is a 32 byte containing the SHA-256 hash of the payload.
// Payload Type is a 4 byte integer containing the payload type.
// Payload length is an 4 byte unsigned integer containing the byte length of data in the Payload field.
// Payload is a variable length byte data.
//
// | HL|         MessageType           |Ver|  CD   |  Seq  | Flags |
// |         MessageId                     |           Digest              |PayType| PayLen|
// |         Payload      			|

const (
	AgentMessage_HLLength             = 4
	AgentMessage_MessageTypeLength    = 32
	AgentMessage_SchemaVersionLength  = 4
	AgentMessage_CreatedDateLength    = 8
	AgentMessage_SequenceNumberLength = 8
	AgentMessage_FlagsLength          = 8
	AgentMessage_MessageIdLength      = 16
	AgentMessage_PayloadDigestLength  = 32
	AgentMessage_PayloadTypeLength    = 4
	AgentMessage_PayloadLengthLength  = 4
)

const (
	AgentMessage_HLOffset             = 0
	AgentMessage_MessageTypeOffset    = AgentMessage_HLOffset + AgentMessage_HLLength
	AgentMessage_SchemaVersionOffset  = AgentMessage_MessageTypeOffset + AgentMessage_MessageTypeLength
	AgentMessage_CreatedDateOffset    = AgentMessage_SchemaVersionOffset + AgentMessage_SchemaVersionLength
	AgentMessage_SequenceNumberOffset = AgentMessage_CreatedDateOffset + AgentMessage_CreatedDateLength
	AgentMessage_FlagsOffset          = AgentMessage_SequenceNumberOffset + AgentMessage_SequenceNumberLength
	AgentMessage_MessageIdOffset      = AgentMessage_FlagsOffset + AgentMessage_FlagsLength
	AgentMessage_PayloadDigestOffset  = AgentMessage_MessageIdOffset + AgentMessage_MessageIdLength
	AgentMessage_PayloadTypeOffset    = AgentMessage_PayloadDigestOffset + AgentMessage_PayloadDigestLength
	AgentMessage_PayloadLengthOffset  = AgentMessage_PayloadTypeOffset + AgentMessage_PayloadTypeLength
	AgentMessage_PayloadOffset        = AgentMessage_PayloadLengthOffset + AgentMessage_PayloadLengthLength
)

const (
	documentContent  = "DocumentContent"
	runtimeConfig    = "runtimeConfig"
	cloudwatchPlugin = "aws:cloudWatch"
	properties       = "properties"
	parameters       = "Parameters"
)

// Deserialize deserializes the byte array into an AgentMessage message.
// * Payload is a variable length byte data.
// * | HL|         MessageType           |Ver|  CD   |  Seq  | Flags |
// * |         MessageId                     |           Digest              |PayType| PayLen|
// * |         Payload      			|
func (agentMessage *AgentMessage) Deserialize(log logger.T, input []byte) (err error) {
	agentMessage.MessageType, err = getString(log, input, AgentMessage_MessageTypeOffset, AgentMessage_MessageTypeLength)
	if err != nil {
		log.Errorf("Could not deserialize field MessageType with error: %v", err)
		return err
	}
	agentMessage.SchemaVersion, err = getUInteger(log, input, AgentMessage_SchemaVersionOffset)
	if err != nil {
		log.Errorf("Could not deserialize field SchemaVersion with error: %v", err)
		return err
	}
	agentMessage.CreatedDate, err = getULong(log, input, AgentMessage_CreatedDateOffset)
	if err != nil {
		log.Errorf("Could not deserialize field CreatedDate with error: %v", err)
		return err
	}
	agentMessage.SequenceNumber, err = getLong(log, input, AgentMessage_SequenceNumberOffset)
	if err != nil {
		log.Errorf("Could not deserialize field SequenceNumber with error: %v", err)
		return err
	}
	agentMessage.Flags, err = getULong(log, input, AgentMessage_FlagsOffset)
	if err != nil {
		log.Errorf("Could not deserialize field Flags with error: %v", err)
		return err
	}
	agentMessage.MessageId, err = getUuid(log, input, AgentMessage_MessageIdOffset)
	if err != nil {
		log.Errorf("Could not deserialize field MessageId with error: %v", err)
		return err
	}

	agentMessage.PayloadDigest, err = getBytes(log, input, AgentMessage_PayloadDigestOffset, AgentMessage_PayloadDigestLength)
	if err != nil {
		log.Errorf("Could not deserialize field PayloadDigest with error: %v", err)
		return err
	}
	agentMessage.PayloadType, err = getUInteger(log, input, AgentMessage_PayloadTypeOffset)
	if err != nil {
		log.Errorf("Could not deserialize field PayloadType with error: %v", err)
		return err
	}

	agentMessage.PayloadLength, err = getUInteger(log, input, AgentMessage_PayloadLengthOffset)

	headerLength, herr := getUInteger(log, input, AgentMessage_HLOffset)
	if herr != nil {
		log.Errorf("Could not deserialize field HeaderLength with error: %v", err)
		return err
	}

	agentMessage.HeaderLength = headerLength
	agentMessage.Payload = input[headerLength+AgentMessage_PayloadLengthLength:]

	return nil
}

// Serialize serializes AgentMessage message into a byte array.
// * Payload is a variable length byte data.
// * | HL|         MessageType           |Ver|  CD   |  Seq  | Flags |
// * |         MessageId                     |           Digest              |PayType| PayLen|
// * |         Payload      			|
func (agentMessage *AgentMessage) Serialize(log logger.T) (result []byte, err error) {
	payloadLength := uint32(len(agentMessage.Payload))
	headerLength := uint32(AgentMessage_PayloadLengthOffset)
	// If the payloadinfo length is incorrect, fix it.
	if payloadLength != agentMessage.PayloadLength {
		log.Debugf("Payload length will be adjusted: %v", agentMessage.PayloadLength)
		agentMessage.PayloadLength = payloadLength
	}

	totalMessageLength := headerLength + AgentMessage_PayloadLengthLength + payloadLength
	result = make([]byte, totalMessageLength)

	if err = putUInteger(log, result, AgentMessage_HLOffset, headerLength); err != nil {
		log.Errorf("Could not serialize HeaderLength with error: %v", err)
		return make([]byte, 1), err
	}

	startPosition := AgentMessage_MessageTypeOffset
	endPosition := AgentMessage_MessageTypeOffset + AgentMessage_MessageTypeLength - 1
	if err = putString(log, result, startPosition, endPosition, agentMessage.MessageType); err != nil {
		log.Errorf("Could not serialize MessageType with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putUInteger(log, result, AgentMessage_SchemaVersionOffset, agentMessage.SchemaVersion); err != nil {
		log.Errorf("Could not serialize SchemaVersion with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putULong(log, result, AgentMessage_CreatedDateOffset, agentMessage.CreatedDate); err != nil {
		log.Errorf("Could not serialize CreatedDate with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putLong(log, result, AgentMessage_SequenceNumberOffset, agentMessage.SequenceNumber); err != nil {
		log.Errorf("Could not serialize SequenceNumber with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putULong(log, result, AgentMessage_FlagsOffset, agentMessage.Flags); err != nil {
		log.Errorf("Could not serialize Flags with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putUuid(log, result, AgentMessage_MessageIdOffset, agentMessage.MessageId); err != nil {
		log.Errorf("Could not serialize MessageId with error: %v", err)
		return make([]byte, 1), err
	}

	hasher := sha256.New()
	hasher.Write(agentMessage.Payload)

	startPosition = AgentMessage_PayloadDigestOffset
	endPosition = AgentMessage_PayloadDigestOffset + AgentMessage_PayloadDigestLength - 1
	if err = putBytes(log, result, startPosition, endPosition, hasher.Sum(nil)); err != nil {
		log.Errorf("Could not serialize PayloadDigest with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putUInteger(log, result, AgentMessage_PayloadTypeOffset, agentMessage.PayloadType); err != nil {
		log.Errorf("Could not serialize PayloadType with error: %v", err)
		return make([]byte, 1), err
	}

	if err = putUInteger(log, result, AgentMessage_PayloadLengthOffset, agentMessage.PayloadLength); err != nil {
		log.Errorf("Could not serialize PayloadLength with error: %v", err)
		return make([]byte, 1), err
	}

	startPosition = AgentMessage_PayloadOffset
	endPosition = AgentMessage_PayloadOffset + int(payloadLength) - 1
	if err = putBytes(log, result, startPosition, endPosition, agentMessage.Payload); err != nil {
		log.Errorf("Could not serialize Payload with error: %v", err)
		return make([]byte, 1), err
	}

	return result, nil
}

// Validate returns error if the message is invalid
func (agentMessage *AgentMessage) Validate() error {
	if agentMessage.HeaderLength == 0 {
		return errors.New("HeaderLength cannot be zero")
	}
	if agentMessage.MessageType == "" {
		return errors.New("MessageType is missing")
	}
	if agentMessage.CreatedDate == 0 {
		return errors.New("CreatedDate is missing")
	}
	return nil
}

func (agentMessage *AgentMessage) ParseAgentMessage(context context.T, messagesOrchestrationRootDir string, instanceId string) (*contracts.DocumentState, error) {
	log := context.Log()

	log.Infof("Parsing AgentMessage %s, Payload: %s", agentMessage.MessageId, string(agentMessage.Payload))

	if agentMessage.MessageType == InteractiveShellMessage {
		docState, err := parseAgentTaskMessage(context, messagesOrchestrationRootDir, instanceId, agentMessage)
		if err != nil {
			return nil, err
		}
		return docState, nil
	} else if agentMessage.MessageType == AgentJobMessage {
		docState, err := parseAgentJobMessage(context, messagesOrchestrationRootDir, instanceId, agentMessage)
		if err != nil {
			return nil, err
		}
		return docState, nil
	} else if agentMessage.MessageType == ChannelClosedMessage {
		docState, err := parseTerminateSessionMessage(context, agentMessage, instanceId)
		if err != nil {
			return nil, err
		}
		return docState, nil
	}

	return nil, fmt.Errorf("invalid agentmessage type: %s", agentMessage.MessageType)
}

func (agentMessage *AgentMessage) GetAgentJobId(context context.T) (string, error) {
	log := context.Log()

	if agentMessage.MessageType == AgentJobMessage {
		log.Debugf("Getting AgentJobId.")

		var agentJobPayload AgentJobPayload
		err := json.Unmarshal(agentMessage.Payload, &agentJobPayload)
		if err != nil {
			log.Errorf("Could not deserialize AgentJob rawMessage: %v.", err)
			return "", err
		}
		return agentJobPayload.JobId, nil
	}

	return "", fmt.Errorf("invalid agentmessage type: %s", agentMessage.MessageType)
}

// parseAgentTaskMessage parses session message to documentState object for processor.
func parseAgentTaskMessage(context context.T,
	messagesOrchestrationRootDir string,
	instanceId string,
	agentMessage *AgentMessage) (*contracts.DocumentState, error) {

	log := context.Log()
	clientId := uuid.New().String()

	// parse message to retrieve parameters
	parsedMessagePayload, err := deserializeAgentTaskPayload(log, *agentMessage)
	if err != nil {
		errorMsg := fmt.Errorf("encountered error while parsing input - internal error %v", err)
		return nil, fmt.Errorf("%v", errorMsg)
	}

	log.Debugf("Receiving session id %s, clientId: %s", parsedMessagePayload.SessionId, clientId)
	log.Tracef("Processing start-session message %s", agentMessage.Payload)

	// adapt plugin configuration format from MGS to plugin expected format
	documentInfo := buildSessionDocumentInfo(*agentMessage, parsedMessagePayload.SessionId, parsedMessagePayload, instanceId)
	messageOrchestrationDirectory := filepath.Join(messagesOrchestrationRootDir, parsedMessagePayload.SessionId)

	parserInfo := docparser.DocumentParserInfo{
		OrchestrationDir: messageOrchestrationDirectory,
		MessageId:        documentInfo.MessageID,
		DocumentId:       documentInfo.DocumentID,
	}
	docContent := &docparser.SessionDocContent{
		SchemaVersion: parsedMessagePayload.DocumentContent.SchemaVersion,
		Description:   parsedMessagePayload.DocumentContent.Description,
		SessionType:   parsedMessagePayload.DocumentContent.SessionType,
		Inputs:        parsedMessagePayload.DocumentContent.Inputs,
		Parameters:    parsedMessagePayload.DocumentContent.Parameters,
		Properties:    parsedMessagePayload.DocumentContent.Properties,
	}

	docState, err := docparser.InitializeDocState(
		context,
		contracts.StartSession,
		docContent,
		documentInfo,
		parserInfo,
		parsedMessagePayload.Parameters)
	if err != nil {
		return nil, fmt.Errorf("error initialing document state: %s", err)
	}
	docState.UpstreamServiceName = contracts.MessageGatewayService
	log.Debugf("Docstate document ID after Initializing: %s", docState.DocumentInformation.DocumentID)
	return &docState, nil
}

// parseTerminateSessionMessage parses a channel closed AgentMessage to DocumentState object for processor
func parseTerminateSessionMessage(context context.T, agentMessage *AgentMessage, instanceId string) (*contracts.DocumentState, error) {
	log := context.Log()
	log.Debugf("Processing TerminateSession message %s", agentMessage.MessageId.String())

	channelClosed := &ChannelClosed{}
	if err := channelClosed.Deserialize(log, *agentMessage); err != nil {
		log.Errorf("Cannot parse AgentTask message to ChannelClosed message: %s, err: %v.", agentMessage.MessageId, err)
		return nil, err
	}
	log.Debugf("ChannelClosed message %s, sessionId %s", channelClosed.MessageId, channelClosed.SessionId)

	documentInfo := contracts.DocumentInfo{
		InstanceID:     instanceId,
		CreatedDate:    channelClosed.CreatedDate,
		MessageID:      channelClosed.SessionId,
		CommandID:      channelClosed.SessionId,
		DocumentID:     channelClosed.SessionId,
		RunID:          times.ToIsoDashUTC(times.DefaultClock.Now()),
		DocumentStatus: contracts.ResultStatusInProgress,
	}

	cancelSessionInfo := new(contracts.CancelCommandInfo)
	cancelSessionInfo.Payload = ""
	sessionId := channelClosed.SessionId
	cancelSessionInfo.CancelMessageID = sessionId
	cancelSessionInfo.CancelCommandID = sessionId
	cancelSessionInfo.DebugInfo = fmt.Sprintf("Session %v is yet to be terminated", sessionId)

	docState := contracts.DocumentState{
		DocumentInformation: documentInfo,
		CancelInformation:   *cancelSessionInfo,
		DocumentType:        contracts.TerminateSession,
		UpstreamServiceName: contracts.MessageGatewayService,
	}

	return &docState, nil
}

// parseAgentJobMessage parses run command SendCommand or CancelCommand to documentState object for processor.
func parseAgentJobMessage(context context.T, commandOrchestrationRootDir string, instanceId string, agentMessage *AgentMessage) (*contracts.DocumentState, error) {
	log := context.Log()

	if agentMessage.MessageType != AgentJobMessage {
		err := errors.New("AgentMessage is not of type agent_job")
		return nil, err
	}
	log.Debugf("Processing AgentJob Message: %s", agentMessage.MessageId)
	log.Debugf("AgentJobPayload: %s", string(agentMessage.Payload))

	var agentJobPayload AgentJobPayload
	err := json.Unmarshal(agentMessage.Payload, &agentJobPayload)
	if err != nil {
		log.Errorf("Could not deserialize AgentJob rawMessage: %v.", err)
		return nil, err
	}

	var docState *contracts.DocumentState
	if strings.HasPrefix(agentJobPayload.Topic, string(utils.SendCommandTopicPrefix)) {
		docState, err = parseAgentJobSendCommandMessage(context, agentJobPayload, commandOrchestrationRootDir, instanceId, *agentMessage)
		if err != nil {
			return nil, err
		}
	} else if strings.HasPrefix(agentJobPayload.Topic, string(utils.CancelCommandTopicPrefix)) {
		docState, err = parseAgentJobCancelCommandMessage(context, agentJobPayload, instanceId, *agentMessage)
		if err != nil {
			return nil, err
		}
	} else {
		errorMsg := "Unexpected AgentJob Topic: " + agentJobPayload.Topic
		log.Error(errorMsg)
		return nil, fmt.Errorf("%v", errorMsg)
	}

	return docState, nil
}

func parseAgentJobCancelCommandMessage(context context.T, agentJobPayload AgentJobPayload, instanceId string, agentMessage AgentMessage) (*contracts.DocumentState, error) {
	log := context.Log()
	log.Debug("Processing agent job cancel command message - ", agentJobPayload.JobId)
	log.Debug("Payload: ", jsonutil.Indent(agentJobPayload.Payload))
	message := toInstanceMessage(instanceId, agentMessage, agentJobPayload)
	return utils.ParseCancelCommandMessage(context, message, contracts.MessageGatewayService)
}

func parseAgentJobSendCommandMessage(context context.T, agentJobPayload AgentJobPayload, commandOrchestrationRootDir string, instanceId string, agentMessage AgentMessage) (*contracts.DocumentState, error) {
	log := context.Log()
	log.Debug("Processing agent job send command message jobId: ", agentJobPayload.JobId)
	log.Debug("Payload: ", jsonutil.Indent(agentJobPayload.Payload))
	message := toInstanceMessage(instanceId, agentMessage, agentJobPayload)
	return utils.ParseSendCommandMessage(context, message, commandOrchestrationRootDir, contracts.MessageGatewayService)
}

func toInstanceMessage(instanceId string, agentMessage AgentMessage, agentJobPayload AgentJobPayload) messageContracts.InstanceMessage {
	return messageContracts.InstanceMessage{
		CreatedDate: time.Unix(int64(agentMessage.CreatedDate), 0).String(),
		Destination: instanceId,
		MessageId:   agentJobPayload.JobId,
		Payload:     agentJobPayload.Payload,
		Topic:       agentJobPayload.Topic,
	}
}

// deserializeAgentTaskPayload parses agent task message payloads received.
func deserializeAgentTaskPayload(log logger.T, agentMessage AgentMessage) (agentTaskPayload AgentTaskPayload, err error) {
	if agentMessage.MessageType != InteractiveShellMessage {
		err = errors.New("AgentMessage is not of type interactive_shell")
		return
	}
	whatstring := string(agentMessage.Payload)
	log.Debug(whatstring)

	var mgsPayload MGSPayload
	if err = json.Unmarshal(agentMessage.Payload, &mgsPayload); err != nil {
		log.Errorf("Could not deserialize rawMessage: %s", string(agentMessage.Payload))
	}

	// workaround to unmarshal the real payload (should be fixed from the service side)
	if err = json.Unmarshal([]byte(mgsPayload.Payload), &agentTaskPayload); err != nil {
		log.Errorf("Could not deserialize AgentTask payload rawMessage: %v", err)
	}
	return
}

// buildSessionDocumentInfo builds new DocumentInfo object for Sessions
func buildSessionDocumentInfo(
	agentMessage AgentMessage,
	sessionId string,
	parsedMessagePayload AgentTaskPayload,
	instanceId string) contracts.DocumentInfo {

	// DocumentID is a unique name for file system
	// For Session, DocumentID = SessionID
	return contracts.DocumentInfo{
		CommandID:      sessionId,
		CreatedDate:    time.Unix(int64(agentMessage.CreatedDate), 0).String(),
		DocumentID:     sessionId,
		InstanceID:     instanceId,
		MessageID:      sessionId,
		RunID:          times.ToIsoDashUTC(times.DefaultClock.Now()),
		DocumentName:   parsedMessagePayload.DocumentName,
		DocumentStatus: contracts.ResultStatusInProgress,
		RunAsUser:      parsedMessagePayload.RunAsUser,
		SessionOwner:   parsedMessagePayload.SessionOwner,
	}
}

// getUuid gets the 128bit uuid from an array of bytes starting from the offset.
func getUuid(log logger.T, byteArray []byte, offset int) (result uuid.UUID, err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+16-1 > byteArrayLength-1 || offset < 0 {
		log.Error("getUuid failed: Offset is invalid.")
		return uuid.Nil, errors.New("Offset is outside the byte array.")
	}

	leastSignificantLong, err := getLong(log, byteArray, offset)
	if err != nil {
		log.Error("getUuid failed: failed to get uuid LSBs Long value.")
		return uuid.Nil, errors.New("Failed to get uuid LSBs long value.")
	}

	leastSignificantBytes, err := longToBytes(log, leastSignificantLong)
	if err != nil {
		log.Error("getUuid failed: failed to get uuid LSBs bytes value.")
		return uuid.Nil, errors.New("Failed to get uuid LSBs bytes value.")
	}

	mostSignificantLong, err := getLong(log, byteArray, offset+8)
	if err != nil {
		log.Error("getUuid failed: failed to get uuid MSBs Long value.")
		return uuid.Nil, errors.New("Failed to get uuid MSBs long value.")
	}

	mostSignificantBytes, err := longToBytes(log, mostSignificantLong)
	if err != nil {
		log.Error("getUuid failed: failed to get uuid MSBs bytes value.")
		return uuid.Nil, errors.New("Failed to get uuid MSBs bytes value.")
	}

	uuidBytes := append(mostSignificantBytes, leastSignificantBytes...)

	uuidFromBytes, _ := uuid.FromBytes(uuidBytes)

	return uuidFromBytes, nil
}

// putUuid puts the 128 bit uuid to an array of bytes starting from the offset.
func putUuid(log logger.T, byteArray []byte, offset int, input uuid.UUID) (err error) {
	if input == uuid.Nil {
		log.Error("putUuid failed: input is null.")
		return errors.New("putUuid failed: input is null.")
	}

	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+16-1 > byteArrayLength-1 || offset < 0 {
		log.Error("putUuid failed: Offset is invalid.")
		return errors.New("Offset is outside the byte array.")
	}

	leastSignificantLong, err := bytesToLong(log, input[8:16])
	if err != nil {
		log.Error("putUuid failed: Failed to get leastSignificant Long value.")
		return errors.New("Failed to get leastSignificant Long value.")
	}

	mostSignificantLong, err := bytesToLong(log, input[0:8])
	if err != nil {
		log.Error("putUuid failed: Failed to get mostSignificantLong Long value.")
		return errors.New("Failed to get mostSignificantLong Long value.")
	}

	if err = putLong(log, byteArray, offset, leastSignificantLong); err != nil {
		log.Error("putUuid failed: Failed to put leastSignificantLong Long value.")
		return errors.New("Failed to put leastSignificantLong Long value.")
	}

	if err = putLong(log, byteArray, offset+8, mostSignificantLong); err != nil {
		log.Error("putUuid failed: Failed to put mostSignificantLong Long value.")
		return errors.New("Failed to put mostSignificantLong Long value.")
	}

	return nil
}

// getBytes gets an array of bytes starting from the offset.
func getBytes(log logger.T, byteArray []byte, offset int, byteLength int) (result []byte, err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+byteLength-1 > byteArrayLength-1 || offset < 0 {
		log.Error("getBytes failed: Offset is invalid.")
		return make([]byte, byteLength), errors.New("Offset is outside the byte array.")
	}
	return byteArray[offset : offset+byteLength], nil
}

// putBytes puts bytes into the array at the correct offset.
func putBytes(log logger.T, byteArray []byte, offsetStart int, offsetEnd int, inputBytes []byte) (err error) {
	byteArrayLength := len(byteArray)
	if offsetStart > byteArrayLength-1 || offsetEnd > byteArrayLength-1 || offsetStart > offsetEnd || offsetStart < 0 {
		log.Error("putBytes failed: Offset is invalid.")
		return errors.New("Offset is outside the byte array.")
	}

	if offsetEnd-offsetStart+1 != len(inputBytes) {
		log.Error("putBytes failed: Not enough space to save the bytes.")
		return errors.New("Not enough space to save the bytes.")
	}

	copy(byteArray[offsetStart:offsetEnd+1], inputBytes)
	return nil
}

// getString get a string value from the byte array starting from the specified offset to the defined length.
func getString(log logger.T, byteArray []byte, offset int, stringLength int) (result string, err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+stringLength-1 > byteArrayLength-1 || offset < 0 {
		log.Error("getString failed: Offset is invalid.")
		return "", errors.New("Offset is outside the byte array.")
	}

	//remove nulls from the bytes array
	b := bytes.Trim(byteArray[offset:offset+stringLength], "\x00")

	return strings.TrimSpace(string(b)), nil
}

// putString puts a string value to a byte array starting from the specified offset.
func putString(log logger.T, byteArray []byte, offsetStart int, offsetEnd int, inputString string) (err error) {
	byteArrayLength := len(byteArray)
	if offsetStart > byteArrayLength-1 || offsetEnd > byteArrayLength-1 || offsetStart > offsetEnd || offsetStart < 0 {
		log.Error("putString failed: Offset is invalid.")
		return errors.New("Offset is outside the byte array.")
	}

	if offsetEnd-offsetStart+1 < len(inputString) {
		log.Error("putString failed: Not enough space to save the string.")
		return errors.New("Not enough space to save the string.")
	}

	// wipe out the array location first and then insert the new value.
	for i := offsetStart; i <= offsetEnd; i++ {
		byteArray[i] = ' '
	}

	copy(byteArray[offsetStart:offsetEnd+1], inputString)
	return nil
}

// getUInteger gets an unsigned integer
func getUInteger(log logger.T, byteArray []byte, offset int) (result uint32, err error) {
	var temp int32
	temp, err = getInteger(log, byteArray, offset)
	return uint32(temp), err
}

// putUInteger puts an unsigned integer
func putUInteger(log logger.T, byteArray []byte, offset int, value uint32) (err error) {
	return putInteger(log, byteArray, offset, int32(value))
}

// getULong gets an unsigned long integer
func getULong(log logger.T, byteArray []byte, offset int) (result uint64, err error) {
	var temp int64
	temp, err = getLong(log, byteArray, offset)
	return uint64(temp), err
}

// putULong puts an unsigned long integer.
func putULong(log logger.T, byteArray []byte, offset int, value uint64) (err error) {
	return putLong(log, byteArray, offset, int64(value))
}

// getLong gets a long integer value from a byte array starting from the specified offset. 64 bit.
func getLong(log logger.T, byteArray []byte, offset int) (result int64, err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+8 > byteArrayLength-1 || offset < 0 {
		log.Error("getLong failed: Offset is invalid.")
		return 0, errors.New("Offset is outside the byte array.")
	}
	return bytesToLong(log, byteArray[offset:offset+8])
}

// putLong puts a long integer value to a byte array starting from the specified offset.
func putLong(log logger.T, byteArray []byte, offset int, value int64) (err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+8 > byteArrayLength-1 || offset < 0 {
		log.Error("putLong failed: Offset is invalid.")
		return errors.New("Offset is outside the byte array.")
	}

	mbytes, err := longToBytes(log, value)
	if err != nil {
		log.Error("putLong failed: getBytesFromInteger Failed.")
		return err
	}

	copy(byteArray[offset:offset+8], mbytes)
	return nil
}

// getInteger gets an integer value from a byte array starting from the specified offset.
func getInteger(log logger.T, byteArray []byte, offset int) (result int32, err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+4 > byteArrayLength-1 || offset < 0 {
		log.Error("getInteger failed: Offset is invalid.")
		return 0, errors.New("Offset is bigger than the byte array.")
	}
	return bytesToInteger(log, byteArray[offset:offset+4])
}

// putInteger puts an integer value to a byte array starting from the specified offset.
func putInteger(log logger.T, byteArray []byte, offset int, value int32) (err error) {
	byteArrayLength := len(byteArray)
	if offset > byteArrayLength-1 || offset+4 > byteArrayLength-1 || offset < 0 {
		log.Error("putInteger failed: Offset is invalid.")
		return errors.New("Offset is outside the byte array.")
	}

	bytes, err := integerToBytes(log, value)
	if err != nil {
		log.Error("putInteger failed: getBytesFromInteger Failed.")
		return err
	}

	copy(byteArray[offset:offset+4], bytes)
	return nil
}

// bytesToLong gets a Long integer from a byte array.
func bytesToLong(log logger.T, input []byte) (result int64, err error) {
	var res int64
	inputLength := len(input)
	if inputLength != 8 {
		log.Error("bytesToLong failed: input array size is not equal to 8.")
		return 0, errors.New("Input array size is not equal to 8.")
	}
	buf := bytes.NewBuffer(input)
	binary.Read(buf, binary.BigEndian, &res)
	return res, nil
}

// longToBytes gets bytes array from a long integer.
func longToBytes(log logger.T, input int64) (result []byte, err error) {
	buf := new(bytes.Buffer)
	binary.Write(buf, binary.BigEndian, input)
	if buf.Len() != 8 {
		log.Error("longToBytes failed: buffer output length is not equal to 8.")
		return make([]byte, 8), errors.New("Input array size is not equal to 8.")
	}

	return buf.Bytes(), nil
}

// integerToBytes gets bytes array from an integer.
func integerToBytes(log logger.T, input int32) (result []byte, err error) {
	buf := new(bytes.Buffer)
	binary.Write(buf, binary.BigEndian, input)
	if buf.Len() != 4 {
		log.Error("integerToBytes failed: buffer output length is not equal to 4.")
		return make([]byte, 4), errors.New("Input array size is not equal to 4.")
	}

	return buf.Bytes(), nil
}

// bytesToInteger gets an integer from a byte array.
func bytesToInteger(log logger.T, input []byte) (result int32, err error) {
	var res int32
	inputLength := len(input)
	if inputLength != 4 {
		log.Error("bytesToInteger failed: input array size is not equal to 4.")
		return 0, errors.New("Input array size is not equal to 4.")
	}
	buf := bytes.NewBuffer(input)
	binary.Read(buf, binary.BigEndian, &res)
	return res, nil
}
