package v2

import (
	"errors"

	"github.com/gogo/protobuf/proto"
	"github.com/tendermint/tendermint/p2p"
	bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain"
	"github.com/tendermint/tendermint/state"
	"github.com/tendermint/tendermint/types"
)

var (
	errPeerQueueFull = errors.New("peer queue full")
)

type iIO interface {
	sendBlockRequest(peer p2p.Peer, height int64) error
	sendBlockToPeer(block *types.Block, peer p2p.Peer) error
	sendBlockNotFound(height int64, peer p2p.Peer) error
	sendStatusResponse(base, height int64, peer p2p.Peer) error

	sendStatusRequest(peer p2p.Peer) error
	broadcastStatusRequest() error

	trySwitchToConsensus(state state.State, skipWAL bool) bool
}

type switchIO struct {
	sw *p2p.Switch
}

func newSwitchIo(sw *p2p.Switch) *switchIO {
	return &switchIO{
		sw: sw,
	}
}

const (
	// BlockchainChannel is a channel for blocks and status updates (`BlockStore` height)
	BlockchainChannel = byte(0x40)
)

type consensusReactor interface {
	// for when we switch from blockchain reactor and fast sync to
	// the consensus machine
	SwitchToConsensus(state state.State, skipWAL bool)
}

func (sio *switchIO) sendBlockRequest(peer p2p.Peer, height int64) error {
	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_BlockRequest{
			BlockRequest: &bcproto.BlockRequest{
				Height: height,
			},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	queued := peer.TrySend(BlockchainChannel, msgBytes)
	if !queued {
		return errPeerQueueFull
	}
	return nil
}

func (sio *switchIO) sendStatusResponse(base int64, height int64, peer p2p.Peer) error {
	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_StatusResponse{
			StatusResponse: &bcproto.StatusResponse{
				Height: height,
				Base:   base,
			},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued {
		return errPeerQueueFull
	}

	return nil
}

func (sio *switchIO) sendBlockToPeer(block *types.Block, peer p2p.Peer) error {
	if block == nil {
		panic("trying to send nil block")
	}

	bpb, err := block.ToProto()
	if err != nil {
		return err
	}

	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_BlockResponse{
			BlockResponse: &bcproto.BlockResponse{
				Block: bpb,
			},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued {
		return errPeerQueueFull
	}

	return nil
}

func (sio *switchIO) sendBlockNotFound(height int64, peer p2p.Peer) error {
	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_NoBlockResponse{
			NoBlockResponse: &bcproto.NoBlockResponse{
				Height: height,
			},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued {
		return errPeerQueueFull
	}

	return nil
}

func (sio *switchIO) trySwitchToConsensus(state state.State, skipWAL bool) bool {
	conR, ok := sio.sw.Reactor("CONSENSUS").(consensusReactor)
	if ok {
		conR.SwitchToConsensus(state, skipWAL)
	}
	return ok
}

func (sio *switchIO) sendStatusRequest(peer p2p.Peer) error {
	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_StatusRequest{
			StatusRequest: &bcproto.StatusRequest{},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued {
		return errPeerQueueFull
	}

	return nil
}

func (sio *switchIO) broadcastStatusRequest() error {
	msgProto := &bcproto.Message{
		Sum: &bcproto.Message_StatusRequest{
			StatusRequest: &bcproto.StatusRequest{},
		},
	}

	msgBytes, err := proto.Marshal(msgProto)
	if err != nil {
		return err
	}

	// XXX: maybe we should use an io specific peer list here
	sio.sw.Broadcast(BlockchainChannel, msgBytes)

	return nil
}
