package downloader

import (
	"errors"
	"math/big"
	"sync"

	"github.com/ethereum/go-ethereum/common"
	"gopkg.in/fatih/set.v0"
)

const (
	workingState = 2
	idleState    = 4
)

type hashFetcherFn func(common.Hash) error
type blockFetcherFn func([]common.Hash) error

// XXX make threadsafe!!!!
type peers map[string]*peer

func (p peers) reset() {
	for _, peer := range p {
		peer.reset()
	}
}

func (p peers) get(state int) []*peer {
	var peers []*peer
	for _, peer := range p {
		peer.mu.RLock()
		if peer.state == state {
			peers = append(peers, peer)
		}
		peer.mu.RUnlock()
	}

	return peers
}

func (p peers) setState(id string, state int) {
	if peer, exist := p[id]; exist {
		peer.mu.Lock()
		defer peer.mu.Unlock()
		peer.state = state
	}
}

func (p peers) getPeer(id string) *peer {
	return p[id]
}

func (p peers) bestPeer() *peer {
	var peer *peer
	for _, cp := range p {
		if peer == nil || cp.td.Cmp(peer.td) > 0 {
			peer = cp
		}
	}
	return peer
}

// peer represents an active peer
type peer struct {
	state int // Peer state (working, idle)
	rep   int // TODO peer reputation

	mu         sync.RWMutex
	id         string
	td         *big.Int
	recentHash common.Hash

	ignored *set.Set

	getHashes hashFetcherFn
	getBlocks blockFetcherFn
}

// create a new peer
func newPeer(id string, td *big.Int, hash common.Hash, getHashes hashFetcherFn, getBlocks blockFetcherFn) *peer {
	return &peer{
		id:         id,
		td:         td,
		recentHash: hash,
		getHashes:  getHashes,
		getBlocks:  getBlocks,
		state:      idleState,
		ignored:    set.New(),
	}
}

// fetch a chunk using the peer
func (p *peer) fetch(chunk *chunk) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.state == workingState {
		return errors.New("peer already fetching chunk")
	}

	// set working state
	p.state = workingState
	// convert the set to a fetchable slice
	hashes, i := make([]common.Hash, chunk.hashes.Size()), 0
	chunk.hashes.Each(func(v interface{}) bool {
		hashes[i] = v.(common.Hash)
		i++
		return true
	})
	p.getBlocks(hashes)

	return nil
}

// promote increases the peer's reputation
func (p *peer) promote() {
	p.mu.Lock()
	defer p.mu.Unlock()

	p.rep++
}

// demote decreases the peer's reputation or leaves it at 0
func (p *peer) demote() {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.rep > 1 {
		p.rep -= 2
	} else {
		p.rep = 0
	}
}

func (p *peer) reset() {
	p.state = idleState
	p.ignored.Clear()
}
