/*
NAME
  amf.go

DESCRIPTION
  Action Message Format (AMF) encoding/decoding functions.
  See https://en.wikipedia.org/wiki/Action_Message_Format.

AUTHORS
  Saxon Nelson-Milton <saxon@ausocean.org>
  Dan Kortschak <dan@ausocean.org>
  Jake Lane <jake@ausocean.org>
  Alan Noble <alan@ausocean.org>

LICENSE
  amf.go is Copyright (C) 2017-2019 the Australian Ocean Lab (AusOcean)

  It is free software: you can redistribute it and/or modify them
  under the terms of the GNU General Public License as published by the
  Free Software Foundation, either version 3 of the License, or (at your
  option) any later version.

  It is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  for more details.

  You should have received a copy of the GNU General Public License
  along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.

  Derived from librtmp under the GNU Lesser General Public License 2.1
  Copyright (C) 2005-2008 Team XBMC http://www.xbmc.org
  Copyright (C) 2008-2009 Andrej Stepanchuk
  Copyright (C) 2009-2010 Howard Chu
*/

// Package amf implements Action Message Format (AMF) encoding and decoding.
// In AMF, encoding of numbers is big endian by default, unless specified otherwise,
// and numbers are all unsigned.
// See https://en.wikipedia.org/wiki/Action_Message_Format.
package amf

import (
	"encoding/binary"
	"errors"
	"math"
)

// AMF data types, as defined by the AMF specification.
// NB: we export these sparingly.
const (
	typeNumber      = 0x00
	typeBoolean     = 0x01
	TypeString      = 0x02
	TypeObject      = 0x03
	typeMovieClip   = 0x04
	TypeNull        = 0x05
	typeUndefined   = 0x06
	typeReference   = 0x07
	typeEcmaArray   = 0x08
	TypeObjectEnd   = 0x09
	typeStrictArray = 0x0A
	typeDate        = 0x0B
	typeLongString  = 0x0C
	typeUnsupported = 0x0D
	typeRecordset   = 0x0E
	typeXmlDoc      = 0x0F
	typeTypedObject = 0x10
	typeAvmplus     = 0x11
	typeInvalid     = 0xff
)

// AMF represents an AMF object, which is simply a collection of properties.
type Object struct {
	Properties []Property
}

// Property represents an AMF property, which is effectively a
// union. The Type is the AMF data type (uint8 per the specification),
// and specifies which member holds a value.  Numeric types use
// Number, string types use String and arrays and objects use
// Object. The Name is optional.
type Property struct {
	Type   uint8
	
	Name   string
	Number float64
	String string
	Object Object
}

// AMF errors:
var (
	ErrShortBuffer      = errors.New("amf: short buffer")       // The supplied buffer was too short.
	ErrInvalidType      = errors.New("amf: invalid type")       // An invalid type was supplied to the encoder.
	ErrUnexpectedType   = errors.New("amf: unexpected type")    // An unexpected type was encountered while decoding.
	ErrPropertyNotFound = errors.New("amf: property not found") // The requested property was not found.
)

// DecodeInt16 decodes a 16-bit integer.
func DecodeInt16(buf []byte) uint16 {
	return binary.BigEndian.Uint16(buf)
}

// DecodeInt24 decodes a 24-bit integer.
func DecodeInt24(buf []byte) uint32 {
	return uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])
}

// DecodeInt32 decodes a 32-bit integer.
func DecodeInt32(buf []byte) uint32 {
	return binary.BigEndian.Uint32(buf)
}

// DecodeInt32LE decodes a 32-bit little-endian integer.
func DecodeInt32LE(buf []byte) uint32 {
	return binary.LittleEndian.Uint32(buf)
}

// DecodeString decodes a string that is less than 2^16 bytes long.
func DecodeString(buf []byte) string {
	n := DecodeInt16(buf)
	return string(buf[2 : 2+n])
}

// DecodeLongString decodes a long string.
func DecodeLongString(buf []byte) string {
	n := DecodeInt32(buf)
	return string(buf[2 : 2+n])
}

// DecodeNumber decodes a 64-bit floating-point number.
func DecodeNumber(buf []byte) float64 {
	return math.Float64frombits(binary.BigEndian.Uint64(buf))
}

// DecodeBoolean decodes a boolean.
func DecodeBoolean(buf []byte) bool {
	return buf[0] != 0
}

// EncodeInt24 encodes a 24-bit integer.
func EncodeInt24(buf []byte, val uint32) ([]byte, error) {
	if len(buf) < 3 {
		return nil, ErrShortBuffer
	}
	buf[0] = byte(val >> 16)
	buf[1] = byte(val >> 8)
	buf[2] = byte(val)
	return buf[3:], nil
}

// EncodeInt32 encodes a 32-bit integer.
func EncodeInt32(buf []byte, val uint32) ([]byte, error) {
	if len(buf) < 4 {
		return nil, ErrShortBuffer
	}
	binary.BigEndian.PutUint32(buf, val)
	return buf[4:], nil
}

