// Copyright 2018 The agentx authors
// Licensed under the LGPLv3 with static-linking exception.
// See LICENCE file for details.

package pdu

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"net"
	"time"

	"github.com/posteo/go-agentx/value"
)

// Variable defines the pdu varbind packet.
type Variable struct {
	Type  VariableType
	Name  ObjectIdentifier
	Value interface{}
}

// Set sets the variable.
func (v *Variable) Set(oid value.OID, t VariableType, value interface{}) {
	v.Name.SetIdentifier(oid)
	v.Type = t
	v.Value = value
}

// ByteSize returns the number of bytes, the binding would need in the encoded version.
func (v *Variable) ByteSize() int {
	bytes, err := v.MarshalBinary()
	if err != nil {
		panic(err)
	}
	return len(bytes)
}

// MarshalBinary returns the pdu packet as a slice of bytes.
func (v *Variable) MarshalBinary() ([]byte, error) {
	buffer := &bytes.Buffer{}

	binary.Write(buffer, binary.LittleEndian, &v.Type)
	buffer.WriteByte(0x00)
	buffer.WriteByte(0x00)

	nameBytes, err := v.Name.MarshalBinary()
	if err != nil {
		return nil, err
	}
	buffer.Write(nameBytes)

	switch v.Type {
	case VariableTypeInteger:
		value := v.Value.(int32)
		binary.Write(buffer, binary.LittleEndian, &value)
	case VariableTypeOctetString:
		octetString := &OctetString{Text: v.Value.(string)}
		octetStringBytes, err := octetString.MarshalBinary()
		if err != nil {
			return nil, err
		}
		buffer.Write(octetStringBytes)
	case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView:
		break
	case VariableTypeObjectIdentifier:
		targetOID, err := value.ParseOID(v.Value.(string))
		if err != nil {
			return nil, err
		}

		oi := &ObjectIdentifier{}
		oi.SetIdentifier(targetOID)
		oiBytes, err := oi.MarshalBinary()
		if err != nil {
			return nil, err
		}
		buffer.Write(oiBytes)
	case VariableTypeIPAddress:
		ip := v.Value.(net.IP)
		octetString := &OctetString{Text: string(ip)}
		octetStringBytes, err := octetString.MarshalBinary()
		if err != nil {
			return nil, err
		}
		buffer.Write(octetStringBytes)
	case VariableTypeCounter32, VariableTypeGauge32:
		value := v.Value.(uint32)
		binary.Write(buffer, binary.LittleEndian, &value)
	case VariableTypeTimeTicks:
		value := uint32(v.Value.(time.Duration).Seconds() * 100)
		binary.Write(buffer, binary.LittleEndian, &value)
	case VariableTypeOpaque:
		octetString := &OctetString{Text: string(v.Value.([]byte))}
		octetStringBytes, err := octetString.MarshalBinary()
		if err != nil {
			return nil, err
		}
		buffer.Write(octetStringBytes)
	case VariableTypeCounter64:
		value := v.Value.(uint64)
		binary.Write(buffer, binary.LittleEndian, &value)
	default:
		return nil, fmt.Errorf("unhandled variable type %s", v.Type)
	}

	return buffer.Bytes(), nil
}

// UnmarshalBinary sets the packet structure from the provided slice of bytes.
func (v *Variable) UnmarshalBinary(data []byte) error {
	buffer := bytes.NewBuffer(data)

	if err := binary.Read(buffer, binary.LittleEndian, &v.Type); err != nil {
		return err
	}
	offset := 4

	if err := v.Name.UnmarshalBinary(data[offset:]); err != nil {
		return err
	}
	offset += v.Name.ByteSize()

	switch v.Type {
	case VariableTypeInteger:
		value := int32(0)
		if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
			return err
		}
		v.Value = value
	case VariableTypeOctetString:
		octetString := &OctetString{}
		if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
			return err
		}
		v.Value = octetString.Text
	case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView:
		v.Value = nil
	case VariableTypeObjectIdentifier:
		oid := &ObjectIdentifier{}
		if err := oid.UnmarshalBinary(data[offset:]); err != nil {
			return err
		}
		v.Value = oid.GetIdentifier()
	case VariableTypeIPAddress:
		octetString := &OctetString{}
		if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
			return err
		}
		v.Value = net.IP(octetString.Text)
	case VariableTypeCounter32, VariableTypeGauge32:
		value := uint32(0)
		if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
			return err
		}
		v.Value = value
	case VariableTypeTimeTicks:
		value := uint32(0)
		if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
			return err
		}
		v.Value = time.Duration(value) * time.Second / 100
	case VariableTypeOpaque:
		octetString := &OctetString{}
		if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
			return err
		}
		v.Value = []byte(octetString.Text)
	case VariableTypeCounter64:
		value := uint64(0)
		if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
			return err
		}
		v.Value = value
	default:
		return fmt.Errorf("unhandled variable type %s", v.Type)
	}

	return nil
}

func (v *Variable) String() string {
	return fmt.Sprintf("(variable %s = %v)", v.Type, v.Value)
}