package torrent

import (
	"context"
	"strconv"
	"strings"

	"github.com/anacrolix/chansync/events"
	"github.com/anacrolix/missinggo/v2/pubsub"
	"github.com/anacrolix/sync"

	"github.com/anacrolix/torrent/metainfo"
)

// The Torrent's infohash. This is fixed and cannot change. It uniquely
// identifies a torrent. TODO: If this doesn't change, should we stick to
// referring to a Torrent by the original infohash given to us?
func (t *Torrent) InfoHash() metainfo.Hash {
	return *t.canonicalShortInfohash()
}

// Returns a channel that is closed when the info (.Info()) for the torrent has become available.
func (t *Torrent) GotInfo() events.Done {
	return t.gotMetainfoC
}

// Returns the metainfo info dictionary, or nil if it's not yet available.
func (t *Torrent) Info() (info *metainfo.Info) {
	t.nameMu.RLock()
	info = t.info
	t.nameMu.RUnlock()
	return
}

// Returns a Reader bound to the torrent's data. All read calls block until the data requested is
// actually available. Note that you probably want to ensure the Torrent Info is available first.
func (t *Torrent) NewReader() Reader {
	return t.newReader(0, t.length())
}

func (t *Torrent) newReader(offset, length int64) Reader {
	r := reader{
		mu:     t.cl.locker(),
		t:      t,
		offset: offset,
		length: length,
		ctx:    context.Background(),
	}
	r.readaheadFunc = defaultReadaheadFunc
	t.addReader(&r)
	return &r
}

type PieceStateRuns []PieceStateRun

func (me PieceStateRuns) String() (s string) {
	if len(me) > 0 {
		var sb strings.Builder
		sb.WriteString(me[0].String())
		for i := 1; i < len(me); i += 1 {
			sb.WriteByte(' ')
			sb.WriteString(me[i].String())
		}
		return sb.String()
	}
	return
}

// Returns the state of pieces of the torrent. They are grouped into runs of same state. The sum of
// the state run-lengths is the number of pieces in the torrent.
func (t *Torrent) PieceStateRuns() (runs PieceStateRuns) {
	t.cl.rLock()
	runs = t.pieceStateRuns()
	t.cl.rUnlock()
	return
}

func (t *Torrent) PieceState(piece pieceIndex) (ps PieceState) {
	t.cl.rLock()
	ps = t.pieceState(piece)
	t.cl.rUnlock()
	return
}

// The number of pieces in the torrent. This requires that the info has been
// obtained first.
func (t *Torrent) NumPieces() pieceIndex {
	return t.numPieces()
}

// Get missing bytes count for specific piece.
func (t *Torrent) PieceBytesMissing(piece int) int64 {
	t.cl.rLock()
	defer t.cl.rUnlock()

	return int64(t.pieces[piece].bytesLeft())
}

// Drop the torrent from the client, and close it. It's always safe to do this. No data corruption
// can, or should occur to either the torrent's data, or connected peers.
func (t *Torrent) Drop() {
	if t.closed.IsSet() {
		return
	}
	t.cl.lock()
	defer t.cl.unlock()
	if t.closed.IsSet() {
		return
	}
	var wg sync.WaitGroup
	t.close(&wg)
	wg.Wait()
}

// Number of bytes of the entire torrent we have completed. This is the sum of
// completed pieces, and dirtied chunks of incomplete pieces. Do not use this
// for download rate, as it can go down when pieces are lost or fail checks.
// Sample Torrent.Stats.DataBytesRead for actual file data download rate.
func (t *Torrent) BytesCompleted() int64 {
	t.cl.rLock()
	defer t.cl.rUnlock()
	return t.bytesCompleted()
}

// The subscription emits as (int) the index of pieces as their state changes.
// A state change is when the PieceState for a piece alters in value.
func (t *Torrent) SubscribePieceStateChanges() *pubsub.Subscription[PieceStateChange] {
	return t.pieceStateChanges.Subscribe()
}

// Returns true if the torrent is currently being seeded. This occurs when the
// client is willing to upload without wanting anything in return.
func (t *Torrent) Seeding() (ret bool) {
	t.cl.rLock()
	ret = t.seeding()
	t.cl.rUnlock()
	return
}

// Clobbers the torrent display name if metainfo is unavailable.
// The display name is used as the torrent name while the metainfo is unavailable.
func (t *Torrent) SetDisplayName(dn string) {
	t.nameMu.Lock()
	if !t.haveInfo() {
		t.displayName = dn
	}
	t.nameMu.Unlock()
}

