package orderbook

import (
	"sync/atomic"
	"time"
)

const (
	neutral uint32 = iota
	active
)

var (
	defaultInterval  = time.Minute
	defaultAllowance = time.Second * 30
)

// node defines a linked list node for an orderbook item
type node struct {
	value Item
	next  *node
	prev  *node
	// Denotes time pushed to stack, this will influence cleanup routine when
	// there is a pause or minimal actions during period
	shelved time.Time
}

// stack defines a FILO list of reusable nodes
type stack struct {
	nodes []*node
	sema  uint32
	count int32
}

// newStack returns a ptr to a new stack instance, also starts the cleaning
// service
func newStack() *stack {
	s := &stack{}
	go s.cleaner()
	return s
}

// now defines a time which is now to ensure no other values get passed in
type now time.Time

// getNow returns the time at which it is called
func getNow() now {
	return now(time.Now())
}

// Push pushes a node pointer into the stack to be reused the time is passed in
// to allow for inlining which sets the time at which the node is theoretically
// pushed to a stack.
func (s *stack) Push(n *node, tn now) {
	if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
		// Stack is in use, for now we can dereference pointer
		n = nil
		return
	}
	// Adds a time when its placed back on to stack.
	n.shelved = time.Time(tn)
	n.next = nil
	n.prev = nil
	n.value = Item{}

	// Allows for resize when overflow TODO: rethink this
	s.nodes = append(s.nodes[:s.count], n)
	s.count++
	atomic.StoreUint32(&s.sema, neutral)
}

// Pop returns the last pointer off the stack and reduces the count and if empty
// will produce a lovely fresh node
func (s *stack) Pop() *node {
	if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
		// Stack is in use, for now we can allocate a new node pointer
		return &node{}
	}

	if s.count == 0 {
		// Create an empty node when no nodes are in slice or when cleaning
		// service is running
		atomic.StoreUint32(&s.sema, neutral)
		return &node{}
	}
	s.count--
	n := s.nodes[s.count]
	atomic.StoreUint32(&s.sema, neutral)
	return n
}

// cleaner (POC) runs to the defaultTimer to clean excess nodes (nodes not being
// utilised) TODO: Couple time parameters to check for a reduction in activity.
// Add in counter per second function (?) so if there is a lot of activity don't
// inhibit stack performance.
func (s *stack) cleaner() {
	tt := time.NewTimer(defaultInterval)
sleeperino:
	for range tt.C {
		if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
			// Stack is in use, reset timer to zero to recheck for neutral state.
			tt.Reset(0)
			continue
		}
		// As the old nodes are going to be left justified on this slice we
		// should just be able to shift the nodes that are still within time
		// allowance all the way to the left. Not going to resize capacity
		// because if it can get this big, it might as well stay this big.
		// TODO: Test and rethink if sizing is an issue
		for x := int32(0); x < s.count; x++ {
			if time.Since(s.nodes[x].shelved) > defaultAllowance {
				// Old node found continue
				continue
			}
			// First good node found, everything to the left of this on the
			// slice can be reassigned
			var counter int32
			for y := int32(0); y+x < s.count; y++ { // Go through good nodes
				// Reassign
				s.nodes[y] = s.nodes[y+x]
				// Add to the changed counter to remove from main
				// counter
				counter++
			}
			s.count -= counter
			atomic.StoreUint32(&s.sema, neutral)
			tt.Reset(defaultInterval)
			continue sleeperino
		}
		// Nodes are old, flush entirety.
		s.count = 0
		atomic.StoreUint32(&s.sema, neutral)
		tt.Reset(defaultInterval)
	}
}
