package types

import (
	"fmt"
	"io"

	"github.com/vapor/encoding/blockchain"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
)

// serflag variables for output types.
const (
	IntraChainOutputType uint8 = iota
	CrossChainOutputType
	VoteOutputType
)

type (
	// TxOutput is the top level struct of tx output.
	TxOutput struct {
		AssetVersion uint64
		TypedOutput
		// Unconsumed suffixes of the commitment and witness extensible strings.
		CommitmentSuffix []byte
	}

	// TypedOutput return the txoutput type.
	TypedOutput interface {
		OutputType() uint8
	}
)

// OutputCommitment return the OutputCommitment of a txoutput.
func (to *TxOutput) OutputCommitment() OutputCommitment {
	switch outp := to.TypedOutput.(type) {
	case *IntraChainOutput:
		return outp.OutputCommitment

	case *CrossChainOutput:
		return outp.OutputCommitment

	case *VoteOutput:
		return outp.OutputCommitment

	default:
		return OutputCommitment{}
	}
}

// AssetAmount return the asset id and amount of a txoutput.
func (to *TxOutput) AssetAmount() bc.AssetAmount {
	switch outp := to.TypedOutput.(type) {
	case *IntraChainOutput:
		return outp.AssetAmount

	case *CrossChainOutput:
		return outp.AssetAmount

	case *VoteOutput:
		return outp.AssetAmount

	default:
		return bc.AssetAmount{}
	}
}

// ControlProgram return the control program of the txoutput
func (to *TxOutput) ControlProgram() []byte {
	switch outp := to.TypedOutput.(type) {
	case *IntraChainOutput:
		return outp.ControlProgram

	case *CrossChainOutput:
		return outp.ControlProgram

	case *VoteOutput:
		return outp.ControlProgram

	default:
		return nil
	}
}

// VMVersion return the VM version of the txoutput
func (to *TxOutput) VMVersion() uint64 {
	switch outp := to.TypedOutput.(type) {
	case *IntraChainOutput:
		return outp.VMVersion

	case *CrossChainOutput:
		return outp.VMVersion

	case *VoteOutput:
		return outp.VMVersion

	default:
		return 0
	}
}

func (to *TxOutput) readFrom(r *blockchain.Reader) (err error) {
	if to.AssetVersion, err = blockchain.ReadVarint63(r); err != nil {
		return errors.Wrap(err, "reading asset version")
	}

	to.CommitmentSuffix, err = blockchain.ReadExtensibleString(r, func(r *blockchain.Reader) error {
		if to.AssetVersion != 1 {
			return nil
		}

		var outType [1]byte
		if _, err = io.ReadFull(r, outType[:]); err != nil {
			return errors.Wrap(err, "reading output type")
		}

		switch outType[0] {
		case IntraChainOutputType:
			out := new(IntraChainOutput)
			to.TypedOutput = out
			if out.CommitmentSuffix, err = out.OutputCommitment.readFrom(r, to.AssetVersion); err != nil {
				return errors.Wrap(err, "reading intra-chain output commitment")
			}

		case CrossChainOutputType:
			out := new(CrossChainOutput)
			to.TypedOutput = out
			if out.CommitmentSuffix, err = out.OutputCommitment.readFrom(r, to.AssetVersion); err != nil {
				return errors.Wrap(err, "reading cross-chain output commitment")
			}

		case VoteOutputType:
			out := new(VoteOutput)
			to.TypedOutput = out
			if out.Vote, err = blockchain.ReadVarstr31(r); err != nil {
				return errors.Wrap(err, "reading vote output vote")
			}

			if out.CommitmentSuffix, err = out.OutputCommitment.readFrom(r, to.AssetVersion); err != nil {
				return errors.Wrap(err, "reading vote output commitment")
			}

		default:
			return fmt.Errorf("unsupported output type %d", outType[0])
		}

		return nil
	})

	if err != nil {
		return err
	}

	// read and ignore the (empty) output witness
	_, err = blockchain.ReadVarstr31(r)
	return errors.Wrap(err, "reading output witness")
}

func (to *TxOutput) writeTo(w io.Writer) error {
	if _, err := blockchain.WriteVarint63(w, to.AssetVersion); err != nil {
		return errors.Wrap(err, "writing asset version")
	}

	if _, err := blockchain.WriteExtensibleString(w, to.CommitmentSuffix, to.writeOutputCommitment); err != nil {
		return errors.Wrap(err, "writing output commitment")
	}

	if _, err := blockchain.WriteVarstr31(w, nil); err != nil {
		return errors.Wrap(err, "writing witness")
	}

	return nil
}

func (to *TxOutput) writeOutputCommitment(w io.Writer) error {
	if to.AssetVersion != 1 {
		return nil
	}

	switch outp := to.TypedOutput.(type) {
	case *IntraChainOutput:
		if _, err := w.Write([]byte{IntraChainOutputType}); err != nil {
			return err
		}
		return outp.OutputCommitment.writeExtensibleString(w, outp.CommitmentSuffix, to.AssetVersion)

	case *CrossChainOutput:
		if _, err := w.Write([]byte{CrossChainOutputType}); err != nil {
			return err
		}
		return outp.OutputCommitment.writeExtensibleString(w, outp.CommitmentSuffix, to.AssetVersion)

	case *VoteOutput:
		if _, err := w.Write([]byte{VoteOutputType}); err != nil {
			return err
		}
		if _, err := blockchain.WriteVarstr31(w, outp.Vote); err != nil {
			return err
		}
		return outp.OutputCommitment.writeExtensibleString(w, outp.CommitmentSuffix, to.AssetVersion)

	default:
		return nil
	}
}

// ComputeOutputID assembles an intra-chain(!) output entry given a spend
// commitment and computes and returns its corresponding entry ID.
func ComputeOutputID(sc *SpendCommitment, inputType uint8, vote []byte) (h bc.Hash, err error) {
	defer func() {
		if r, ok := recover().(error); ok {
			err = r
		}
	}()
	src := &bc.ValueSource{
		Ref:      &sc.SourceID,
		Value:    &sc.AssetAmount,
		Position: sc.SourcePosition,
	}
	var o bc.Entry
	switch inputType {
	case SpendInputType:
		o = bc.NewIntraChainOutput(src, &bc.Program{VmVersion: sc.VMVersion, Code: sc.ControlProgram}, 0)
	case VetoInputType:
		o = bc.NewVoteOutput(src, &bc.Program{VmVersion: sc.VMVersion, Code: sc.ControlProgram}, 0, vote)
	default:
		return h, fmt.Errorf("Input type error:[%v]", inputType)
	}

	h = bc.EntryID(o)
	return h, nil
}
