package native

import (
	"encoding/base64"
	"encoding/hex"
	"errors"
	"math/big"
	"strings"

	"github.com/mr-tron/base58"
	"github.com/nspcc-dev/neo-go/pkg/core/interop"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// Std represents StdLib contract.
type Std struct {
	interop.ContractMD
}

const stdContractID = -2

var (
	// ErrInvalidBase is returned when base is invalid.
	ErrInvalidBase = errors.New("invalid base")
	// ErrInvalidFormat is returned when string is not a number.
	ErrInvalidFormat = errors.New("invalid format")
)

func newStd() *Std {
	s := &Std{ContractMD: *interop.NewContractMD(nativenames.StdLib, stdContractID)}
	defer s.UpdateHash()

	desc := newDescriptor("serialize", smartcontract.ByteArrayType,
		manifest.NewParameter("item", smartcontract.AnyType))
	md := newMethodAndPrice(s.serialize, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("deserialize", smartcontract.AnyType,
		manifest.NewParameter("data", smartcontract.ByteArrayType))
	md = newMethodAndPrice(s.deserialize, 1<<14, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("jsonSerialize", smartcontract.ByteArrayType,
		manifest.NewParameter("item", smartcontract.AnyType))
	md = newMethodAndPrice(s.jsonSerialize, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("jsonDeserialize", smartcontract.AnyType,
		manifest.NewParameter("json", smartcontract.ByteArrayType))
	md = newMethodAndPrice(s.jsonDeserialize, 1<<14, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("itoa", smartcontract.StringType,
		manifest.NewParameter("value", smartcontract.IntegerType),
		manifest.NewParameter("base", smartcontract.IntegerType))
	md = newMethodAndPrice(s.itoa, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("atoi", smartcontract.IntegerType,
		manifest.NewParameter("value", smartcontract.StringType),
		manifest.NewParameter("base", smartcontract.IntegerType))
	md = newMethodAndPrice(s.atoi, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("base64Encode", smartcontract.StringType,
		manifest.NewParameter("data", smartcontract.ByteArrayType))
	md = newMethodAndPrice(s.base64Encode, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("base64Decode", smartcontract.ByteArrayType,
		manifest.NewParameter("s", smartcontract.StringType))
	md = newMethodAndPrice(s.base64Decode, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("base58Encode", smartcontract.StringType,
		manifest.NewParameter("data", smartcontract.ByteArrayType))
	md = newMethodAndPrice(s.base58Encode, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	desc = newDescriptor("base58Decode", smartcontract.ByteArrayType,
		manifest.NewParameter("s", smartcontract.StringType))
	md = newMethodAndPrice(s.base58Decode, 1<<12, callflag.NoneFlag)
	s.AddMethod(md, desc)

	return s
}

func (s *Std) serialize(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	data, err := stackitem.SerializeItem(args[0])
	if err != nil {
		panic(err)
	}
	if len(data) > stackitem.MaxSize {
		panic(errors.New("too big item"))
	}

	return stackitem.NewByteArray(data)
}

func (s *Std) deserialize(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	data, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}

	item, err := stackitem.DeserializeItem(data)
	if err != nil {
		panic(err)
	}

	return item
}

func (s *Std) jsonSerialize(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	data, err := stackitem.ToJSON(args[0])
	if err != nil {
		panic(err)
	}
	if len(data) > stackitem.MaxSize {
		panic(errors.New("too big item"))
	}

	return stackitem.NewByteArray(data)
}

func (s *Std) jsonDeserialize(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	data, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}

	item, err := stackitem.FromJSON(data)
	if err != nil {
		panic(err)
	}

	return item
}

func (s *Std) itoa(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	num := toBigInt(args[0])
	base := toBigInt(args[1])
	if !base.IsInt64() {
		panic(ErrInvalidBase)
	}
	var str string
	switch b := base.Int64(); b {
	case 10:
		str = num.Text(10)
	case 16:
		if num.Sign() == 0 {
			str = "0"
			break
		}
		bs := bigint.ToBytes(num)
		reverse(bs)
		str = hex.EncodeToString(bs)
		if pad := bs[0] & 0xF8; pad == 0 || pad == 0xF8 {
			str = str[1:]
		}
		str = strings.ToUpper(str)
	default:
		panic(ErrInvalidBase)
	}
	return stackitem.NewByteArray([]byte(str))
}

func (s *Std) atoi(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	num := toString(args[0])
	base := toBigInt(args[1])
	if !base.IsInt64() {
		panic(ErrInvalidBase)
	}
	var bi *big.Int
	switch b := base.Int64(); b {
	case 10:
		var ok bool
		bi, ok = new(big.Int).SetString(num, int(b))
		if !ok {
			panic(ErrInvalidFormat)
		}
	case 16:
		changed := len(num)%2 != 0
		if changed {
			num = "0" + num
		}
		bs, err := hex.DecodeString(num)
		if err != nil {
			panic(ErrInvalidFormat)
		}
		if changed && bs[0]&0x8 != 0 {
			bs[0] |= 0xF0
		}
		reverse(bs)
		bi = bigint.FromBytes(bs)
	default:
		panic(ErrInvalidBase)
	}

	return stackitem.NewBigInteger(bi)
}

func reverse(b []byte) {
	l := len(b)
	for i := 0; i < l/2; i++ {
		b[i], b[l-i-1] = b[l-i-1], b[i]
	}
}

func (s *Std) base64Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	src, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	result := base64.StdEncoding.EncodeToString(src)

	return stackitem.NewByteArray([]byte(result))
}

func (s *Std) base64Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	src := toString(args[0])
	result, err := base64.StdEncoding.DecodeString(src)
	if err != nil {
		panic(err)
	}

	return stackitem.NewByteArray(result)
}

func (s *Std) base58Encode(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	src, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	result := base58.Encode(src)

	return stackitem.NewByteArray([]byte(result))
}

func (s *Std) base58Decode(_ *interop.Context, args []stackitem.Item) stackitem.Item {
	src := toString(args[0])
	result, err := base58.Decode(src)
	if err != nil {
		panic(err)
	}

	return stackitem.NewByteArray(result)
}

// Metadata implements Contract interface.
func (s *Std) Metadata() *interop.ContractMD {
	return &s.ContractMD
}

// Initialize implements Contract interface.
func (s *Std) Initialize(ic *interop.Context) error {
	return nil
}

// OnPersist implements Contract interface.
func (s *Std) OnPersist(ic *interop.Context) error {
	return nil
}

// PostPersist implements Contract interface.
func (s *Std) PostPersist(ic *interop.Context) error {
	return nil
}