/*
 * 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 knxnetip

import (
	"context"
	"math"
	"runtime/debug"
	"strconv"
	"time"

	"github.com/pkg/errors"

	"github.com/apache/plc4x/plc4go/pkg/api/values"
	driverModel "github.com/apache/plc4x/plc4go/protocols/knxnetip/readwrite/model"
	"github.com/apache/plc4x/plc4go/spi/options"
	"github.com/apache/plc4x/plc4go/spi/utils"
	spiValues "github.com/apache/plc4x/plc4go/spi/values"
)

///////////////////////////////////////////////////////////////////////////////////////////////////////
// KNX Specific Operations used by the driver internally
//
// These functions all provide access to some of the internal KNX operations
// They provide this functionality to other parts of the KNX driver, which are
// not part of the PLC4Go API.
//
// Remarks about these functions:
// They expect the called private functions to handle timeouts, so these will not.
///////////////////////////////////////////////////////////////////////////////////////////////////////

func (m *Connection) ReadGroupAddress(ctx context.Context, groupAddress []byte, datapointType *driverModel.KnxDatapointType) <-chan KnxReadResult {
	result := make(chan KnxReadResult, 1)

	sendResponse := func(value values.PlcValue, numItems uint8, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxReadResult{
			value:    value,
			numItems: numItems,
			err:      err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		groupAddressReadResponse, err := m.sendGroupAddressReadRequest(ctx, groupAddress)
		if err != nil {
			sendResponse(nil, 0, errors.Wrap(err, "error reading group address"))
			return
		}

		var payload []byte
		// TODO: maybe groupAddressReadResponse.DataFirstByte can be written as uint 6 so the we wouldn't need to cast
		payload = append(payload, byte(groupAddressReadResponse.GetDataFirstByte()))
		payload = append(payload, groupAddressReadResponse.GetData()...)

		// Parse the response data.
		rb := utils.NewReadBufferByteBased(payload)
		// If the size of the tag is greater than 6, we have to skip the first byte
		if datapointType.DatapointMainType().SizeInBits() > 6 {
			_, _ = rb.ReadUint8("datapointType", 8)
		}
		// Set a default datatype if none is provided
		if *datapointType == driverModel.KnxDatapointType_DPT_UNKNOWN {
			defaultDatapointType := driverModel.KnxDatapointType_USINT
			datapointType = &defaultDatapointType
		}
		// Parse the value
		plcValue, err := driverModel.KnxDatapointParseWithBuffer(context.Background(), rb, *datapointType)
		if err != nil {
			sendResponse(nil, 0, errors.Wrap(err, "error parsing group address response"))
			return
		}

		// Return the value
		sendResponse(plcValue, 1, nil)
	}()

	return result
}

func (m *Connection) DeviceConnect(ctx context.Context, targetAddress driverModel.KnxAddress) <-chan KnxDeviceConnectResult {
	result := make(chan KnxDeviceConnectResult, 1)

	sendResponse := func(connection *KnxDeviceConnection, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxDeviceConnectResult{
			connection: connection,
			err:        err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		// If we're already connected, use that connection instead.
		if connection, ok := m.DeviceConnections[targetAddress]; ok {
			sendResponse(connection, nil)
			return
		}

		// First send a connection request
		controlConnectResponse, err := m.sendDeviceConnectionRequest(ctx, targetAddress)
		if err != nil {
			sendResponse(nil, errors.Wrap(err, "error creating device connection"))
			return
		}
		if controlConnectResponse == nil {
			sendResponse(nil, errors.New("error creating device connection"))
			return
		}

		// Create the new connection object.
		connection := &KnxDeviceConnection{
			counter: 0,
			// I was told this value on the knx-forum.
			// Seems the max payload is 3 bytes less ...
			maxApdu: 0, // This is the default max APDU Size
		}
		m.DeviceConnections[targetAddress] = connection

		// If the connection request was successful, try to read the device-descriptor
		deviceDescriptorResponse, err := m.sendDeviceDeviceDescriptorReadRequest(ctx, targetAddress)
		if err != nil {
			sendResponse(nil, errors.New(
				"error reading device descriptor: "+err.Error()))
			return
		}
		// Save the device-descriptor value
		deviceDescriptor := uint16(deviceDescriptorResponse.GetData()[0])<<8 | (uint16(deviceDescriptorResponse.GetData()[1]) & 0xFF)
		connection.deviceDescriptor = deviceDescriptor

		// Last, not least, read the max APDU size
		// If we were able to read the max APDU size, then use the minimum of
		// the connection APDU size and the device APDU size, otherwise use the
		// default APDU Size of 15
		// Defined in: 03_05_01 Resources v01.09.03 AS Page 40
		deviceApduSize := uint16(15)
		propertyValueResponse, err := m.sendDevicePropertyReadRequest(ctx, targetAddress, 0, 56, 1, 1)
		if err == nil {
			// If the count is 0, then this property doesn't exist or the user has no permission to read it.
			// In all other cases we expect the response to contain the value.
			if propertyValueResponse.GetCount() > 0 {
				dataLength := uint8(len(propertyValueResponse.GetData()))
				data := propertyValueResponse.GetData()
				ctxForModel := options.GetLoggerContextForModel(ctx, m.log, options.WithPassLoggerToModel(m.passLogToModel))
				plcValue, err := driverModel.KnxPropertyParse(ctxForModel, data,
					driverModel.KnxInterfaceObjectProperty_PID_DEVICE_MAX_APDULENGTH.PropertyDataType(), dataLength)

				// Return the result
				if err == nil {
					deviceApduSize = plcValue.GetUint16()
				} else {
					m.log.Debug().Err(err).Msg("Error parsing knx property")
				}
			}
		}

		// Set the max apdu size for this connection.
		connection.maxApdu = uint16(math.Min(float64(deviceApduSize), 240))

		sendResponse(connection, nil)
	}()

	return result
}

func (m *Connection) DeviceDisconnect(ctx context.Context, targetAddress driverModel.KnxAddress) <-chan KnxDeviceDisconnectResult {
	result := make(chan KnxDeviceDisconnectResult, 1)

	sendResponse := func(connection *KnxDeviceConnection, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxDeviceDisconnectResult{
			connection: connection,
			err:        err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		if connection, ok := m.DeviceConnections[targetAddress]; ok {
			_, err := m.sendDeviceDisconnectionRequest(ctx, targetAddress)

			// Remove the connection from the list.
			delete(m.DeviceConnections, targetAddress)

			sendResponse(connection, err)
		} else {
			sendResponse(connection, nil)
		}
	}()

	return result
}

func (m *Connection) DeviceAuthenticate(ctx context.Context, targetAddress driverModel.KnxAddress, buildingKey []byte) <-chan KnxDeviceAuthenticateResult {
	result := make(chan KnxDeviceAuthenticateResult, 1)

	sendResponse := func(err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxDeviceAuthenticateResult{
			err: err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		// Check if there is already a connection available,
		// if not, create a new one.
		connection, ok := m.DeviceConnections[targetAddress]
		if !ok {
			connections := m.DeviceConnect(ctx, targetAddress)
			deviceConnectionResult := <-connections
			// If we didn't get a connect, abort
			if deviceConnectionResult.err != nil {
				sendResponse(errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)))
			}
		}

		// If we successfully got a connection, read the property
		if connection == nil {
			sendResponse(errors.New("unable to connect to device"))
			return
		}
		authenticationLevel := uint8(0)
		authenticationResponse, err := m.sendDeviceAuthentication(ctx, targetAddress, authenticationLevel, buildingKey)
		if err == nil {
			if authenticationResponse.GetLevel() == authenticationLevel {
				sendResponse(nil)
			} else {
				// We authenticated correctly but not to the level requested.
				sendResponse(errors.Errorf("got error authenticating at device %s",
					KnxAddressToString(targetAddress)))
			}
		} else {
			sendResponse(errors.Errorf("got error authenticating at device %s", KnxAddressToString(targetAddress)))
		}
	}()

	return result
}

func (m *Connection) DeviceReadProperty(ctx context.Context, targetAddress driverModel.KnxAddress, objectId uint8, propertyId uint8, propertyIndex uint16, numElements uint8) <-chan KnxReadResult {
	result := make(chan KnxReadResult, 1)

	sendResponse := func(value values.PlcValue, numItems uint8, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxReadResult{
			value:    value,
			numItems: numItems,
			err:      err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		// Check if there is already a connection available,
		// if not, create a new one.
		connection, ok := m.DeviceConnections[targetAddress]
		if !ok {
			connections := m.DeviceConnect(ctx, targetAddress)
			deviceConnectionResult := <-connections
			// If we didn't get a connect, abort
			if deviceConnectionResult.err != nil {
				sendResponse(nil,
					0,
					errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
				)
			}
		}

		// If we successfully got a connection, read the property
		if connection == nil {
			sendResponse(nil, 0, errors.New("unable to connect to device"))
			return
		}
		propertyValueResponse, err := m.sendDevicePropertyReadRequest(ctx, targetAddress, objectId, propertyId, propertyIndex, numElements)
		if err != nil {
			sendResponse(nil, 0, err)
			return
		}

		// Find out the type of the property
		var objectType *driverModel.KnxInterfaceObjectType
		for curObjectType := driverModel.KnxInterfaceObjectType_OT_UNKNOWN; curObjectType <= driverModel.KnxInterfaceObjectType_OT_SUNBLIND_SENSOR_BASIC; curObjectType++ {
			if curObjectType.Code() == strconv.Itoa(int(objectId)) {
				objectType = &curObjectType
				break
			}
		}
		property := driverModel.KnxInterfaceObjectProperty_PID_UNKNOWN
		if objectType != nil {
			for curProperty := driverModel.KnxInterfaceObjectProperty_PID_UNKNOWN; curProperty <= driverModel.KnxInterfaceObjectProperty_PID_SUNBLIND_SENSOR_BASIC_ENABLE_TOGGLE_MODE; curProperty++ {
				if curProperty.PropertyId() == propertyId &&
					(curProperty.ObjectType() == driverModel.KnxInterfaceObjectType_OT_GENERAL ||
						curProperty.ObjectType() == *objectType) {
					property = curProperty
					break
				}
			}
		}

		dataLength := uint8(len(propertyValueResponse.GetData()))
		data := propertyValueResponse.GetData()
		ctxForModel := options.GetLoggerContextForModel(ctx, m.log, options.WithPassLoggerToModel(m.passLogToModel))
		plcValue, err := driverModel.KnxPropertyParse(ctxForModel, data, property.PropertyDataType(), dataLength)
		if err != nil {
			sendResponse(nil, 0, err)
		} else {
			sendResponse(plcValue, 1, err)
		}
	}()

	return result
}

func (m *Connection) DeviceReadPropertyDescriptor(ctx context.Context, targetAddress driverModel.KnxAddress, objectId uint8, propertyId uint8) <-chan KnxReadResult {
	result := make(chan KnxReadResult, 1)

	sendResponse := func(value values.PlcValue, numItems uint8, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxReadResult{
			value:    value,
			numItems: numItems,
			err:      err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		// Check if there is already a connection available,
		// if not, create a new one.
		connection, ok := m.DeviceConnections[targetAddress]
		if !ok {
			connections := m.DeviceConnect(ctx, targetAddress)
			deviceConnectionResult := <-connections
			// If we didn't get a connect, abort
			if deviceConnectionResult.err != nil {
				sendResponse(
					nil,
					0,
					errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
				)
			}
		}

		if connection == nil {
			sendResponse(nil, 0, errors.New("unable to connect to device"))
			return
		}
		// If we successfully got a connection, read the property
		propertyDescriptionResponse, err := m.sendDevicePropertyDescriptionReadRequest(ctx, targetAddress, objectId, propertyId)
		if err != nil {
			sendResponse(nil, 0, err)
			return
		}

		val := map[string]values.PlcValue{}
		val["writable"] = spiValues.NewPlcBOOL(propertyDescriptionResponse.GetWriteEnabled())
		val["dataType"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetPropertyDataType().Name())
		val["maxElements"] = spiValues.NewPlcUINT(propertyDescriptionResponse.GetMaxNrOfElements())
		val["readLevel"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetReadLevel().String())
		val["writeLevel"] = spiValues.NewPlcSTRING(propertyDescriptionResponse.GetWriteLevel().String())
		str := spiValues.NewPlcStruct(val)
		sendResponse(&str, 1, nil)
	}()

	return result
}

func (m *Connection) DeviceReadMemory(ctx context.Context, targetAddress driverModel.KnxAddress, address uint16, numElements uint8, datapointType *driverModel.KnxDatapointType) <-chan KnxReadResult {
	result := make(chan KnxReadResult, 1)

	sendResponse := func(value values.PlcValue, numItems uint8, err error) {
		timeout := time.NewTimer(10 * time.Millisecond)
		select {
		case result <- KnxReadResult{
			value:    value,
			numItems: numItems,
			err:      err,
		}:
			if !timeout.Stop() {
				<-timeout.C
			}
		case <-timeout.C:
			timeout.Stop()
		}
	}

	m.wg.Add(1)
	go func() {
		defer m.wg.Done()
		defer func() {
			if err := recover(); err != nil {
				m.log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
		}()
		// Set a default datatype, if none is specified
		if datapointType == nil {
			dpt := driverModel.KnxDatapointType_USINT
			datapointType = &dpt
		}

		// Check if there is already a connection available,
		// if not, create a new one.
		connection, ok := m.DeviceConnections[targetAddress]
		if !ok {
			connections := m.DeviceConnect(ctx, targetAddress)
			deviceConnectionResult := <-connections
			// If we didn't get a connect, abort
			if deviceConnectionResult.err != nil {
				sendResponse(
					nil,
					0,
					errors.Wrapf(deviceConnectionResult.err, "error connecting to device at: %s", KnxAddressToString(targetAddress)),
				)
			}
		}

		if connection == nil {
			// TODO: do we need to send a response here
			return
		}
		// If we successfully got a connection, read the property
		// Depending on the gateway Max APDU and the device Max APDU, split this up into multiple requests.
		// An APDU starts with the last 6 bits of the first data byte containing the count
		// followed by the 16-bit address, so these are already used.
		elementSize := datapointType.DatapointMainType().SizeInBits() / 8
		remainingRequestElements := numElements
		curStartingAddress := address
		var results []values.PlcValue
		for remainingRequestElements > 0 {
			// As the maxApdu can change, we have to do this in the loop.
			maxNumBytes := uint8(math.Min(float64(connection.maxApdu-3), float64(63)))
			maxNumElementsPerRequest := uint8(math.Floor(float64(maxNumBytes / elementSize)))
			numElements := uint8(math.Min(float64(remainingRequestElements), float64(maxNumElementsPerRequest)))
			numBytes := numElements * uint8(math.Max(float64(1), float64(datapointType.DatapointMainType().SizeInBits()/8)))
			memoryReadResponse, err := m.sendDeviceMemoryReadRequest(ctx, targetAddress, curStartingAddress, numBytes)
			if err != nil {
				// TODO: do we need to send a response here
				return
			}

			// If the number of bytes read is less than expected,
			// Update the connection.maxApdu value. This is required
			// as some devices seem to be sending back less than the
			// number of bytes specified than the maxApdu.
			if uint8(len(memoryReadResponse.GetData())) < numBytes {
				connection.maxApdu = uint16(len(memoryReadResponse.GetData()) + 3)
			}

			// Parse the data according to the property type information
			rb := utils.NewReadBufferByteBased(memoryReadResponse.GetData())
			for rb.HasMore(datapointType.DatapointMainType().SizeInBits()) {
				plcValue, err := driverModel.KnxDatapointParseWithBuffer(context.Background(), rb, *datapointType)
				// Return the result
				if err != nil {
					sendResponse(nil, 0, err)
					return
				}
				results = append(results, plcValue)

				// Update the counters and addresses.
				remainingRequestElements--
				curStartingAddress = curStartingAddress + uint16(elementSize)
			}
			// If there are still remaining bytes, keep them for the next time.
		}
		if len(results) > 1 {
			var plcList values.PlcValue
			plcList = spiValues.NewPlcList(results)
			sendResponse(plcList, 1, nil)
		} else if len(results) == 1 {
			sendResponse(results[0], 1, nil)
		}
	}()

	return result
}
