package transaction

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/nspcc-dev/neo-go/pkg/io"
)

// Attribute represents a Transaction attribute.
type Attribute struct {
	Usage AttrUsage
	Data  []byte
}

// attrJSON is used for JSON I/O of Attribute.
type attrJSON struct {
	Usage string `json:"usage"`
	Data  string `json:"data"`
}

// DecodeBinary implements Serializable interface.
func (attr *Attribute) DecodeBinary(br *io.BinReader) {
	attr.Usage = AttrUsage(br.ReadB())

	// very special case
	if attr.Usage == ECDH02 || attr.Usage == ECDH03 {
		attr.Data = make([]byte, 33)
		attr.Data[0] = byte(attr.Usage)
		br.ReadBytes(attr.Data[1:])
		return
	}
	var datasize uint64
	switch attr.Usage {
	case ContractHash, Vote, Hash1, Hash2, Hash3, Hash4, Hash5,
		Hash6, Hash7, Hash8, Hash9, Hash10, Hash11, Hash12, Hash13,
		Hash14, Hash15:
		datasize = 32
	case DescriptionURL:
		// It's not VarUint as per C# implementation, dunno why
		var urllen = br.ReadB()
		datasize = uint64(urllen)
	case Description, Remark, Remark1, Remark2, Remark3, Remark4,
		Remark5, Remark6, Remark7, Remark8, Remark9, Remark10, Remark11,
		Remark12, Remark13, Remark14, Remark15:
		datasize = br.ReadVarUint()
	default:
		br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Usage))
		return
	}
	attr.Data = make([]byte, datasize)
	br.ReadBytes(attr.Data)
}

// EncodeBinary implements Serializable interface.
func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
	bw.WriteB(byte(attr.Usage))
	switch attr.Usage {
	case ECDH02, ECDH03:
		bw.WriteBytes(attr.Data[1:])
	case Description, Remark, Remark1, Remark2, Remark3, Remark4,
		Remark5, Remark6, Remark7, Remark8, Remark9, Remark10, Remark11,
		Remark12, Remark13, Remark14, Remark15:
		bw.WriteVarBytes(attr.Data)
	case DescriptionURL:
		bw.WriteB(byte(len(attr.Data)))
		fallthrough
	case ContractHash, Vote, Hash1, Hash2, Hash3, Hash4, Hash5, Hash6,
		Hash7, Hash8, Hash9, Hash10, Hash11, Hash12, Hash13, Hash14, Hash15:
		bw.WriteBytes(attr.Data)
	default:
		bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Usage)
	}
}

// MarshalJSON implements the json Marshaller interface.
func (attr *Attribute) MarshalJSON() ([]byte, error) {
	return json.Marshal(attrJSON{
		Usage: attr.Usage.String(),
		Data:  hex.EncodeToString(attr.Data),
	})
}

// UnmarshalJSON implements the json.Unmarshaller interface.
func (attr *Attribute) UnmarshalJSON(data []byte) error {
	aj := new(attrJSON)
	err := json.Unmarshal(data, aj)
	if err != nil {
		return err
	}
	binData, err := hex.DecodeString(aj.Data)
	if err != nil {
		return err
	}
	switch aj.Usage {
	case "ContractHash":
		attr.Usage = ContractHash
	case "ECDH02":
		attr.Usage = ECDH02
	case "ECDH03":
		attr.Usage = ECDH03
	case "Vote":
		attr.Usage = Vote
	case "CertURL":
		attr.Usage = CertURL
	case "DescriptionURL":
		attr.Usage = DescriptionURL
	case "Description":
		attr.Usage = Description
	case "Hash1":
		attr.Usage = Hash1
	case "Hash2":
		attr.Usage = Hash2
	case "Hash3":
		attr.Usage = Hash3
	case "Hash4":
		attr.Usage = Hash4
	case "Hash5":
		attr.Usage = Hash5
	case "Hash6":
		attr.Usage = Hash6
	case "Hash7":
		attr.Usage = Hash7
	case "Hash8":
		attr.Usage = Hash8
	case "Hash9":
		attr.Usage = Hash9
	case "Hash10":
		attr.Usage = Hash10
	case "Hash11":
		attr.Usage = Hash11
	case "Hash12":
		attr.Usage = Hash12
	case "Hash13":
		attr.Usage = Hash13
	case "Hash14":
		attr.Usage = Hash14
	case "Hash15":
		attr.Usage = Hash15
	case "Remark":
		attr.Usage = Remark
	case "Remark1":
		attr.Usage = Remark1
	case "Remark2":
		attr.Usage = Remark2
	case "Remark3":
		attr.Usage = Remark3
	case "Remark4":
		attr.Usage = Remark4
	case "Remark5":
		attr.Usage = Remark5
	case "Remark6":
		attr.Usage = Remark6
	case "Remark7":
		attr.Usage = Remark7
	case "Remark8":
		attr.Usage = Remark8
	case "Remark9":
		attr.Usage = Remark9
	case "Remark10":
		attr.Usage = Remark10
	case "Remark11":
		attr.Usage = Remark11
	case "Remark12":
		attr.Usage = Remark12
	case "Remark13":
		attr.Usage = Remark13
	case "Remark14":
		attr.Usage = Remark14
	case "Remark15":
		attr.Usage = Remark15
	default:
		return errors.New("wrong Usage")

	}
	attr.Data = binData
	return nil
}