// Copyright 2021 Molecula Corp. All rights reserved.

package task

import (
	"fmt"
	"golang.org/x/sync/errgroup"
	"math/rand"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

// db represents a thing which can be locked, and which can
// perform read and write operations, which are modeled as channels which
// a workload can wait on writes to, and which embeds a lockable RWMutex.
// The RWMutex actually makes this slightly stricter than the semantics
// of RBF, which usually allows writes and reads to coexist, but fairly
// accurately represents the specific issue that RBF can't *finish* a write
// while an older read is active. Not the same, but has similar impact.
type db struct {
	read, write chan struct{}
	sync.Mutex
}

// server represents a set of dbs, which jobs can be run against. They're
// [26] because they're denoted by lowercase/uppercase letters.
type server struct {
	dbs     [26]db
	mu      sync.Mutex     // mutex to govern access to readers
	readers [26][]workload // a list of readers associated with each db
	waiters [26]struct {
		mu   sync.Mutex
		cond *sync.Cond
	}
	pool *Pool
	jobs chan *job
	tb   testing.TB
}

// a job represents a single operation on a server, and a receiver
// waiting to hear back when it's done. It also has a reference to the
// bitmasks of read/write locks so that the parent operation can clean
// them all up when it's done. This is roughly parallel to the Qcx/Tx
// locking behavior in featurebase.
type job struct {
	descr  workload // the workload that generated this job, used to identify them
	id     int
	write  bool
	locked *uint32 // bitmask of read-locked jobs
	ch     chan<- struct{}
}

// workload represents a series of jobs as letters;
// lowercase letters read from the read channel of a component, uppercase
// letters read from the write channel of the corresponding lowercase
// component. each operation locks components as it reaches them for
// the first time, then unlocks all of them at the end of the string.
type workload string

// runJob grabs a single job from the server's job queue, does it, and
// notifies the waiter. To "do" a job is to acquire the appropriate
// lock (for read or write), mark the appropriate bit in a bitmap of
// active locks, and then read from either a read or write channel, which
// then corresponds to values being passed to Satisfy.
func (s *server) runJob() {
	j, ok := <-s.jobs
	if !ok {
		return
	}
	if j.write {
		s.pool.Block()
		s.dbs[j.id].Lock()
		s.pool.Unblock()
		s.mu.Lock()
		// obtain list of existing readers
		waiting := make([]workload, len(s.readers[j.id]))
		copy(waiting, s.readers[j.id])
		s.mu.Unlock()
		<-s.dbs[j.id].write
		// In RBF, the write lock can't be released until the last outstanding
		// reader predating this write terminates, but that's asynchronous
		// from the actual request processing. So, similarly, we launch a thing
		// that will unlock this slot in the database, once it's done waiting
		// for any readers. We do that without the pool marked as blocked.
		go func() {
			// but the write can't actually complete until any pending readers
			// that were already in play complete
			if len(waiting) > 0 {
				// We might need to wait for things. We need to be sure,
				// though, that the server's list of readers for this isn't
				// changing while we're checking it. So, we grab the specific
				// lock, then check the reader list, and if we think we need
				// to wait, we wait on a condition variable which then
				// releases that lock so something else can update the reader
				// list and notify us.
				func() {
					s.waiters[j.id].mu.Lock()
					defer s.waiters[j.id].mu.Unlock()
					// we have to check this with the specific lock held, so if
					// anything were to change the list, it'd have to wait
					// until we're done or waiting on the cond.
					stillWaiting := s.stillWaiting(j.id, waiting)
					for stillWaiting {
						s.waiters[j.id].cond.Wait()
						stillWaiting = s.stillWaiting(j.id, waiting)
					}
				}()
			}
			s.dbs[j.id].Unlock()
		}()
	} else {
		s.pool.Block()
		// attach us to the list of known readers, which must exit before
		// any writers starting after them can exit
		s.mu.Lock()
		s.readers[j.id] = append(s.readers[j.id], j.descr)
		s.mu.Unlock()
		s.pool.Unblock()
		cur := atomic.LoadUint32(j.locked)
		// mask this bit in
		for (cur>>j.id)&1 == 0 {
			added := cur | (1 << j.id)
			atomic.CompareAndSwapUint32(j.locked, cur, added)
			cur = atomic.LoadUint32(j.locked)
		}
		<-s.dbs[j.id].read
	}
	j.ch <- struct{}{}
}

// stillWaiting determines whether we're still waiting on anything in
// a given list terminating.
func (s *server) stillWaiting(id int, waitingOn []workload) bool {
	s.mu.Lock()
	readers := s.readers[id]
	s.mu.Unlock()
	for _, waiter := range waitingOn {
		for _, reader := range readers {
			if waiter == reader {
				return true
			}
		}
	}
	return false
}

// runWorkload runs the tasks within a workload, passing them to the worker
// queue, and then waiting for them all to complete. When it's done waiting
// for them, it releases any locks they obtained.
func (s *server) runWorkload(w workload) {
	var locked uint32
	defer func() {
		// unlock everything marked as locked
		read := atomic.LoadUint32(&locked)
		for i := 0; i < 32; i++ {
			if (read>>i)&1 != 0 {
				s.waiters[i].mu.Lock()
				s.mu.Lock()
				// remove us from readers list
				for j := range s.readers[i] {
					if s.readers[i][j] == w {
						copy(s.readers[i][j:], s.readers[i][j+1:])
						s.readers[i] = s.readers[i][:len(s.readers[i])-1]
						break
					}
				}
				s.mu.Unlock()
				s.waiters[i].mu.Unlock()
				// and wake up anything that was waiting for this.
				s.waiters[i].cond.Broadcast()
			}
		}
	}()
	ch := make(chan struct{})
	eg := &errgroup.Group{}
	j := job{ch: ch, locked: &locked, descr: w}
	for _, c := range w {
		switch {
		case c >= 'a' && c <= 'z':
			j.id = int(c - 'a')
			j.write = false
		case c >= 'A' && c <= 'Z':
			j.id = int(c - 'A')
			j.write = true
		default:
			s.tb.Logf("unhandled character '%c'", c)
			continue
		}
		j := j
		eg.Go(func() error {
			s.jobs <- &j
			<-ch
			return nil
		})
	}
	_ = eg.Wait()
}

// newServer creates a server associated with the given testing.TB,
// allowing us to log things.
func newServer(tb testing.TB) *server {
	s := &server{tb: tb, jobs: make(chan *job)}
	for i := range s.dbs {
		s.dbs[i].read = make(chan struct{})
		s.dbs[i].write = make(chan struct{})
		s.waiters[i].cond = sync.NewCond(&s.waiters[i].mu)
	}
	return s
}

// close shuts the server down by closing all of its channels, and may
// not really be necessary.
func (s *server) close() {
	for i := range s.dbs {
		db := &s.dbs[i]
		db.Lock()
		close(db.read)
		close(db.write)
		db.Unlock()
	}
	close(s.jobs)
}

// Satisfy satisfies the given read or write operations asynchronously,
// but waits for all of them in this batch to complete before returning.
func (s *server) satisfy(w workload) {
	var eg errgroup.Group
	for _, c := range w {
		var id int
		var write bool
		switch {
		case c >= 'a' && c <= 'z':
			id = int(c - 'a')
			write = false
		case c >= 'A' && c <= 'Z':
			id = int(c - 'A')
			write = true
		default:
			s.tb.Logf("unhandled character '%c'", c)
			continue
		}
		eg.Go(func() error {
			if write {
				s.dbs[id].write <- struct{}{}
			} else {
				s.dbs[id].read <- struct{}{}
			}
			return nil
		})
	}
	_ = eg.Wait()
}

// makeWorkload generates a sequence of letters, some of which may be
// capitalized, in order
func makeWorkload() workload {
	var letters [26]byte
	var n int
	write := rand.Intn(8) == 0
	for i := 0; i < 26; i++ {
		if rand.Intn(4) == 0 {
			if write {
				letters[n] = 'A' + byte(i)
			} else {
				letters[n] = 'a' + byte(i)
			}
			n++
		}
	}
	return workload(letters[:n])
}

// testRandomWorkload makes up an arbitrary workload and tries to run
// the server against it.
func testRandomWorkload(t *testing.T) {
	s := newServer(t)
	eg := &errgroup.Group{}
	p := NewPool(2, s.runJob, nil)
	s.pool = p
	defer p.Close()
	defer s.close()
	var workloads []workload // the requests we make
	var quick []workload     // the requests that get satisfied soon
	var slow []workload      // the requests that don't get satisfied until later
	for i := 0; i < 10; i++ {
		w := makeWorkload()
		if len(w) == 0 {
			continue
		}
		workloads = append(workloads, w)
		partial := rand.Intn(26)
		// possibly truncate and postpone some
		if partial < len(w) {
			quick = append(quick, w[:partial])
			slow = append(slow, w[partial:])
		} else {
			quick = append(quick, w)
		}
	}
	for _, w := range workloads {
		w := w
		eg.Go(func() error {
			s.runWorkload(w)
			return nil
		})
	}
	for _, w := range quick {
		w := w
		eg.Go(func() error {
			s.satisfy(w)
			return nil
		})
	}
	l, u, target := p.Stats()
	// Only one worker at a time can be invoking the mark-as-blocked logic,
	// so you can run after it marks that, but before the new worker is spawned,
	// but the next worker can't invoke the blocked logic until that completes.
	//
	// Live count always decreases after unblocked count on the exit path, and
	// increases before unblocked count on the startup path. So even if the
	// samples are interrupted, I think it should be impossible for live
	// to be less than unblocked.
	if u < target-1 || l < u {
		t.Fatalf("inconsistent pool stats: %d live, %d unblocked, %d target", l, u, target)
	}
	for _, w := range slow {
		w := w
		eg.Go(func() error {
			s.satisfy(w)
			return nil
		})
	}
	_ = eg.Wait()
}

// TestRandomWorkloads makes up some arbitrary workloads, then tries to
// satisfy them out of order.
// In theory, this should work for any sequence of operations as long as
// no operation has the same letter for both read and write ops, and
// ops always occur in order.
func TestRandomWorkloads(t *testing.T) {
	for i := 0; i < 10; i++ {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			testRandomWorkload(t)
		})
	}
}

