//  Copyright (c) 2020 Cisco and/or its affiliates.
//
//  Licensed 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:
//
//      http://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 binapigen

import (
	"fmt"
	"strings"

	"github.com/sirupsen/logrus"
)

// define api
const (
	defineApiPrefix = "vl_api_"
	defineApiSuffix = "_t"
)

// toApiType returns name that is used as type reference in VPP binary API
func toApiType(name string) string {
	return defineApiPrefix + name + defineApiSuffix
}

func fromApiType(typ string) string {
	name := typ
	name = strings.TrimPrefix(name, defineApiPrefix)
	name = strings.TrimSuffix(name, defineApiSuffix)
	return name
}

const (
	U8     = "u8"
	I8     = "i8"
	U16    = "u16"
	I16    = "i16"
	U32    = "u32"
	I32    = "i32"
	U64    = "u64"
	I64    = "i64"
	F64    = "f64"
	BOOL   = "bool"
	STRING = "string"
)

var BaseTypeSizes = map[string]int{
	U8:     1,
	I8:     1,
	U16:    2,
	I16:    2,
	U32:    4,
	I32:    4,
	U64:    8,
	I64:    8,
	F64:    8,
	BOOL:   1,
	STRING: 1,
}

var BaseTypesGo = map[string]string{
	U8:     "uint8",
	I8:     "int8",
	U16:    "uint16",
	I16:    "int16",
	U32:    "uint32",
	I32:    "int32",
	U64:    "uint64",
	I64:    "int64",
	F64:    "float64",
	BOOL:   "bool",
	STRING: "string",
}

func fieldActualType(field *Field) (actual string) {
	switch {
	case field.TypeAlias != nil:
		actual = field.TypeAlias.Type
	case field.TypeEnum != nil:
		actual = field.TypeEnum.Type
	default:
		actual = field.Type
	}
	return
}

func fieldGoType(g *GenFile, field *Field) string {
	switch {
	case field.TypeAlias != nil:
		return g.GoIdent(field.TypeAlias.GoIdent)
	case field.TypeEnum != nil:
		return g.GoIdent(field.TypeEnum.GoIdent)
	case field.TypeStruct != nil:
		return g.GoIdent(field.TypeStruct.GoIdent)
	case field.TypeUnion != nil:
		return g.GoIdent(field.TypeUnion.GoIdent)
	}
	t, ok := BaseTypesGo[field.Type]
	if !ok {
		logrus.Panicf("type %s is not base type", field.Type)
	}
	return t
}

func getFieldType(g *GenFile, field *Field) string {
	gotype := fieldGoType(g, field)
	if field.Array {
		switch gotype {
		case "uint8":
			return "[]byte"
		case "string":
			return "string"
		}
		if _, ok := BaseTypesGo[field.Type]; !ok && field.Length > 0 {
			return fmt.Sprintf("[%d]%s", field.Length, gotype)
		}
		return "[]" + gotype
	}
	return gotype
}

func getUnionSize(union *Union) (maxSize int) {
	for _, field := range union.Fields {
		if size, isBaseType := getSizeOfField(field); isBaseType {
			logrus.Panicf("union %s field %s has unexpected type %q", union.Name, field.Name, field.Type)
		} else if size > maxSize {
			maxSize = size
		}
	}
	//logf("getUnionSize: %s %+v max=%v", union.Name, union.Fields, maxSize)
	return
}

func getSizeOfField(field *Field) (size int, isBaseType bool) {
	if alias := field.TypeAlias; alias != nil {
		size = getSizeOfBinapiBaseType(alias.Type, alias.Length)
		return
	}
	if enum := field.TypeEnum; enum != nil {
		size = getSizeOfBinapiBaseType(enum.Type, field.Length)
		return
	}
	if structType := field.TypeStruct; structType != nil {
		size = getSizeOfStruct(structType)
		return
	}
	if union := field.TypeUnion; union != nil {
		size = getUnionSize(union)
		return
	}
	return size, true
}

func getSizeOfStruct(typ *Struct) (size int) {
	for _, field := range typ.Fields {
		fieldSize, isBaseType := getSizeOfField(field)
		if isBaseType {
			size += getSizeOfBinapiBaseType(field.Type, field.Length)
			continue
		}
		size += fieldSize
	}
	return size
}

// Returns size of base type multiplied by length. Length equal to zero
// returns base type size.
func getSizeOfBinapiBaseType(typ string, length int) (size int) {
	if n := BaseTypeSizes[typ]; n > 0 {
		if length > 1 {
			return n * length
		} else {
			return n
		}
	}
	return
}