// EncodeString encodes a string.
// Strings less than 65536 in length are encoded as TypeString, while longer strings are ecodeded as typeLongString.
func EncodeString(buf []byte, val string) ([]byte, error) {
	const typeSize = 1
	if len(val) < 65536 && len(val)+typeSize+binary.Size(int16(0)) > len(buf) {
		return nil, ErrShortBuffer
	}

	if len(val)+typeSize+binary.Size(uint32(0)) > len(buf) {
		return nil, ErrShortBuffer
	}

	if len(val) < 65536 {
		buf[0] = TypeString
		buf = buf[1:]
		binary.BigEndian.PutUint16(buf[:2], uint16(len(val)))
		buf = buf[2:]
		copy(buf, val)
		return buf[len(val):], nil
	}

	buf[0] = typeLongString
	buf = buf[1:]
	binary.BigEndian.PutUint32(buf[:4], uint32(len(val)))
	buf = buf[4:]
	copy(buf, val)
	return buf[len(val):], nil
}

// EncodeNumber encodes a 64-bit floating-point number.
func EncodeNumber(buf []byte, val float64) ([]byte, error) {
	if len(buf) < 9 {
		return nil, ErrShortBuffer
	}
	buf[0] = typeNumber
	buf = buf[1:]
	binary.BigEndian.PutUint64(buf, math.Float64bits(val))
	return buf[8:], nil
}

// EncodeBoolean encodes a boolean.
func EncodeBoolean(buf []byte, val bool) ([]byte, error) {
	if len(buf) < 2 {
		return nil, ErrShortBuffer
	}
	buf[0] = typeBoolean
	if val {
		buf[1] = 1
	} else {
		buf[1] = 0
	}
	return buf[2:], nil

}

// EncodeNamedString encodes a named string, where key is the name and val is the string value.
func EncodeNamedString(buf []byte, key, val string) ([]byte, error) {
	if 2+len(key) > len(buf) {
		return nil, ErrShortBuffer
	}
	binary.BigEndian.PutUint16(buf[:2], uint16(len(key)))
	buf = buf[2:]
	copy(buf, key)
	return EncodeString(buf[len(key):], val)
}

// EncodeNamedNumber encodes a named number, where key is the name and val is the number value.
func EncodeNamedNumber(buf []byte, key string, val float64) ([]byte, error) {
	if 2+len(key) > len(buf) {
		return nil, ErrShortBuffer
	}
	binary.BigEndian.PutUint16(buf[:2], uint16(len(key)))
	buf = buf[2:]
	copy(buf, key)
	return EncodeNumber(buf[len(key):], val)
}

// EncodeNamedNumber encodes a named boolean, where key is the name and val is the boolean value.
func EncodeNamedBoolean(buf []byte, key string, val bool) ([]byte, error) {
	if 2+len(key) > len(buf) {
		return nil, ErrShortBuffer
	}
	binary.BigEndian.PutUint16(buf[:2], uint16(len(key)))
	buf = buf[2:]
	copy(buf, key)
	return EncodeBoolean(buf[len(key):], val)
}

// EncodeProperty encodes a property.
func EncodeProperty(prop *Property, buf []byte) ([]byte, error) {
	if prop.Type != TypeNull && prop.Name != "" {
		if len(buf) < 2+len(prop.Name) {
			return nil, ErrShortBuffer
		}
		binary.BigEndian.PutUint16(buf[:2], uint16(len(prop.Name)))
		buf = buf[2:]
		copy(buf, prop.Name)
		buf = buf[len(prop.Name):]
	}

	switch prop.Type {
	case typeNumber:
		return EncodeNumber(buf, prop.Number)
	case typeBoolean:
		return EncodeBoolean(buf, prop.Number != 0)
	case TypeString:
		return EncodeString(buf, prop.String)
	case TypeNull:
		if len(buf) < 2 {
			return nil, ErrShortBuffer
		}
		buf[0] = TypeNull
		buf = buf[1:]
	case TypeObject:
		return Encode(&prop.Object, buf)
	case typeEcmaArray:
		return EncodeEcmaArray(&prop.Object, buf)
	case typeStrictArray:
		return EncodeArray(&prop.Object, buf)
	default:
		return nil, ErrInvalidType
	}
	return buf, nil
}

