// Copyright [2019] LinkedIn Corp. 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.

package goavro

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"math"
	"strconv"
)

const (
	doubleEncodedLength = 8 // double requires 8 bytes
	floatEncodedLength  = 4 // float requires 4 bytes
)

////////////////////////////////////////
// Binary Decode
////////////////////////////////////////

func doubleNativeFromBinary(buf []byte) (interface{}, []byte, error) {
	if len(buf) < doubleEncodedLength {
		return nil, nil, fmt.Errorf("cannot decode binary double: %s", io.ErrShortBuffer)
	}
	return math.Float64frombits(binary.LittleEndian.Uint64(buf[:doubleEncodedLength])), buf[doubleEncodedLength:], nil
}

func floatNativeFromBinary(buf []byte) (interface{}, []byte, error) {
	if len(buf) < floatEncodedLength {
		return nil, nil, fmt.Errorf("cannot decode binary float: %s", io.ErrShortBuffer)
	}
	return math.Float32frombits(binary.LittleEndian.Uint32(buf[:floatEncodedLength])), buf[floatEncodedLength:], nil
}

////////////////////////////////////////
// Binary Encode
////////////////////////////////////////

func doubleBinaryFromNative(buf []byte, datum interface{}) ([]byte, error) {
	var value float64
	switch v := datum.(type) {
	case float64:
		value = v
	case float32:
		value = float64(v)
	case int:
		if value = float64(v); int(value) != v {
			return nil, fmt.Errorf("cannot encode binary double: provided Go int would lose precision: %d", v)
		}
	case int64:
		if value = float64(v); int64(value) != v {
			return nil, fmt.Errorf("cannot encode binary double: provided Go int64 would lose precision: %d", v)
		}
	case int32:
		if value = float64(v); int32(value) != v {
			return nil, fmt.Errorf("cannot encode binary double: provided Go int32 would lose precision: %d", v)
		}
	default:
		return nil, fmt.Errorf("cannot encode binary double: expected: Go numeric; received: %T", datum)
	}
	buf = append(buf, 0, 0, 0, 0, 0, 0, 0, 0)
	binary.LittleEndian.PutUint64(buf[len(buf)-doubleEncodedLength:], math.Float64bits(value))
	return buf, nil
}

func floatBinaryFromNative(buf []byte, datum interface{}) ([]byte, error) {
	var value float32
	switch v := datum.(type) {
	case float32:
		value = v
	case float64:
		// Assume runtime can cast special floats correctly, and if there is a
		// loss of precision from float64 and float32, that should be expected
		// or at least understood by the client.
		value = float32(v)
	case int:
		if value = float32(v); int(value) != v {
			return nil, fmt.Errorf("cannot encode binary float: provided Go int would lose precision: %d", v)
		}
	case int64:
		if value = float32(v); int64(value) != v {
			return nil, fmt.Errorf("cannot encode binary float: provided Go int64 would lose precision: %d", v)
		}
	case int32:
		if value = float32(v); int32(value) != v {
			return nil, fmt.Errorf("cannot encode binary float: provided Go int32 would lose precision: %d", v)
		}
	default:
		return nil, fmt.Errorf("cannot encode binary float: expected: Go numeric; received: %T", datum)
	}
	// return floatingBinaryEncoder(buf, uint64(math.Float32bits(value)), floatEncodedLength)
	buf = append(buf, 0, 0, 0, 0)
	binary.LittleEndian.PutUint32(buf[len(buf)-floatEncodedLength:], uint32(math.Float32bits(value)))
	return buf, nil
}

////////////////////////////////////////
// Text Decode
////////////////////////////////////////

func doubleNativeFromTextual(buf []byte) (interface{}, []byte, error) {
	return floatingTextDecoder(buf, 64)
}

func floatNativeFromTextual(buf []byte) (interface{}, []byte, error) {
	return floatingTextDecoder(buf, 32)
}

func floatingTextDecoder(buf []byte, bitSize int) (interface{}, []byte, error) {
	buflen := len(buf)
	if buflen >= 4 {
		if bytes.Equal(buf[:4], []byte("null")) {
			return math.NaN(), buf[4:], nil
		}
		if buflen >= 5 {
			if bytes.Equal(buf[:5], []byte("1e999")) {
				return math.Inf(1), buf[5:], nil
			}
			if buflen >= 6 {
				if bytes.Equal(buf[:6], []byte("-1e999")) {
					return math.Inf(-1), buf[6:], nil
				}
			}
		}
	}
	index, err := numberLength(buf, true) // NOTE: floatAllowed = true
	if err != nil {
		return nil, nil, err
	}
	datum, err := strconv.ParseFloat(string(buf[:index]), bitSize)
	if err != nil {
		return nil, nil, err
	}
	if bitSize == 32 {
		return float32(datum), buf[index:], nil
	}
	return datum, buf[index:], nil
}

