/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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 ads

import (
	"context"
	"encoding/binary"
	"fmt"
	"runtime/debug"
	"strings"

	"github.com/pkg/errors"

	"github.com/apache/plc4x/plc4go/internal/ads/model"
	apiModel "github.com/apache/plc4x/plc4go/pkg/api/model"
	apiValues "github.com/apache/plc4x/plc4go/pkg/api/values"
	driverModel "github.com/apache/plc4x/plc4go/protocols/ads/readwrite/model"
	spiModel "github.com/apache/plc4x/plc4go/spi/model"
	"github.com/apache/plc4x/plc4go/spi/utils"
)

func (m *Connection) WriteRequestBuilder() apiModel.PlcWriteRequestBuilder {
	return spiModel.NewDefaultPlcWriteRequestBuilder(m.GetPlcTagHandler(), m.GetPlcValueHandler(), m)
}

func (m *Connection) Write(ctx context.Context, writeRequest apiModel.PlcWriteRequest) <-chan apiModel.PlcWriteRequestResult {
	m.log.Trace().Msg("Writing")
	result := make(chan apiModel.PlcWriteRequestResult, 1)
	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Errorf("panic-ed %v. Stack: %s", err, debug.Stack()))
			}
		}()
		if len(writeRequest.GetTagNames()) <= 1 {
			m.singleWrite(ctx, writeRequest, result)
		} else {
			m.multiWrite(ctx, writeRequest, result)
		}
	}()
	return result
}

func (m *Connection) singleWrite(ctx context.Context, writeRequest apiModel.PlcWriteRequest, result chan apiModel.PlcWriteRequestResult) {
	if len(writeRequest.GetTagNames()) != 1 {
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.New("this part of the ads driver only supports single-item requests"))
		m.log.Debug().Int("nTags", len(writeRequest.GetTagNames())).Msg("this part of the ads driver only supports single-item requests. Got nTags tags")
		return
	}

	// Here we can be sure that we're only handling a single request.
	tagName := writeRequest.GetTagNames()[0]
	tag := writeRequest.GetTag(tagName)
	if model.NeedsResolving(tag) {
		adsField, err := model.CastToSymbolicPlcTagFromPlcTag(tag)
		if err != nil {
			result <- spiModel.NewDefaultPlcWriteRequestResult(
				writeRequest,
				nil,
				errors.Wrap(err, "invalid tag item type"),
			)
			m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
			return
		}
		// Replace the symbolic tag with a direct one
		tag, err = m.resolveSymbolicTag(ctx, adsField)
		if err != nil {
			result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "invalid tag item type"))
			m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
			return
		}
	}
	directAdsTag, ok := tag.(*model.DirectPlcTag)
	if !ok {
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.New("invalid tag item type"))
		m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
		return
	}

	// Get the value from the request and serialize it to a byte array
	value := writeRequest.GetValue(tagName)
	io := utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
	err := m.serializePlcValue(directAdsTag.DataType, directAdsTag.GetArrayInfo(), value, io)
	if err != nil {
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "error serializing plc value"))
		return
	}
	data := io.GetBytes()

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Errorf("panic-ed %v. Stack: %s", err, debug.Stack()))
			}
		}()
		response, err := m.ExecuteAdsWriteRequest(ctx, directAdsTag.IndexGroup, directAdsTag.IndexOffset, data)
		if err != nil {
			result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "got error executing the write request"))
			return
		}

		if response.GetErrorCode() != 0x00000000 {
			// TODO: Handle this ...
		}

		responseCodes := map[string]apiModel.PlcResponseCode{}
		if response.GetErrorCode() != 0x00000000 {
			// TODO: Correctly handle this.
			responseCodes[tagName] = apiModel.PlcResponseCode_REMOTE_ERROR
		} else {
			responseCodes[tagName] = apiModel.PlcResponseCode_OK
		}
		// Return the response to the caller.
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, spiModel.NewDefaultPlcWriteResponse(writeRequest, responseCodes), nil)
	}()
}

