package housekeeper

import (
	"fmt"
	"os"
	"strconv"
	"sync"
	"time"
)

// CachedBeatmap represents a beatmap that is held in the cache of CheeseGull.
type CachedBeatmap struct {
	ID         int
	NoVideo    bool
	LastUpdate time.Time

	lastRequested time.Time

	fileSize     uint64
	isDownloaded bool
	mtx          sync.RWMutex
	waitGroup    sync.WaitGroup
}

// File opens the File of the beatmap from the filesystem.
func (c *CachedBeatmap) File() (*os.File, error) {
	return os.Open(c.fileName())
}

// CreateFile creates the File of the beatmap in the filesystem, and returns it
// in write mode.
func (c *CachedBeatmap) CreateFile() (*os.File, error) {
	return os.Create(c.fileName())
}

func (c *CachedBeatmap) fileName() string {
	n := ""
	if c.NoVideo {
		n = "n"
	}
	return "data/" + strconv.Itoa(c.ID) + n + ".osz"
}

// IsDownloaded checks whether the beatmap has been downloaded.
func (c *CachedBeatmap) IsDownloaded() bool {
	c.mtx.RLock()
	i := c.isDownloaded
	c.mtx.RUnlock()
	return i
}

// FileSize returns the FileSize of c.
func (c *CachedBeatmap) FileSize() uint64 {
	c.mtx.RLock()
	i := c.fileSize
	c.mtx.RUnlock()
	return i
}

// MustBeDownloaded will check whether the beatmap is downloaded.
// If it is not, it will wait for it to become downloaded.
func (c *CachedBeatmap) MustBeDownloaded() {
	if c.IsDownloaded() {
		return
	}
	c.waitGroup.Wait()
}

// DownloadCompleted must be called once the beatmap has finished downloading.
func (c *CachedBeatmap) DownloadCompleted(fileSize uint64, parentHouse *House) {
	c.mtx.Lock()
	c.fileSize = fileSize
	c.isDownloaded = true
	c.mtx.Unlock()
	c.waitGroup.Done()
	parentHouse.scheduleCleanup()
}

// SetLastRequested changes the last requested time.
func (c *CachedBeatmap) SetLastRequested(t time.Time) {
	c.mtx.Lock()
	c.lastRequested = t
	c.mtx.Unlock()
}

func (c *CachedBeatmap) String() string {
	return fmt.Sprintf("{ID: %d NoVideo: %t LastUpdate: %v}", c.ID, c.NoVideo, c.LastUpdate)
}

// AcquireBeatmap attempts to add a new CachedBeatmap to the state.
// In order to add a new CachedBeatmap to the state, one must not already exist
// in the state with the same ID, NoVideo and LastUpdate. In case one is already
// found, this is returned, alongside with false. If LastUpdate is newer than
// that of the beatmap stored in the state, then the beatmap in the state's
// downloaded status is switched back to false and the LastUpdate is changed.
// true is also returned, indicating that the caller now has the burden of
// downloading the beatmap.
//
// In the case the cachedbeatmap has not been stored in the state, then
// it is added to the state and, like the case where LastUpdated has been
// changed, true is returned, indicating that the caller must now download the
// beatmap.
//
// If you're confused attempting to read this, let me give you an example:
//
//   A: Yo, is this beatmap cached?
//   B: Yes, yes it is! Here you go with the information about it. No need to do
//      anything else.
//      ----
//   A: Yo, got this beatmap updated 2 hours ago. Have you got it cached?
//   B: Ah, I'm afraid that I only have the version updated 10 hours ago.
//      Mind downloading the updated version for me?
//      ----
//   A: Yo, is this beatmap cached?
//   B: Nope, I didn't know it existed before you told me. I've recorded its
//      info now, but jokes on you, you now have to actually download it.
//      Chop chop!
func (h *House) AcquireBeatmap(c *CachedBeatmap) (*CachedBeatmap, bool) {
	if c == nil {
		return nil, false
	}

	h.stateMutex.RLock()
	for _, b := range h.state {
		// if the id or novideo is different, then all is good and we
		// can proceed with the next element.
		if b.ID != c.ID || b.NoVideo != c.NoVideo {
			continue
		}
		// unlocking because in either branch, we will return.
		h.stateMutex.RUnlock()

		// if c is not newer than b, then just return.
		if !b.LastUpdate.Before(c.LastUpdate) {
			return b, false
		}

		b.LastUpdate = c.LastUpdate
		return b, true
	}
	h.stateMutex.RUnlock()

	// c was not present in our state: we need to add it.

	// we need to recreate the CachedBeatmap: this way we can be sure the zero
	// is set for the unexported fields.
	n := &CachedBeatmap{
		ID:         c.ID,
		NoVideo:    c.NoVideo,
		LastUpdate: c.LastUpdate,
	}
	n.waitGroup.Add(1)
	h.stateMutex.Lock()
	h.state = append(h.state, n)
	h.stateMutex.Unlock()

	return n, true
}