func TestServer(t *testing.T) {
	s := newServer(t)
	eg := &errgroup.Group{}
	p := NewPool(3, s.runJob, nil)
	s.pool = p
	defer p.Close()
	defer s.close()
	request := func(w workload) {
		eg.Go(func() error {
			s.runWorkload(w)
			return nil
		})
	}
	// requesting "abcd" means that the reader "abcd" will still be active on
	// a until all the other letters show up.
	request("abcd")
	// satisfy won't complete until at least two of the jobs have happened,
	// so there's a decent chance that we've marked ourselves as a reader on
	// a.
	s.satisfy("abc")
	// so we request a write on A. we get the write lock, but we can't
	// relinquish it until "d" shows up.
	request("A")
	// satisfy that request immediately, but to no avail.
	s.satisfy("A")
	// three more requests come in. if they get pool slots, they definitely
	// block; that request on A can't have finished yet. so they could fully
	// block our work pool.
	request("A")
	request("A")
	request("A")
	// spawn something to provide "efg"
	go s.satisfy("efg")
	// runWorkload means we actually block waiting for it. if all the worker
	// pool is blocked waiting on A, we can't do that.
	s.runWorkload("efg")
	// now we provide the missing d, which should allow the first request to
	// finally complete, and then the next three A, which should finish
	// the rest.
	s.satisfy("dAAA")
	// If we spawned new jobs, this should complete. Otherwise it should hang
	// because the requests can't be satisfied because the queue is full
	// of blocked operations.
	_ = eg.Wait()
}

func TestPoolStartup(t *testing.T) {
	var counter int32
	started := make(chan struct{})
	done := make(chan struct{})
	addAndWait := func() {
		<-started
		atomic.AddInt32(&counter, 1)
		<-done
	}
	// we expect this to spawn three counters
	p := NewPool(3, addAndWait, nil)
	time.Sleep(50 * time.Millisecond)
	v := atomic.LoadInt32(&counter)
	if v != 0 {
		t.Fatalf("expected no adds yet, got %d", v)
	}
	close(started)
	time.Sleep(50 * time.Millisecond)
	v = atomic.LoadInt32(&counter)
	if v != 3 {
		t.Fatalf("expected 3 adds, got %d", v)
	}
	// Tell the pool to stop processing jobs
	p.Shutdown()
	// Allow the jobs to complete. Since this happens after the
	// shutdown has set desired pool size to zero, they should now all exit.
	close(done)
	p.Close()
	time.Sleep(50 * time.Millisecond)
	v = atomic.LoadInt32(&counter)
	if v != 3 {
		t.Fatalf("expected no more adds, got %d including previous 3", v)
	}
}
