package atree

import (
	"errors"
	"fmt"
	"io/fs"
	"math"
	"os"

	"gordenko.dev/dima/qb"
	"gordenko.dev/dima/qb/bin"
)

// fix - додати ID, щоб потім перемістити із тимчасового буфера в pages, або звільнити
type AllocatedPage struct {
	PageNo   uint32
	Data     []byte
	IsReused bool
}

type readResult struct {
	Data []byte
	Err  error
}

// INDEX PAGES

func (s *Atree) DeletePages(pageNumbers []uint32) {
	s.mutex.Lock()
	for _, pageNo := range pageNumbers {
		delete(s.pages, pageNo)
	}
	s.mutex.Unlock()
}

func (s *Atree) fetchIndexPage(pageNo uint32) ([]byte, error) {
	return s.fetchPage(pageNo, PageTypeIndex)
}

func (s *Atree) fetchDataPage(pageNo uint32) ([]byte, error) {
	return s.fetchPage(pageNo, PageTypeData)
}

func (s *Atree) fetchPage(pageNo uint32, pageTypeIdx byte) ([]byte, error) {
	s.mutex.Lock()
	p, ok := s.pages[pageNo]
	if ok {
		if p.Buf[pageTypeIdx] != pageTypeIdx {
			return nil, fmt.Errorf("wrong pageType %d instead of %d", p.Buf[pageTypeIdx], pageTypeIdx)
		}
		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.verifyCRC(result.Data, PageSize)
	}
	return result.Data, result.Err
}

func (s *Atree) 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 *Atree) allocPage() AllocatedPage {
	var (
		allocated = AllocatedPage{
			Data: make([]byte, PageSize),
		}
	)
	allocated.PageNo = s.freelist.ReservePage()
	s.mutex.Lock()
	if allocated.PageNo > 0 {
		allocated.IsReused = true
	} else {
		if s.allocatedPagesQty == math.MaxUint32 {
			qb.Abort(qb.MaxAtreeSizeExceeded, errors.New("no space in Atree index"))
		}
		s.allocatedPagesQty++
		allocated.PageNo = s.allocatedPagesQty
	}

	// s.pages[allocated.PageNo] = &_page{
	// 	// fix pageType
	// 	PageNo:         allocated.PageNo,
	// 	Buf:            allocated.Data,
	// 	ReferenceCount: 1,
	// }
	s.mutex.Unlock()
	return allocated
}

// // fix - без freelist
// func (s *Atree) allocIndexPage() AllocatedPage {
// 	var (
// 		allocated = AllocatedPage{
// 			Data: make([]byte, PageSize),
// 		}
// 	)

// 	allocated.PageNo = s.freelist.ReservePage()
// 	s.mutex.Lock()
// 	if allocated.PageNo > 0 {
// 		allocated.IsReused = true
// 	} else {
// 		if s.allocatedPagesQty == math.MaxUint32 {
// 			qb.Abort(qb.MaxAtreeSizeExceeded, errors.New("no space in Atree index"))
// 		}
// 		s.allocatedPagesQty++
// 		allocated.PageNo = s.allocatedPagesQty
// 	}

// 	s.pages[allocated.PageNo] = &_page{
// 		// fix pageType
// 		PageNo:         allocated.PageNo,
// 		Buf:            allocated.Data,
// 		ReferenceCount: 1,
// 	}
// 	s.mutex.Unlock()
// 	return allocated
// }

// func (s *Atree) allocDataPage() AllocatedPage {
// 	var (
// 		allocated = AllocatedPage{
// 			Data: make([]byte, PageSize),
// 		}
// 	)

// 	allocated.PageNo = s.freelist.ReservePage()
// 	s.mutex.Lock()
// 	if allocated.PageNo > 0 {
// 		allocated.IsReused = true
// 	} else {
// 		if s.allocatedPagesQty == math.MaxUint32 {
// 			qb.Abort(qb.MaxAtreeSizeExceeded, errors.New("no space in Atree index"))
// 		}
// 		s.allocatedPagesQty++
// 		allocated.PageNo = s.allocatedPagesQty
// 	}