func (m *Connection) multiWrite(ctx context.Context, writeRequest apiModel.PlcWriteRequest, result chan apiModel.PlcWriteRequestResult) {
	// Calculate the size of all tags together.
	// Calculate the expected size of the response data.
	expectedResponseDataSize := uint32(0)
	directAdsTags := map[string]*model.DirectPlcTag{}
	requestItems := make([]driverModel.AdsMultiRequestItem, 0)
	io := utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.LittleEndian))
	for _, tagName := range writeRequest.GetTagNames() {
		tag := writeRequest.GetTag(tagName)
		if model.NeedsResolving(tag) {
			adsField, err := model.CastToSymbolicPlcTagFromPlcTag(tag)
			if err != nil {
				result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "invalid tag item type"))
				m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
				return
			}
			// Replace the symbolic tag with a direct one
			tag, err = m.resolveSymbolicTag(ctx, adsField)
			if err != nil {
				result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "invalid tag item type"))
				m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
				return
			}
		}
		directAdsTag, ok := tag.(*model.DirectPlcTag)
		if !ok {
			result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.New("invalid tag item type"))
			m.log.Debug().Type("tag", tag).Msg("Invalid tag item type")
			return
		}

		directAdsTags[tagName] = directAdsTag

		// Serialize the plc value
		err := m.serializePlcValue(directAdsTag.DataType, directAdsTag.GetArrayInfo(), writeRequest.GetValue(tagName), io)
		if err != nil {
			result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "error serializing plc value"))
			return
		}

		// Size of one element.
		size := directAdsTag.DataType.GetSize()

		// Calculate how many elements in total we'll be reading.
		arraySize := uint32(1)
		if len(tag.GetArrayInfo()) > 0 {
			for _, arrayInfo := range tag.GetArrayInfo() {
				arraySize = arraySize * arrayInfo.GetSize()
			}
		}

		// Status code + payload size
		expectedResponseDataSize += 4

		requestItems = append(requestItems, driverModel.NewAdsMultiRequestItemWrite(
			directAdsTag.IndexGroup, directAdsTag.IndexOffset, size*arraySize))
	}

	response, err := m.ExecuteAdsReadWriteRequest(ctx,
		uint32(driverModel.ReservedIndexGroups_ADSIGRP_MULTIPLE_WRITE), uint32(len(directAdsTags)),
		expectedResponseDataSize, requestItems, io.GetBytes())
	if err != nil {
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, errors.Wrap(err, "error executing multi-item write request"))
		return
	}

	if response.GetResult() != driverModel.ReturnCode_OK {
		result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, nil, fmt.Errorf("got return result %s from remote", response.GetResult().String()))
		return
	}

	rb := utils.NewReadBufferByteBased(response.GetData(), utils.WithByteOrderForReadBufferByteBased(binary.LittleEndian))

	// Read in the response codes first.
	responseCodes := map[string]apiModel.PlcResponseCode{}
	for _, tagName := range writeRequest.GetTagNames() {
		returnCodeValue, err := rb.ReadUint32("returnCode", 32)
		if err != nil {
			responseCodes[tagName] = apiModel.PlcResponseCode_INTERNAL_ERROR
		} else if returnCodeValue != 0x00000000 {
			// TODO: Correctly handle this.
			responseCodes[tagName] = apiModel.PlcResponseCode_REMOTE_ERROR
		} else {
			responseCodes[tagName] = apiModel.PlcResponseCode_OK
		}
	}

	// Return the response to the caller.
	result <- spiModel.NewDefaultPlcWriteRequestResult(writeRequest, spiModel.NewDefaultPlcWriteResponse(writeRequest, responseCodes), nil)
}

func (m *Connection) serializePlcValue(dataType driverModel.AdsDataTypeTableEntry, arrayInfo []apiModel.ArrayInfo, plcValue apiValues.PlcValue, wb utils.WriteBufferByteBased) error {
	// Decode the data according to the information from the request
	// Based on the AdsDataTypeTableEntry in tag.DataType() parse the data
	if len(arrayInfo) > 0 {
		// This is an Array/List type.
		curArrayInfo := arrayInfo[0]
		// Do some initial checks
		if !plcValue.IsList() {
			return fmt.Errorf("expecting a plc value of type list")
		}
		plcValues := plcValue.GetList()
		if uint32(len(plcValues)) != curArrayInfo.GetSize() {
			return fmt.Errorf("expecting exactly %d items in the list", len(plcValues))
		}

		arrayItemTypeName := dataType.GetDataTypeName()[strings.Index(dataType.GetDataTypeName(), " OF ")+4:]
		arrayItemType, ok := m.driverContext.dataTypeTable[arrayItemTypeName]
		if !ok {
			return fmt.Errorf("couldn't resolve array item type %s", arrayItemTypeName)
		}

		for _, plcValue := range plcValues {
			restArrayInfo := arrayInfo[1:]
			err := m.serializePlcValue(arrayItemType, restArrayInfo, plcValue, wb)
			if err != nil {
				return errors.Wrap(err, "error encoding list item")
			}
		}
		return nil
	} else if len(dataType.GetChildren()) > 0 {
		// Do some initial checks
		if !plcValue.IsStruct() {
			return fmt.Errorf("expecting a plc value of type struct")
		}
		plcValues := plcValue.GetStruct()
		if len(plcValues) != len(dataType.GetChildren()) {
			return fmt.Errorf("expecting exactly %d children in struct, but got %d",
				len(plcValues), len(dataType.GetChildren()))
		}

		// This is a Struct type.
		startPos := uint32(wb.GetPos())
		curPos := uint32(0)
		for _, child := range dataType.GetChildren() {
			childName := child.GetPropertyName()
			childDataType, ok := m.driverContext.dataTypeTable[child.GetDataTypeName()]
			if !ok {
				return fmt.Errorf("couldn't find data type named %s for property %s of type %s", child.GetDataTypeName(), childName, dataType.GetDataTypeName())
			}
			if child.GetOffset() > curPos {
				skipBytes := child.GetOffset() - curPos
				for i := uint32(0); i < skipBytes; i++ {
					_ = wb.WriteByte("", 0x00)
				}
			}
			var childArrayInfo []apiModel.ArrayInfo
			for _, adsArrayInfo := range childDataType.GetArrayInfo() {
				childArrayInfo = append(childArrayInfo, &spiModel.DefaultArrayInfo{
					LowerBound: adsArrayInfo.GetLowerBound(),
					UpperBound: adsArrayInfo.GetUpperBound(),
				})
			}
			err := m.serializePlcValue(childDataType, childArrayInfo, plcValues[childName], wb)
			if err != nil {
				return errors.Wrap(err, fmt.Sprintf("error parsing propery %s of type %s", childName, dataType.GetDataTypeName()))
			}
			curPos = uint32(wb.GetPos()) - startPos
		}
		return nil
	} else {
		// This is a primitive type.
		valueType, stringLength := m.getPlcValueForAdsDataTypeTableEntry(dataType)
		if valueType == apiValues.NULL {
			return errors.New(fmt.Sprintf("error converting %s into plc4x plc-value type", dataType.GetDataTypeName()))
		}
		adsValueType, ok := apiValues.PlcValueTypeByName(valueType.String())
		if !ok {
			return errors.New(fmt.Sprintf("error converting plc4x plc-value type %s into ads plc-value type", valueType.String()))
		}
		return driverModel.DataItemSerializeWithWriteBuffer(context.Background(), wb, plcValue, adsValueType, stringLength)
	}
}
