package multisig

import (
	fmt "fmt"

	tmcrypto "github.com/tendermint/tendermint/crypto"

	"github.com/cosmos/cosmos-sdk/codec/types"
	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
	multisigtypes "github.com/cosmos/cosmos-sdk/crypto/types/multisig"
	"github.com/cosmos/cosmos-sdk/types/tx/signing"
)

var _ multisigtypes.PubKey = &LegacyAminoPubKey{}
var _ types.UnpackInterfacesMessage = &LegacyAminoPubKey{}

// NewLegacyAminoPubKey returns a new LegacyAminoPubKey.
// Multisig can be constructed with multiple same keys - it will increase the power of
// the owner of that key (he will still need to add multiple signatures in the right order).
// Panics if len(pubKeys) < k or 0 >= k.
func NewLegacyAminoPubKey(threshold int, pubKeys []cryptotypes.PubKey) *LegacyAminoPubKey {
	if threshold <= 0 {
		panic("threshold k of n multisignature: k <= 0")
	}
	if len(pubKeys) < threshold {
		panic("threshold k of n multisignature: len(pubKeys) < k")
	}
	anyPubKeys, err := packPubKeys(pubKeys)
	if err != nil {
		panic(err)
	}
	return &LegacyAminoPubKey{Threshold: uint32(threshold), PubKeys: anyPubKeys}
}

// Address implements cryptotypes.PubKey Address method
func (m *LegacyAminoPubKey) Address() cryptotypes.Address {
	return tmcrypto.AddressHash(m.Bytes())
}

// Bytes returns the proto encoded version of the LegacyAminoPubKey
func (m *LegacyAminoPubKey) Bytes() []byte {
	return AminoCdc.MustMarshalBinaryBare(m)
}

// VerifyMultisignature implements the multisigtypes.PubKey VerifyMultisignature method.
// The signatures must be added in an order corresponding to the public keys order in
// LegacyAminoPubKey. It's OK to have multiple same keys in the multisig - it will increase
// the power of the owner of that key - in that case the signer will still need to append
// multiple same signatures in the right order.
func (m *LegacyAminoPubKey) VerifyMultisignature(getSignBytes multisigtypes.GetSignBytesFunc, sig *signing.MultiSignatureData) error {
	bitarray := sig.BitArray
	sigs := sig.Signatures
	size := bitarray.Count()
	pubKeys := m.GetPubKeys()
	// ensure bit array is the correct size
	if len(pubKeys) != size {
		return fmt.Errorf("bit array size is incorrect, expecting: %d", len(pubKeys))
	}
	// ensure size of signature list
	if len(sigs) < int(m.Threshold) || len(sigs) > size {
		return fmt.Errorf("signature size is incorrect %d", len(sigs))
	}
	// ensure at least k signatures are set
	if bitarray.NumTrueBitsBefore(size) < int(m.Threshold) {
		return fmt.Errorf("not enough signatures set, have %d, expected %d", bitarray.NumTrueBitsBefore(size), int(m.Threshold))
	}
	// index in the list of signatures which we are concerned with.
	sigIndex := 0
	for i := 0; i < size; i++ {
		if bitarray.GetIndex(i) {
			si := sig.Signatures[sigIndex]
			switch si := si.(type) {
			case *signing.SingleSignatureData:
				msg, err := getSignBytes(si.SignMode)
				if err != nil {
					return err
				}
				if !pubKeys[i].VerifySignature(msg, si.Signature) {
					return fmt.Errorf("unable to verify signature at index %d", i)
				}
			case *signing.MultiSignatureData:
				nestedMultisigPk, ok := pubKeys[i].(multisigtypes.PubKey)
				if !ok {
					return fmt.Errorf("unable to parse pubkey of index %d", i)
				}
				if err := nestedMultisigPk.VerifyMultisignature(getSignBytes, si); err != nil {
					return err
				}
			default:
				return fmt.Errorf("improper signature data type for index %d", sigIndex)
			}
			sigIndex++
		}
	}
	return nil
}

// VerifySignature implements cryptotypes.PubKey VerifySignature method,
// it panics because it can't handle MultiSignatureData
// cf. https://github.com/cosmos/cosmos-sdk/issues/7109#issuecomment-686329936
func (m *LegacyAminoPubKey) VerifySignature(msg []byte, sig []byte) bool {
	panic("not implemented")
}

// GetPubKeys implements the PubKey.GetPubKeys method
func (m *LegacyAminoPubKey) GetPubKeys() []cryptotypes.PubKey {
	if m != nil {
		pubKeys := make([]cryptotypes.PubKey, len(m.PubKeys))
		for i := 0; i < len(m.PubKeys); i++ {
			pubKeys[i] = m.PubKeys[i].GetCachedValue().(cryptotypes.PubKey)
		}
		return pubKeys
	}

	return nil
}

// Equals returns true if m and other both have the same number of keys, and
// all constituent keys are the same, and in the same order.
func (m *LegacyAminoPubKey) Equals(key cryptotypes.PubKey) bool {
	otherKey, ok := key.(multisigtypes.PubKey)
	if !ok {
		return false
	}
	pubKeys := m.GetPubKeys()
	otherPubKeys := otherKey.GetPubKeys()
	if m.GetThreshold() != otherKey.GetThreshold() || len(pubKeys) != len(otherPubKeys) {
		return false
	}

	for i := 0; i < len(pubKeys); i++ {
		if !pubKeys[i].Equals(otherPubKeys[i]) {
			return false
		}
	}
	return true
}

// GetThreshold implements the PubKey.GetThreshold method
func (m *LegacyAminoPubKey) GetThreshold() uint {
	return uint(m.Threshold)
}

// Type returns multisig type
func (m *LegacyAminoPubKey) Type() string {
	return "PubKeyMultisigThreshold"
}

// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (m *LegacyAminoPubKey) UnpackInterfaces(unpacker types.AnyUnpacker) error {
	for _, any := range m.PubKeys {
		var pk cryptotypes.PubKey
		err := unpacker.UnpackAny(any, &pk)
		if err != nil {
			return err
		}
	}
	return nil
}

func packPubKeys(pubKeys []cryptotypes.PubKey) ([]*types.Any, error) {
	anyPubKeys := make([]*types.Any, len(pubKeys))

	for i := 0; i < len(pubKeys); i++ {
		any, err := types.NewAnyWithValue(pubKeys[i])
		if err != nil {
			return nil, err
		}
		anyPubKeys[i] = any
	}
	return anyPubKeys, nil
}
