package playhead

import (
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"code.justin.tv/video/spectre/backend"
	"code.justin.tv/video/spectre/usher"
	"code.justin.tv/video/spectre/vods"

	"github.com/stvp/rollbar"
)

// Playhead contains all the data necessary to serve an active offline playlist
type Playhead struct {
	ChunkIndex         int
	XTwitchElapsedSecs float64 // TODO: fix bug where on spectre restart, this value gets reset to 0
	Channel            backend.Channel
	CurrentVod         *vods.Vod
	NextVod            *vods.Vod
	NumVods            int
	Quit               chan struct{}
	Closing            bool
}

var (
	// Backend used to query for data
	Backend               *backend.Backend
	playheadRefreshPeriod = 4 * time.Second
	playheadMaxLife       = 24 * time.Hour
	playheads             map[int]*Playhead
	playheadsMutex        *sync.RWMutex
)

func init() {
	playheads = make(map[int]*Playhead)
	playheadsMutex = &sync.RWMutex{}
}

func NumPlayheads() int {
	return len(playheads)
}

// GetPlayhead returns the playhead for a channel id,
// or nil if there is no such playhead
func GetPlayhead(channelID int) (*Playhead, bool) {
	playheadsMutex.RLock()
	defer playheadsMutex.RUnlock()
	playhead, found := playheads[channelID]
	return playhead, found
}

func setPlayhead(channelID int, playhead *Playhead) {
	playheadsMutex.Lock()
	defer playheadsMutex.Unlock()
	playheads[channelID] = playhead
}

// InitPlayhead initializes a new playhead for a channel that has just gone live
func InitPlayhead(channel backend.Channel) {
	now := time.Now()
	playlist, err := Backend.GetPlaylist(channel.ID, channel.ActivePlaylistID)
	if err != nil {
		fmt.Printf("Canceling channel %v's playhead creation. Channel owns no playlist of id %v\n", channel.ID, channel.ActivePlaylistID)
		return
	}

	p := &Playhead{
		Channel: channel,
		Quit:    make(chan struct{}),
	}

	p.NumVods = len(playlist.VodIDs())
	if p.NumVods == 0 {
		return
	}

	p.Channel.ActiveVodIndex = p.Channel.ActiveVodIndex % p.NumVods
	p.CurrentVod, p.Channel.ActiveVodIndex = vods.InitializeVod(p.Channel.ID, playlist.VodIDs(), p.Channel.ActiveVodIndex)
	if p.CurrentVod == nil {
		return
	}
	p.Channel.NextVodIndex = (p.Channel.ActiveVodIndex + 1) % p.NumVods
	p.NextVod, p.Channel.NextVodIndex = vods.InitializeVod(p.Channel.ID, playlist.VodIDs(), p.Channel.NextVodIndex)
	if p.NextVod == nil {
		return
	}

	setPlayhead(p.Channel.ID, p)

	fmt.Printf("Initialized %v's playhead in %v seconds\n", p.Channel.ID, time.Now().Sub(now).Seconds())

	go func() {
		// In case the playhead is no longer used, we don't want each spectre box
		// to eventually have all playheads. Therefore, destroy playhead periodically.
		// Alternative is to have a TTL on playhead but this would involve saving
		// whenever playhead is accessed, which is expensive.

		numIters := int(playheadMaxLife/playheadRefreshPeriod) + 1
		for i := 0; i < numIters; i++ {
			timer := time.NewTimer(playheadRefreshPeriod)
			ph, found := GetPlayhead(channel.ID)
			if !found {
				return
			}
			select {
			case <-timer.C:
				ph.advancePlayhead()
			case <-ph.Quit:
				return
			}
		}
		deletePlayhead(channel.ID)
	}()
}

func deletePlayhead(channelID int) {
	playheadsMutex.Lock()
	defer playheadsMutex.Unlock()
	delete(playheads, channelID)
	fmt.Printf("Deleted playhead for channel %d\n", channelID)
}

