package types

import (
	"fmt"

	jsoniter "github.com/json-iterator/go"

	hmTypes "github.com/maticnetwork/heimdall/types"
)

// Vote represents vote from validator
type Vote struct {
	ProposalID uint64              `json:"proposal_id" yaml:"proposal_id"` //  proposalID of the proposal
	Voter      hmTypes.ValidatorID `json:"voter" yaml:"voter"`             //  id of the voter
	Option     VoteOption          `json:"option" yaml:"option"`           //  option from OptionSet chosen by the voter
}

// NewVote creates a new Vote instance
func NewVote(proposalID uint64, voter hmTypes.ValidatorID, option VoteOption) Vote {
	return Vote{proposalID, voter, option}
}

func (v Vote) String() string {
	return fmt.Sprintf("voter %s voted with option %s on proposal %d", v.Voter.String(), v.Option, v.ProposalID)
}

// Votes is a collection of Vote objects
type Votes []Vote

func (v Votes) String() string {
	if len(v) == 0 {
		return "[]"
	}
	out := fmt.Sprintf("Votes for Proposal %d:", v[0].ProposalID)
	for _, vot := range v {
		out += fmt.Sprintf("\n  %s: %s", vot.Voter.String(), vot.Option)
	}
	return out
}

// Equals returns whether two votes are equal.
func (v Vote) Equals(comp Vote) bool {
	return v.Voter == comp.Voter &&
		v.ProposalID == comp.ProposalID &&
		v.Option == comp.Option
}

// Empty returns whether a vote is empty.
func (v Vote) Empty() bool {
	return v.Equals(Vote{})
}

// VoteOption defines a vote option
type VoteOption byte

// Vote options
const (
	OptionEmpty      VoteOption = 0x00
	OptionYes        VoteOption = 0x01
	OptionAbstain    VoteOption = 0x02
	OptionNo         VoteOption = 0x03
	OptionNoWithVeto VoteOption = 0x04
)

// VoteOptionFromString returns a VoteOption from a string. It returns an error
// if the string is invalid.
func VoteOptionFromString(str string) (VoteOption, error) {
	switch str {
	case "Yes":
		return OptionYes, nil

	case "Abstain":
		return OptionAbstain, nil

	case "No":
		return OptionNo, nil

	case "NoWithVeto":
		return OptionNoWithVeto, nil

	default:
		return VoteOption(0xff), fmt.Errorf("'%s' is not a valid vote option", str)
	}
}

// ValidVoteOption returns true if the vote option is valid and false otherwise.
func ValidVoteOption(option VoteOption) bool {
	if option == OptionYes ||
		option == OptionAbstain ||
		option == OptionNo ||
		option == OptionNoWithVeto {
		return true
	}
	return false
}

// Marshal needed for protobuf compatibility.
func (vo VoteOption) Marshal() ([]byte, error) {
	return []byte{byte(vo)}, nil
}

// Unmarshal needed for protobuf compatibility.
func (vo *VoteOption) Unmarshal(data []byte) error {
	*vo = VoteOption(data[0])
	return nil
}

// MarshalJSON marshals to JSON using string.
func (vo VoteOption) MarshalJSON() ([]byte, error) {
	return jsoniter.ConfigFastest.Marshal(vo.String())
}

// UnmarshalJSON decodes from JSON assuming Bech32 encoding.
func (vo *VoteOption) UnmarshalJSON(data []byte) error {
	var s string
	err := jsoniter.ConfigFastest.Unmarshal(data, &s)
	if err != nil {
		return err
	}

	bz2, err := VoteOptionFromString(s)
	if err != nil {
		return err
	}

	*vo = bz2
	return nil
}

// String implements the Stringer interface.
func (vo VoteOption) String() string {
	switch vo {
	case OptionYes:
		return "Yes"
	case OptionAbstain:
		return "Abstain"
	case OptionNo:
		return "No"
	case OptionNoWithVeto:
		return "NoWithVeto"
	default:
		return ""
	}
}

// Format implements the fmt.Formatter interface.
// nolint: errcheck
func (vo VoteOption) Format(s fmt.State, verb rune) {
	switch verb {
	case 's':
		s.Write([]byte(vo.String()))
	default:
		s.Write([]byte(fmt.Sprintf("%v", byte(vo))))
	}
}
