/*
 * MumbleDJ
 * By Matthieu Grieger
 * bot/cache.go
 * Copyright (c) 2016 Matthieu Grieger (MIT License)
 */

package bot

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/spf13/viper"
)

// SortFilesByAge is a type that holds file information for cached items for
// sorting.
type SortFilesByAge []os.FileInfo

// Len returns the length of the file slice.
func (a SortFilesByAge) Len() int {
	return len(a)
}

// Swap swaps two elements in the file slice.
func (a SortFilesByAge) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

// Less compares two file modification times to determine if one is less than
// the other. Returns true if the item in index i is older than the item in
// index j, false otherwise.
func (a SortFilesByAge) Less(i, j int) bool {
	return time.Since(a[i].ModTime()) < time.Since(a[j].ModTime())
}

// Cache keeps track of the filesize of the audio cache and
// provides methods for pruning the cache.
type Cache struct {
	NumAudioFiles int
	TotalFileSize int64
}

// NewCache creates an empty Cache and returns it.
func NewCache() *Cache {
	return &Cache{
		NumAudioFiles: 0,
		TotalFileSize: 0,
	}
}

// CheckDirectorySize checks the cache directory to determine if the filesize
// of the files within exceed the user-specified size limit. If so, the oldest
// files are cleared until it is no longer exceeding the limit.
func (c *Cache) CheckDirectorySize() {
	const bytesInMiB int = 1048576

	c.UpdateStatistics()
	for c.TotalFileSize > int64(viper.GetInt("cache.maximum_size")*bytesInMiB) {
		if err := c.DeleteOldest(); err != nil {
			break
		}
	}
}

// UpdateStatistics updates the statistics relevant to the cache (number of
// audio files cached, total current size of the cache).
func (c *Cache) UpdateStatistics() {
	c.NumAudioFiles, c.TotalFileSize = c.getCurrentStatistics()
	logrus.WithFields(logrus.Fields{
		"num_audio_files": c.NumAudioFiles,
		"total_file_size": c.TotalFileSize,
	}).Infoln("Updated cache statistics.")
}

// CleanPeriodically loops forever, deleting expired cached audio files as necessary.
func (c *Cache) CleanPeriodically() {
	for range time.Tick(time.Duration(viper.GetInt("cache.check_interval")) * time.Minute) {
		logrus.Infoln("Checking cache for expired files...")
		files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
		for _, file := range files {
			// It is safe to check the modification time because when audio files are
			// played their modification time is updated. This ensures that audio
			// files will not get deleted while they are playing, assuming a reasonable
			// expiry time is set in the configuration.
			hours := time.Since(file.ModTime()).Hours()
			if hours >= viper.GetFloat64("cache.expire_time") {
				logrus.WithFields(logrus.Fields{
					"expired_file": file.Name(),
				}).Infoln("Removing expired cache entry.")
				os.Remove(fmt.Sprintf("%s/%s", os.ExpandEnv(viper.GetString("cache.directory")), file.Name()))
			}
		}
	}
}

// DeleteOldest deletes the oldest file in the cache.
func (c *Cache) DeleteOldest() error {
	files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
	if len(files) > 0 {
		sort.Sort(SortFilesByAge(files))
		os.Remove(fmt.Sprintf("%s/%s", os.ExpandEnv(viper.GetString("cache.directory")), files[0].Name()))
		return nil
	}
	return errors.New("There are no files currently cached")
}

// DeleteAll deletes all cached audio files.
func (c *Cache) DeleteAll() error {
	dir, err := os.Open(os.ExpandEnv(viper.GetString("cache.directory")))
	if err != nil {
		return err
	}

	defer dir.Close()
	names, err := dir.Readdirnames(-1)
	if err != nil {
		return err
	}
	logrus.Infoln("Deleting all cached audio files...")
	for _, name := range names {
		err = os.RemoveAll(filepath.Join(os.ExpandEnv(viper.GetString("cache.directory")), name))
		if err != nil {
			return err
		}
	}
	return nil
}

func (c *Cache) getCurrentStatistics() (int, int64) {
	var totalSize int64
	files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
	for _, file := range files {
		totalSize += file.Size()
	}
	return len(files), totalSize
}
