// Copyright 2018-2019 opcua authors. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.

package ua

import (
	"fmt"
	"math"
	"reflect"
	"time"

	"github.com/gopcua/opcua/debug"
	"github.com/gopcua/opcua/errors"
)

// debugCodec enables printing of debug messages in the opcua codec.
var debugCodec = debug.FlagSet("codec")

// BinaryEncoder is the interface implemented by an object that can
// marshal itself into a binary OPC/UA representation.
type BinaryEncoder interface {
	Encode() ([]byte, error)
}

var binaryEncoder = reflect.TypeOf((*BinaryEncoder)(nil)).Elem()

func isBinaryEncoder(val reflect.Value) bool {
	return val.Type().Implements(binaryEncoder)
}

func Encode(v interface{}) ([]byte, error) {
	val := reflect.ValueOf(v)
	return encode(val, val.Type().String())
}

func encode(val reflect.Value, name string) ([]byte, error) {
	if debugCodec {
		fmt.Printf("encode: %s has type %s and is a %s\n", name, val.Type(), val.Type().Kind())
	}

	buf := NewBuffer(nil)
	switch {
	case isBinaryEncoder(val):
		v := val.Interface().(BinaryEncoder)
		return v.Encode()

	case isTime(val):
		buf.WriteTime(val.Interface().(time.Time))

	default:
		switch val.Kind() {
		case reflect.Bool:
			buf.WriteBool(val.Bool())
		case reflect.Int8:
			buf.WriteInt8(int8(val.Int()))
		case reflect.Uint8:
			buf.WriteUint8(uint8(val.Uint()))
		case reflect.Int16:
			buf.WriteInt16(int16(val.Int()))
		case reflect.Uint16:
			buf.WriteUint16(uint16(val.Uint()))
		case reflect.Int32:
			buf.WriteInt32(int32(val.Int()))
		case reflect.Uint32:
			buf.WriteUint32(uint32(val.Uint()))
		case reflect.Int64:
			buf.WriteInt64(int64(val.Int()))
		case reflect.Uint64:
			buf.WriteUint64(uint64(val.Uint()))
		case reflect.Float32:
			buf.WriteFloat32(float32(val.Float()))
		case reflect.Float64:
			buf.WriteFloat64(float64(val.Float()))
		case reflect.String:
			buf.WriteString(val.String())
		case reflect.Ptr:
			if val.IsNil() {
				return nil, nil
			}
			return encode(val.Elem(), name)
		case reflect.Struct:
			return writeStruct(val, name)
		case reflect.Slice:
			return writeSlice(val, name)
		default:
			return nil, errors.Errorf("unsupported type: %s", val.Type())
		}
	}
	return buf.Bytes(), buf.Error()
}

func writeStruct(val reflect.Value, name string) ([]byte, error) {
	var buf []byte
	valt := val.Type()
	for i := 0; i < val.NumField(); i++ {
		ft := valt.Field(i)
		fname := name + "." + ft.Name
		b, err := encode(val.Field(i), fname)
		if err != nil {
			return nil, err
		}
		buf = append(buf, b...)
	}
	return buf, nil
}

func writeSlice(val reflect.Value, name string) ([]byte, error) {
	buf := NewBuffer(nil)
	if val.IsNil() {
		buf.WriteUint32(null)
		return buf.Bytes(), buf.Error()
	}

	if val.Len() > math.MaxInt32 {
		return nil, errors.Errorf("array too large")
	}

	buf.WriteUint32(uint32(val.Len()))

	// fast path for []byte
	if val.Type().Elem().Kind() == reflect.Uint8 {
		// fmt.Println("[]byte fast path")
		buf.Write(val.Bytes())
		return buf.Bytes(), buf.Error()
	}

	// loop over elements
	for i := 0; i < val.Len(); i++ {
		ename := fmt.Sprintf("%s[%d]", name, i)
		b, err := encode(val.Index(i), ename)
		if err != nil {
			return nil, err
		}
		buf.Write(b)
	}
	return buf.Bytes(), buf.Error()
}