func numberLength(buf []byte, floatAllowed bool) (int, error) {
	// ALGORITHM: increment index as long as bytes are valid for number state engine.
	var index, buflen, count int
	var b byte

	// STATE 0: begin, optional: -
	if buflen = len(buf); index == buflen {
		return 0, io.ErrShortBuffer
	}
	if buf[index] == '-' {
		if index++; index == buflen {
			return 0, io.ErrShortBuffer
		}
	}
	// STATE 1: if 0, goto 2; otherwise if 1-9, goto 3; otherwise bail
	if b = buf[index]; b == '0' {
		if index++; index == buflen {
			return index, nil // valid number
		}
	} else if b >= '1' && b <= '9' {
		if index++; index == buflen {
			return index, nil // valid number
		}
		// STATE 3: absorb zero or more digits
		for {
			if b = buf[index]; b < '0' || b > '9' {
				break
			}
			if index++; index == buflen {
				return index, nil // valid number
			}
		}
	} else {
		return 0, fmt.Errorf("unexpected byte: %q", b)
	}
	if floatAllowed {
		// STATE 2: if ., goto 4; otherwise goto 5
		if buf[index] == '.' {
			if index++; index == buflen {
				return 0, io.ErrShortBuffer
			}
			// STATE 4: absorb one or more digits
			for {
				if b = buf[index]; b < '0' || b > '9' {
					break
				}
				count++
				if index++; index == buflen {
					return index, nil // valid number
				}
			}
			if count == 0 {
				// did not get at least one digit
				return 0, fmt.Errorf("unexpected byte: %q", b)
			}
		}
		// STATE 5: if e|e, goto 6; otherwise goto 7
		if b = buf[index]; b == 'e' || b == 'E' {
			if index++; index == buflen {
				return 0, io.ErrShortBuffer
			}
			// STATE 6: if -|+, goto 8; otherwise goto 8
			if b = buf[index]; b == '+' || b == '-' {
				if index++; index == buflen {
					return 0, io.ErrShortBuffer
				}
			}
			// STATE 8: absorb one or more digits
			count = 0
			for {
				if b = buf[index]; b < '0' || b > '9' {
					break
				}
				count++
				if index++; index == buflen {
					return index, nil // valid number
				}
			}
			if count == 0 {
				// did not get at least one digit
				return 0, fmt.Errorf("unexpected byte: %q", b)
			}
		}
	}
	// STATE 7: end
	return index, nil
}

////////////////////////////////////////
// Text Encode
////////////////////////////////////////

func floatTextualFromNative(buf []byte, datum interface{}) ([]byte, error) {
	return floatingTextEncoder(buf, datum, 32)
}

func doubleTextualFromNative(buf []byte, datum interface{}) ([]byte, error) {
	return floatingTextEncoder(buf, datum, 64)
}

func floatingTextEncoder(buf []byte, datum interface{}, bitSize int) ([]byte, error) {
	var isFloat bool
	var someFloat64 float64
	var someInt64 int64
	switch v := datum.(type) {
	case float32:
		isFloat = true
		someFloat64 = float64(v)
	case float64:
		isFloat = true
		someFloat64 = v
	case int:
		if someInt64 = int64(v); int(someInt64) != v {
			if bitSize == 64 {
				return nil, fmt.Errorf("cannot encode textual double: provided Go int would lose precision: %d", v)
			}
			return nil, fmt.Errorf("cannot encode textual float: provided Go int would lose precision: %d", v)
		}
	case int64:
		someInt64 = v
	case int32:
		if someInt64 = int64(v); int32(someInt64) != v {
			if bitSize == 64 {
				return nil, fmt.Errorf("cannot encode textual double: provided Go int32 would lose precision: %d", v)
			}
			return nil, fmt.Errorf("cannot encode textual float: provided Go int32 would lose precision: %d", v)
		}
	default:
		if bitSize == 64 {
			return nil, fmt.Errorf("cannot encode textual double: expected: Go numeric; received: %T", datum)
		}
		return nil, fmt.Errorf("cannot encode textual float: expected: Go numeric; received: %T", datum)
	}

	if isFloat {
		if math.IsNaN(someFloat64) {
			return append(buf, "null"...), nil
		}
		if math.IsInf(someFloat64, 1) {
			return append(buf, "1e999"...), nil
		}
		if math.IsInf(someFloat64, -1) {
			return append(buf, "-1e999"...), nil
		}
		return strconv.AppendFloat(buf, someFloat64, 'g', -1, bitSize), nil
	}
	return strconv.AppendInt(buf, someInt64, 10), nil
}
