// Copyright 2021 Flamego. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package session

import (
	"container/heap"
	"context"
	"sync"
	"time"
)

var _ Session = (*memorySession)(nil)

// memorySession is an in-memory session.
type memorySession struct {
	*BaseSession

	lock           sync.RWMutex // The mutex to guard accesses to the lastAccessedAt
	lastAccessedAt time.Time    // The last time of the session being accessed

	index int // The index in the heap
}

// newMemorySession returns a new memory session with given session ID.
func newMemorySession(sid string) *memorySession {
	return &memorySession{
		BaseSession: NewBaseSession(sid, nil),
	}
}

func (s *memorySession) LastAccessedAt() time.Time {
	s.lock.RLock()
	defer s.lock.RUnlock()
	return s.lastAccessedAt
}

func (s *memorySession) SetLastAccessedAt(t time.Time) {
	s.lock.Lock()
	defer s.lock.Unlock()
	s.lastAccessedAt = t
}

var _ Store = (*memoryStore)(nil)

// memoryStore is an in-memory implementation of the session store.
type memoryStore struct {
	nowFunc  func() time.Time // The function to return the current time
	lifetime time.Duration    // The duration to have no access to a session before being recycled

	lock  sync.RWMutex              // The mutex to guard accesses to the heap and index
	heap  []*memorySession          // The heap to be managed by operations of heap.Interface
	index map[string]*memorySession // The index to be managed by operations of heap.Interface
}

// newMemoryStore returns a new memory session store based on given
// configuration.
func newMemoryStore(cfg MemoryConfig) *memoryStore {
	return &memoryStore{
		nowFunc:  cfg.nowFunc,
		lifetime: cfg.Lifetime,
		index:    make(map[string]*memorySession),
	}
}

// Len implements `heap.Interface.Len`. It is not concurrent-safe and is the
// caller's responsibility to ensure they're being guarded by a mutex during any
// heap operation, i.e. heap.Fix, heap.Remove, heap.Push, heap.Pop.
func (s *memoryStore) Len() int {
	return len(s.heap)
}

// Less implements `heap.Interface.Less`. It is not concurrent-safe and is the
// caller's responsibility to ensure they're being guarded by a mutex during any
// heap operation, i.e. heap.Fix, heap.Remove, heap.Push, heap.Pop.
func (s *memoryStore) Less(i, j int) bool {
	return s.heap[i].LastAccessedAt().Before(s.heap[j].LastAccessedAt())
}

// Swap implements `heap.Interface.Swap`. It is not concurrent-safe and is the
// caller's responsibility to ensure they're being guarded by a mutex during any
// heap operation, i.e. heap.Fix, heap.Remove, heap.Push, heap.Pop.
func (s *memoryStore) Swap(i, j int) {
	s.heap[i], s.heap[j] = s.heap[j], s.heap[i]
	s.heap[i].index = i
	s.heap[j].index = j
}

// Push implements `heap.Interface.Push`. It is not concurrent-safe and is the
// caller's responsibility to ensure they're being guarded by a mutex during any
// heap operation, i.e. heap.Fix, heap.Remove, heap.Push, heap.Pop.
func (s *memoryStore) Push(x interface{}) {
	n := s.Len()
	sess := x.(*memorySession)
	sess.index = n
	s.heap = append(s.heap, sess)
	s.index[sess.sid] = sess
}

// Pop implements `heap.Interface.Pop`. It is not concurrent-safe and is the
// caller's responsibility to ensure they're being guarded by a mutex during any
// heap operation, i.e. heap.Fix, heap.Remove, heap.Push, heap.Pop.
func (s *memoryStore) Pop() interface{} {
	n := s.Len()
	sess := s.heap[n-1]

	s.heap[n-1] = nil // Avoid memory leak
	sess.index = -1   // For safety

	s.heap = s.heap[:n-1]
	delete(s.index, sess.sid)
	return sess
}

func (s *memoryStore) Exist(_ context.Context, sid string) bool {
	s.lock.RLock()
	defer s.lock.RUnlock()

	_, ok := s.index[sid]
	return ok
}

func (s *memoryStore) Read(_ context.Context, sid string) (Session, error) {
	s.lock.Lock()
	defer s.lock.Unlock()

	sess, ok := s.index[sid]
	if ok {
		// Discard existing data if it's expired
		if !s.nowFunc().Before(sess.LastAccessedAt().Add(s.lifetime)) {
			sess.SetData(make(Data))
		}
		sess.SetLastAccessedAt(s.nowFunc())
		heap.Fix(s, sess.index)
		return sess, nil
	}

	sess = newMemorySession(sid)
	sess.SetLastAccessedAt(s.nowFunc())
	heap.Push(s, sess)
	return sess, nil
}

func (s *memoryStore) Destroy(_ context.Context, sid string) error {
	s.lock.Lock()
	defer s.lock.Unlock()

	sess, ok := s.index[sid]
	if !ok {
		return nil
	}

	heap.Remove(s, sess.index)
	return nil
}

func (s *memoryStore) Save(context.Context, Session) error {
	return nil
}

func (s *memoryStore) GC(ctx context.Context) error {
	// Removing expired sessions from top of the heap until there is no more expired
	// sessions found.
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}

		done := func() bool {
			s.lock.Lock()
			defer s.lock.Unlock()

			if s.Len() == 0 {
				return true
			}

			sess := s.heap[0]

			// If the least accessed session is not expired, there is no need to continue
			if s.nowFunc().Before(sess.LastAccessedAt().Add(s.lifetime)) {
				return true
			}

			heap.Remove(s, sess.index)
			return false
		}()
		if done {
			break
		}
	}
	return nil
}

// MemoryConfig contains options for the memory session store.
type MemoryConfig struct {
	nowFunc func() time.Time // For tests only

	// Lifetime is the duration to have no access to a session before being
	// recycled. Default is 3600 seconds.
	Lifetime time.Duration
}

// MemoryIniter returns the Initer for the memory session store.
func MemoryIniter() Initer {
	return func(_ context.Context, args ...interface{}) (Store, error) {
		var cfg *MemoryConfig
		for i := range args {
			switch v := args[i].(type) {
			case MemoryConfig:
				cfg = &v
			}
		}

		if cfg == nil {
			cfg = &MemoryConfig{}
		}

		if cfg.nowFunc == nil {
			cfg.nowFunc = time.Now
		}
		if cfg.Lifetime.Seconds() < 1 {
			cfg.Lifetime = 3600 * time.Second
		}

		return newMemoryStore(*cfg), nil
	}
}