// DecodeProperty decodes a property, returning the number of bytes consumed from the supplied buffer.
func DecodeProperty(prop *Property, buf []byte, decodeName bool) (int, error) {
	sz := len(buf)

	if decodeName {
		if len(buf) < 4 {
			return 0, ErrShortBuffer
		}
		n := DecodeInt16(buf[:2])
		if int(n) > len(buf)-2 {
			return 0, ErrShortBuffer
		}

		prop.Name = DecodeString(buf)
		buf = buf[2+n:]
	} else {
		prop.Name = ""
	}

	prop.Type = buf[0]
	buf = buf[1:]

	switch prop.Type {
	case typeNumber:
		if len(buf) < 8 {
			return 0, ErrShortBuffer
		}
		prop.Number = DecodeNumber(buf[:8])
		buf = buf[8:]

	case typeBoolean:
		if len(buf) < 1 {
			return 0, ErrShortBuffer
		}
		prop.Number = float64(buf[0])
		buf = buf[1:]

	case TypeString:
		n := DecodeInt16(buf[:2])
		if len(buf) < int(n+2) {
			return 0, ErrShortBuffer
		}
		prop.String = DecodeString(buf)
		buf = buf[2+n:]

	case TypeObject:
		n, err := Decode(&prop.Object, buf, true)
		if err != nil {
			return 0, err
		}
		buf = buf[n:]

	case TypeNull, typeUndefined, typeUnsupported:
		prop.Type = TypeNull

	case typeEcmaArray:
		buf = buf[4:]
		n, err := Decode(&prop.Object, buf, true)
		if err != nil {
			return 0, err
		}
		buf = buf[n:]

	default:
		return 0, ErrUnexpectedType
	}

	return sz - len(buf), nil
}

// Encode encodes an Object into its AMF representation.
func Encode(obj *Object, buf []byte) ([]byte, error) {
	if len(buf) < 5 {
		return nil, ErrShortBuffer
	}

	buf[0] = TypeObject
	buf = buf[1:]

	for i := 0; i < len(obj.Properties); i++ {
		var err error
		buf, err = EncodeProperty(&obj.Properties[i], buf)
		if err != nil {
			return nil, err
		}
	}

	if len(buf) < 3 {
		return nil, ErrShortBuffer
	}
	return EncodeInt24(buf, TypeObjectEnd)
}

// EncodeEcmaArray encodes an ECMA array.
func EncodeEcmaArray(obj *Object, buf []byte) ([]byte, error) {
	if len(buf) < 5 {
		return nil, ErrShortBuffer
	}

	buf[0] = typeEcmaArray
	buf = buf[1:]
	binary.BigEndian.PutUint32(buf[:4], uint32(len(obj.Properties)))
	buf = buf[4:]

	for i := 0; i < len(obj.Properties); i++ {
		var err error
		buf, err = EncodeProperty(&obj.Properties[i], buf)
		if err != nil {
			return nil, err
		}
	}

	if len(buf) < 3 {
		return nil, ErrShortBuffer
	}
	return EncodeInt24(buf, TypeObjectEnd)
}

// EncodeArray encodes an array.
func EncodeArray(obj *Object, buf []byte) ([]byte, error) {
	if len(buf) < 5 {
		return nil, ErrShortBuffer
	}

	buf[0] = typeStrictArray
	buf = buf[1:]
	binary.BigEndian.PutUint32(buf[:4], uint32(len(obj.Properties)))
	buf = buf[4:]

	for i := 0; i < len(obj.Properties); i++ {
		var err error
		buf, err = EncodeProperty(&obj.Properties[i], buf)
		if err != nil {
			return nil, err
		}
	}

	return buf, nil
}

// Decode decodes an object. Property names are only decoded if decodeName is true.
func Decode(obj *Object, buf []byte, decodeName bool) (int, error) {
	sz := len(buf)

	obj.Properties = obj.Properties[:0]
	for len(buf) != 0 {
		if len(buf) >= 3 && DecodeInt24(buf[:3]) == TypeObjectEnd {
			buf = buf[3:]
			break
		}
		var prop Property
		n, err := DecodeProperty(&prop, buf, decodeName)
		if err != nil {
			return 0, err
		}
		buf = buf[n:]
		obj.Properties = append(obj.Properties, prop)
	}

	return sz - len(buf), nil
}

// Object methods:

// Property returns a property, either by its index when idx is non-negative, or by its name otherwise.
// If the requested property is not found or the type does not match, an ErrPropertyNotFound error is returned.
func (obj *Object) Property(name string, idx int, typ uint8) (*Property, error) {
	var prop *Property
	if idx >= 0 {
		if idx < len(obj.Properties) {
			prop = &obj.Properties[idx]
		}
	} else {
		for i, p := range obj.Properties {
			if p.Name == name {
				prop = &obj.Properties[i]
				break
			}
		}
	}
	if prop == nil || prop.Type != typ {
		return nil, ErrPropertyNotFound
	}
	return prop, nil
}

// NumberProperty is a wrapper for Property that returns a Number property's value, if any.
func (obj *Object) NumberProperty(name string, idx int) (float64, error) {
	prop, err := obj.Property(name, idx, typeNumber)
	if err != nil {
		return 0, err
	}
	return prop.Number, nil
}

// StringProperty is a wrapper for Property that returns a String property's  value, if any.
func (obj *Object) StringProperty(name string, idx int) (string, error) {
	prop, err := obj.Property(name, idx, TypeString)
	if err != nil {
		return "", err
	}
	return prop.String, nil
}

// ObjectProperty is a wrapper for Property that returns an Object property's value, if any.
func (obj *Object) ObjectProperty(name string, idx int) (*Object, error) {
	prop, err := obj.Property(name, idx, TypeObject)
	if err != nil {
		return nil, err
	}
	return &prop.Object, nil
}