// The current working name for the torrent. Either the name in the info dict,
// or a display name given such as by the dn value in a magnet link, or "".
func (t *Torrent) Name() string {
	return t.name()
}

// The completed length of all the torrent data, in all its files. This is
// derived from the torrent info, when it is available.
func (t *Torrent) Length() int64 {
	return t._length.Value
}

// Returns a run-time generated metainfo for the torrent that includes the
// info bytes and announce-list as currently known to the client.
func (t *Torrent) Metainfo() metainfo.MetaInfo {
	t.cl.rLock()
	defer t.cl.rUnlock()
	return t.newMetaInfo()
}

func (t *Torrent) addReader(r *reader) {
	t.cl.lock()
	defer t.cl.unlock()
	if t.readers == nil {
		t.readers = make(map[*reader]struct{})
	}
	t.readers[r] = struct{}{}
	r.posChanged()
}

func (t *Torrent) deleteReader(r *reader) {
	delete(t.readers, r)
	t.readersChanged()
}

// Raise the priorities of pieces in the range [begin, end) to at least Normal
// priority. Piece indexes are not the same as bytes. Requires that the info
// has been obtained, see Torrent.Info and Torrent.GotInfo.
func (t *Torrent) DownloadPieces(begin, end pieceIndex) {
	t.cl.lock()
	t.downloadPiecesLocked(begin, end)
	t.cl.unlock()
}

func (t *Torrent) downloadPiecesLocked(begin, end pieceIndex) {
	for i := begin; i < end; i++ {
		if t.pieces[i].priority.Raise(PiecePriorityNormal) {
			t.updatePiecePriority(i, "Torrent.DownloadPieces")
		}
	}
}

func (t *Torrent) CancelPieces(begin, end pieceIndex) {
	t.cl.lock()
	t.cancelPiecesLocked(begin, end, "Torrent.CancelPieces")
	t.cl.unlock()
}

func (t *Torrent) cancelPiecesLocked(begin, end pieceIndex, reason updateRequestReason) {
	for i := begin; i < end; i++ {
		p := t.piece(i)
		// Intentionally cancelling only the piece-specific priority here.
		if p.priority == PiecePriorityNone {
			continue
		}
		p.priority = PiecePriorityNone
		t.updatePiecePriority(i, reason)
	}
}

func (t *Torrent) initFiles() {
	info := t.info
	var offset int64
	t.files = new([]*File)
	for _, fi := range t.info.UpvertedFiles() {
		*t.files = append(*t.files, &File{
			t,
			strings.Join(append([]string{info.BestName()}, fi.BestPath()...), "/"),
			offset,
			fi.Length,
			fi,
			fi.DisplayPath(info),
			PiecePriorityNone,
			fi.PiecesRoot,
		})
		offset += fi.Length
		if info.FilesArePieceAligned() {
			offset = (offset + info.PieceLength - 1) / info.PieceLength * info.PieceLength
		}
	}
}

// Returns handles to the files in the torrent. This requires that the Info is
// available first.
func (t *Torrent) Files() []*File {
	return *t.files
}

func (t *Torrent) AddPeers(pp []PeerInfo) (n int) {
	t.cl.lock()
	defer t.cl.unlock()
	n = t.addPeers(pp)
	return
}

// Marks the entire torrent for download. Requires the info first, see
// GotInfo. Sets piece priorities for historical reasons.
func (t *Torrent) DownloadAll() {
	t.DownloadPieces(0, t.numPieces())
}

func (t *Torrent) String() string {
	s := t.name()
	if s == "" {
		return t.canonicalShortInfohash().HexString()
	} else {
		return strconv.Quote(s)
	}
}

func (t *Torrent) AddTrackers(announceList [][]string) {
	t.cl.lock()
	defer t.cl.unlock()
	t.addTrackers(announceList)
}

func (t *Torrent) ModifyTrackers(announceList [][]string) {
	t.cl.lock()
	defer t.cl.unlock()
	t.modifyTrackers(announceList)
}

func (t *Torrent) Piece(i pieceIndex) *Piece {
	return t.piece(i)
}

func (t *Torrent) PeerConns() []*PeerConn {
	t.cl.rLock()
	defer t.cl.rUnlock()
	ret := make([]*PeerConn, 0, len(t.conns))
	for c := range t.conns {
		ret = append(ret, c)
	}
	return ret
}

// TODO: Misleading method name. Webseed peers are not PeerConns.
func (t *Torrent) WebseedPeerConns() []*Peer {
	t.cl.rLock()
	defer t.cl.rUnlock()
	ret := make([]*Peer, 0, len(t.conns))
	for _, c := range t.webSeeds {
		ret = append(ret, &c.peer)
	}
	return ret
}