// 	s.pages[allocated.PageNo] = &_page{
// 		// fix pageType
// 		PageNo:         allocated.PageNo,
// 		Buf:            allocated.Data,
// 		ReferenceCount: 1,
// 	}
// 	s.mutex.Unlock()
// 	return allocated
// }

// DATA PAGES

// READ

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

func (s *Atree) 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, PageSize)
		off := (pageNo - 1) * PageSize
		n, err := s.file.ReadAt(buf, int64(off))
		if n != PageSize {
			err = fmt.Errorf("read %d instead of %d", n, 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{
				// fix - page type
				PageNo:         pageNo,
				Buf:            buf,
				ReferenceCount: len(resultChannels),
			}
			s.mutex.Unlock()
			for _, resultCh := range resultChannels {
				resultCh <- readResult{
					Data: buf,
				}
			}
		}
	}
}

// WRITE

func (s *Atree) pageWriter() {
	for {
		select {
		case <-s.writeSignalCh:
			err := s.writeTasks()
			if err != nil {
				qb.Abort(qb.WriteToAtreeFailed, err)
			}
		}
	}
}

type WriteTask struct {
	WaitCh chan struct{}
	Pages  []PageToWrite
}

func (s *Atree) appendWriteTaskToQueue(task WriteTask) {
	s.mutex.Lock()
	s.writeTasksQueue = append(s.writeTasksQueue, task)
	s.mutex.Unlock()

	select {
	case s.writeSignalCh <- struct{}{}:
	default:
	}
}

func (s *Atree) writeTasks() error {
	s.mutex.Lock()
	tasks := s.writeTasksQueue
	s.writeTasksQueue = nil
	s.mutex.Unlock()

	for _, task := range tasks {
		for _, p := range task.Pages {
			if len(p.Data) != PageSize {
				return fmt.Errorf("wrong page %d size: %d",
					p.PageNo, len(p.Data))
			}
			bin.PutUint32(p.Data[crc32Idx:], calcChecksum(p.Data[:crc32Idx]))

			off := (p.PageNo - 1) * PageSize
			n, err := s.file.WriteAt(p.Data, int64(off))
			if err != nil {
				return err
			}
			if n != len(p.Data) {
				return fmt.Errorf("write %d instead of %d", n, len(p.Data))
			}
		}
		close(task.WaitCh)
	}
	return nil
}

// IO

func isFileExist(fileName string) (bool, error) {
	_, err := os.Stat(fileName)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return false, nil
		} else {
			return false, err
		}
	} else {
		return true, nil
	}
}

func openFile(fileName string, pageSize int) (_ *os.File, _ uint32, err error) {
	file, err := os.OpenFile(fileName, os.O_RDWR, filePerm)
	if err != nil {
		return
	}
	fi, err := file.Stat()
	if err != nil {
		return
	}
	fileSize := fi.Size()

	if (fileSize % int64(pageSize)) > 0 {
		err = fmt.Errorf("the file size %d is not a multiple of the page size %d",
			fileSize, pageSize)
		return
	}

	allocatedPagesQty := fileSize / int64(pageSize)
	if allocatedPagesQty > math.MaxUint32 {
		err = fmt.Errorf("allocated pages %d is > max pages %d",
			allocatedPagesQty, math.MaxUint32)
		return
	}

	return file, uint32(allocatedPagesQty), nil
}

func (s *Atree) ApplyREDO(task WriteTask) {
	s.appendWriteTaskToQueue(task)
}

func (s *Atree) verifyCRC(data []byte, pageSize int) error {
	var (
		pos           = pageSize - 4
		calculatedCRC = calcChecksum(data[:pos])
		storedCRC     = bin.GetUint32(data[pos:])
	)
	if calculatedCRC != storedCRC {
		return fmt.Errorf("calculatedCRC %d not equal storedCRC %d",
			calculatedCRC, storedCRC)
	}
	return nil
}
