package webassets

import (
	"embed"
	"fmt"
	"github.com/rs/zerolog/log"
	"io"
	bunny "locbunny"
	"os"
	"path/filepath"
	"sync"
	"time"
)

//go:embed *.html
//go:embed *.ico
//go:embed scripts/*.js
//go:embed css/*.css
//go:embed fonts/*.woff
//go:embed fonts/*.woff2
//go:embed icons/*.svg
//go:embed icons/*.png
var _assets embed.FS

type templateCacheEntry struct {
	MDate time.Time
	Value ITemplate
}

type fileCacheEntry struct {
	MDate time.Time
	Value []byte
}

type Assets struct {
	templateCache map[string]templateCacheEntry
	fileCache     map[string]fileCacheEntry
	lock          sync.RWMutex
}

func NewAssets() *Assets {
	return &Assets{
		templateCache: make(map[string]templateCacheEntry, 128),
		fileCache:     make(map[string]fileCacheEntry, 128),
		lock:          sync.RWMutex{},
	}
}

type ITemplate interface {
	Execute(wr io.Writer, data any) error
}

func (a *Assets) ListAssets() []string {
	result := make([]string, 0)

	entries, err := _assets.ReadDir(".")
	if err != nil {
		panic(err)
	}

	for _, entry := range entries {
		if entry.IsDir() {
			panic("TODO implement recursion")
		}

		if entry.Name() == "index.html" {
			continue
		}

		result = append(result, "/"+entry.Name())
	}

	return result
}

func (a *Assets) Read(fp string) ([]byte, error) {
	if bunny.Conf.LiveReload == nil {

		// no live-reload: use embedded data

		bin, err := _assets.ReadFile(fp)
		if err != nil {
			return nil, err
		}
		return bin, nil

	} else {

		liveFP := filepath.Join(*bunny.Conf.LiveReload, fp)

		stat, err := os.Stat(liveFP)
		if err != nil {
			return nil, err
		}

		a.lock.RLock()
		v, ok := a.fileCache[fp]
		a.lock.RUnlock()

		if !ok {

			// initial load

			bin, err := os.ReadFile(liveFP)
			if err != nil {
				return nil, err
			}

			a.lock.Lock()
			a.fileCache[fp] = fileCacheEntry{MDate: stat.ModTime(), Value: bin}
			a.lock.Unlock()

			return bin, nil

		} else if v.MDate != stat.ModTime() {

			// live reload

			log.Info().Msg(fmt.Sprintf("[>>] Live reload file '%s' from filesystem (file changed)", fp))

			bin, err := os.ReadFile(liveFP)
			if err != nil {
				return nil, err
			}

			a.lock.Lock()
			a.fileCache[fp] = fileCacheEntry{MDate: stat.ModTime(), Value: bin}
			a.lock.Unlock()

			return bin, nil

		} else {
			// return from cache
			return v.Value, nil
		}
	}
}

func (a *Assets) Template(fp string, builder func([]byte) (ITemplate, error)) (ITemplate, error) {
	if bunny.Conf.LiveReload == nil {

		// no live-reload: use embedded data, and permanently cache template

		a.lock.RLock()
		v, ok := a.templateCache[fp]
		a.lock.RUnlock()
		if ok {
			return v.Value, nil
		}

		bin, err := _assets.ReadFile(fp)
		if err != nil {
			return nil, err
		}
		t, err := builder(bin)
		if err != nil {
			panic(err)
		}

		a.lock.Lock()
		a.templateCache[fp] = templateCacheEntry{MDate: time.Now(), Value: t}
		a.lock.Unlock()

		return t, nil

	} else {

		a.lock.RLock()
		v, ok := a.templateCache[fp]
		a.lock.RUnlock()

		liveFP := filepath.Join(*bunny.Conf.LiveReload, fp)

		stat, err := os.Stat(liveFP)
		if err != nil {
			return nil, err
		}

		if !ok {

			// initial load

			bin, err := os.ReadFile(liveFP)
			if err != nil {
				return nil, err
			}
			t, err := builder(bin)
			if err != nil {
				return nil, err
			}

			a.lock.Lock()
			a.templateCache[fp] = templateCacheEntry{MDate: stat.ModTime(), Value: t}
			a.lock.Unlock()

			return t, nil

		} else if v.MDate != stat.ModTime() {

			// live reload

			log.Info().Msg(fmt.Sprintf("[>>] Live reload file '%s' from filesystem (file changed)", fp))

			bin, err := os.ReadFile(liveFP)
			if err != nil {
				return nil, err
			}
			t, err := builder(bin)
			if err != nil {
				return nil, err
			}

			a.lock.Lock()
			a.templateCache[fp] = templateCacheEntry{MDate: stat.ModTime(), Value: t}
			a.lock.Unlock()

			return t, nil

		} else {
			// return from cache
			return v.Value, nil
		}
	}
}