func FreezePlayhead(channelID int) {
	playheadsMutex.Lock()
	defer playheadsMutex.Unlock()
	if playheads[channelID].Closing == true {
		return
	}

	fmt.Printf("Froze playhead for channel %d\n", channelID)

	close(playheads[channelID].Quit)
	playheads[channelID].Closing = true
	go func() {
		time.Sleep(20 * time.Second)
		deletePlayhead(channelID)
	}()
}

func (p *Playhead) advancePlayhead() {
	if p.Closing {
		return
	}
	currentVodManifest := p.CurrentVod.Manifests["chunked"]
	transitioned := false
	var xMediaSequenceIndexOnTransition int

	p2 := *p
	newChannel := p.Channel
	p2.Channel = newChannel

	for {

		for {
			if p2.ChunkIndex >= len(currentVodManifest.Chunks) {
				p2.ChunkIndex = 0

				p2.Channel.ActiveVodIndex = p2.Channel.NextVodIndex
				playlist, err := Backend.GetPlaylist(p2.Channel.ID, p2.Channel.ActivePlaylistID)
				if err != nil {
					return
				}

				p2.Channel.StartTime = p2.Channel.StartTime.Add(currentVodManifest.Duration)
				currentVodManifest = p2.NextVod.Manifests["chunked"]
				p2.CurrentVod = p2.NextVod
				p2.Channel.NextVodIndex = (p2.Channel.NextVodIndex + 1) % len(playlist.VodIDs())
				p2.NextVod, p2.Channel.NextVodIndex = vods.InitializeVod(p2.Channel.ID, playlist.VodIDs(), p2.Channel.NextVodIndex)
				if p2.NextVod == nil {
					return
				}

				transitioned = true
				xMediaSequenceIndexOnTransition = p2.Channel.XMediaSequenceIndex

				fmt.Printf("Channel %v advanced to vod index %v (id: %v) at %v\n",
					p.Channel.ID,
					p.Channel.ActiveVodIndex,
					p.CurrentVod.ID,
					p.Channel.StartTime,
				)
			} else {
				break
			}
		}
		if p2.Channel.StartTime.Add(currentVodManifest.Chunks[p2.ChunkIndex].StartTime).After(time.Now().UTC()) {
			break
		}
		p2.Channel.XMediaSequenceIndex++
		p2.XTwitchElapsedSecs += currentVodManifest.Chunks[p2.ChunkIndex].Duration.Seconds()
		p2.ChunkIndex++
	}

	setPlayhead(p2.Channel.ID, &p2)

	if transitioned {
		Backend.TransitionVod(p2.Channel.ID, p2.Channel.ActiveVodIndex, p2.Channel.StartTime, xMediaSequenceIndexOnTransition)
		usher.UsherService{}.Update(p2.Channel.ID, p2.CurrentVod.Properties)
	}
}

func (p *Playhead) MarshalJSON() ([]byte, error) {
	if p.Closing {
		j, err := json.Marshal(nil)
		if err != nil {
			rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
		}
		return j, err
	}
	chunks := p.CurrentVod.Manifests["chunked"].Chunks

	if len(chunks) <= p.ChunkIndex {
		j, err := json.Marshal(nil)
		if err != nil {
			rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
		}
		return j, err
	}
	currentChunkStartTime := chunks[p.ChunkIndex].StartTime
	currentVodDuration := chunks[len(chunks)-1].StartTime + chunks[len(chunks)-1].Duration
	j, err := json.Marshal(map[string]interface{}{
		"playlist_id":                     p.Channel.ActivePlaylistID,
		"vods":                            [2]*vods.Vod{p.CurrentVod, p.NextVod},
		"active_vod_index":                0,
		"remaining_seconds_in_active_vod": (currentVodDuration - currentChunkStartTime).Seconds(),
	})
	if err != nil {
		rollbar.ErrorWithStack(rollbar.WARN, err, rollbar.BuildStack(0))
	}
	return j, err
}
