package pagecache

import (
	"errors"
	"fmt"
	"os"
	"sync"

	"gordenko.dev/dima/qb"
)

type readResult struct {
	Data []byte
	Err  error
}

// INDEX PAGES

type _page struct {
	PageNo         uint32
	Buf            []byte
	ReferenceCount int
}

type PageCache struct {
	mutex         sync.Mutex
	pageSize      int
	verifyPageCRC func([]byte) error
	file          *os.File
	pages         map[uint32]*_page
	pageWaits     map[uint32][]chan readResult
	pagesToRead   []uint32
	readSignalCh  chan struct{}
}

type Options struct {
	File          *os.File
	PageSize      int
	VerifyPageCRC func([]byte) error
}

func New(opt Options) (*PageCache, error) {
	if opt.File == nil {
		return nil, errors.New("File option is required")
	}
	if opt.PageSize <= 0 {
		return nil, errors.New("PageSize option is required")
	}
	if opt.VerifyPageCRC == nil {
		return nil, errors.New("VerifyPageCRC option is required")
	}
	s := &PageCache{
		file:          opt.File,
		pageSize:      opt.PageSize,
		verifyPageCRC: opt.VerifyPageCRC,
		pages:         make(map[uint32]*_page),
		pageWaits:     make(map[uint32][]chan readResult),
		readSignalCh:  make(chan struct{}, 1),
	}
	go s.pageReader()
	return s, nil
}

func (s *PageCache) FetchPage(pageNo uint32) ([]byte, error) {
	s.mutex.Lock()
	p, ok := s.pages[pageNo]
	if ok {
		p.ReferenceCount++
		s.mutex.Unlock()
		return p.Buf, nil
	}

	resultCh := make(chan readResult, 1)
	s.pageWaits[pageNo] = append(s.pageWaits[pageNo], resultCh)
	if len(s.pageWaits[pageNo]) == 1 {
		s.pagesToRead = append(s.pagesToRead, pageNo)
		s.mutex.Unlock()

		select {
		case s.readSignalCh <- struct{}{}:
		default:
		}
	} else {
		s.mutex.Unlock()
	}

	result := <-resultCh
	if result.Err == nil {
		result.Err = s.verifyPageCRC(result.Data)
	}
	return result.Data, result.Err
}

func (s *PageCache) ReleasePage(pageNo uint32) {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	p, ok := s.pages[pageNo]
	if ok {
		if p.ReferenceCount > 0 {
			p.ReferenceCount--
			return
		} else {
			qb.Abort(
				qb.ReferenceCountBug,
				fmt.Errorf("call releasePage on page %d with reference count = %d",
					pageNo, p.ReferenceCount),
			)
		}
	}
}

func (s *PageCache) pageReader() {
	for {
		select {
		case <-s.readSignalCh:
			s.readPages()
		}
	}
}

func (s *PageCache) readPages() {
	s.mutex.Lock()
	if len(s.pagesToRead) == 0 {
		s.mutex.Unlock()
		return
	}
	pagesToRead := s.pagesToRead
	s.pagesToRead = nil
	s.mutex.Unlock()

	for _, pageNo := range pagesToRead {
		buf := make([]byte, s.pageSize)
		off := int(pageNo-1) * s.pageSize
		n, err := s.file.ReadAt(buf, int64(off))
		if n != s.pageSize {
			err = fmt.Errorf("read %d instead of %d", n, s.pageSize)
		}
		s.mutex.Lock()
		resultChannels := s.pageWaits[pageNo]
		delete(s.pageWaits, pageNo)
		if err != nil {
			s.mutex.Unlock()
			for _, resultCh := range resultChannels {
				resultCh <- readResult{
					Err: err,
				}
			}
		} else {
			s.pages[pageNo] = &_page{
				PageNo:         pageNo,
				Buf:            buf,
				ReferenceCount: len(resultChannels),
			}
			s.mutex.Unlock()
			for _, resultCh := range resultChannels {
				resultCh <- readResult{
					Data: buf,
				}
			}
		}
	}
}
