package cursor

import (
	"reflect"
	"sync"

	"github.com/rs/zerolog"

	"github.com/m1k1o/neko/server/pkg/types"
	"github.com/m1k1o/neko/server/pkg/utils"
)

type ImageListener interface {
	SendCursorImage(cur *types.CursorImage, img []byte) error
}

type Image interface {
	Start()
	Shutdown()
	GetCurrent() (cur *types.CursorImage, img []byte, err error)
	AddListener(listener ImageListener)
	RemoveListener(listener ImageListener)
}

type imageEntry struct {
	*types.CursorImage
	ImagePNG []byte
}

type image struct {
	logger  zerolog.Logger
	desktop types.DesktopManager

	listeners   map[uintptr]ImageListener
	listenersMu sync.RWMutex

	cache     map[uint64]*imageEntry
	cacheMu   sync.RWMutex
	current   *imageEntry
	maxSerial uint64
}

func NewImage(logger zerolog.Logger, desktop types.DesktopManager) *image {
	return &image{
		logger:    logger.With().Str("submodule", "cursor-image").Logger(),
		desktop:   desktop,
		listeners: map[uintptr]ImageListener{},
		cache:     map[uint64]*imageEntry{},
		maxSerial: 300, // TODO: Cleanup?
	}
}

func (manager *image) Start() {
	manager.desktop.OnCursorChanged(func(serial uint64) {
		entry, err := manager.getCached(serial)
		if err != nil {
			manager.logger.Err(err).Msg("failed to get cursor image")
			return
		}

		manager.current = entry

		manager.listenersMu.RLock()
		for _, l := range manager.listeners {
			if err := l.SendCursorImage(entry.CursorImage, entry.ImagePNG); err != nil {
				manager.logger.Err(err).Msg("failed to set cursor image")
			}
		}
		manager.listenersMu.RUnlock()
	})

	manager.logger.Info().Msg("starting")
}

func (manager *image) Shutdown() {
	manager.logger.Info().Msg("shutdown")

	manager.listenersMu.Lock()
	for key := range manager.listeners {
		delete(manager.listeners, key)
	}
	manager.listenersMu.Unlock()
}

func (manager *image) getCached(serial uint64) (*imageEntry, error) {
	// zero means no serial available
	if serial == 0 || serial > manager.maxSerial {
		manager.logger.Debug().Uint64("serial", serial).Msg("cache bypass")
		return manager.fetchEntry()
	}

	manager.cacheMu.RLock()
	entry, ok := manager.cache[serial]
	manager.cacheMu.RUnlock()

	if ok {
		return entry, nil
	}

	manager.logger.Debug().Uint64("serial", serial).Msg("cache miss")

	entry, err := manager.fetchEntry()
	if err != nil {
		return nil, err
	}

	manager.cacheMu.Lock()
	manager.cache[entry.Serial] = entry
	manager.cacheMu.Unlock()

	if entry.Serial != serial {
		manager.logger.Warn().
			Uint64("expected-serial", serial).
			Uint64("received-serial", entry.Serial).
			Msg("serial mismatch")
	}

	return entry, nil
}

func (manager *image) GetCurrent() (cur *types.CursorImage, img []byte, err error) {
	if manager.current != nil {
		return manager.current.CursorImage, manager.current.ImagePNG, nil
	}

	entry, err := manager.fetchEntry()
	if err != nil {
		return nil, nil, err
	}

	manager.current = entry
	return entry.CursorImage, entry.ImagePNG, nil
}

func (manager *image) AddListener(listener ImageListener) {
	manager.listenersMu.Lock()
	defer manager.listenersMu.Unlock()

	if listener != nil {
		ptr := reflect.ValueOf(listener).Pointer()
		manager.listeners[ptr] = listener
	}
}

func (manager *image) RemoveListener(listener ImageListener) {
	manager.listenersMu.Lock()
	defer manager.listenersMu.Unlock()

	if listener != nil {
		ptr := reflect.ValueOf(listener).Pointer()
		delete(manager.listeners, ptr)
	}
}

func (manager *image) fetchEntry() (*imageEntry, error) {
	cur := manager.desktop.GetCursorImage()

	img, err := utils.CreatePNGImage(cur.Image)
	if err != nil {
		return nil, err
	}
	cur.Image = nil // free memory

	return &imageEntry{
		CursorImage: cur,
		ImagePNG:    img,
	}, nil
}
