package freelist

import (
	"fmt"
	"io"
	"os"

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

/*
Ідея - я фіксую кількість сторінок записаних в снапшоті.
Додаю і видаляю сторінки із основного файла, поки їх кількість > кількості зафіксованих.
Якщо pageNumbers видалено і кількість сторінок треба зменшити - нові сторінки пишу в delta файл.
При створенні снапшота - сторінки із delta файла записую в основний файл і фіксую в снапшоті
їх кількість. PageNumbers із буфера пишу в снапшот.
*/

const (
	crcSize = 4
	ptrSize = 4
)

type FreeList struct {
	baseFile        *os.File
	deltaFile       *os.File
	pageSize        int
	pointersOnPage  int
	flushTreshold   int // кількість вільних сторінок коли вже треба писати на диск
	frozenPageCount int
	basePagesCount  int
	deltaPagesCount int
	free            []uint32
}

type Options struct {
	PageSize        int
	BaseFilePath    string
	DeltaFilePath   string
	FrozenPageCount int
}

func New(opt Options) (*FreeList, error) {
	s := &FreeList{
		pageSize:        opt.PageSize,
		pointersOnPage:  (opt.PageSize - crcSize) / ptrSize,
		frozenPageCount: opt.FrozenPageCount,
		basePagesCount:  opt.FrozenPageCount,
	}
	s.flushTreshold = int(float64(s.pointersOnPage) * 1.3)

	var err error
	s.baseFile, err = os.OpenFile(opt.BaseFilePath, os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		return nil, err
	}
	s.deltaFile, err = os.OpenFile(opt.DeltaFilePath, os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		return nil, err
	}
	// info, err := s.baseFile.Stat()
	// if err != nil {
	// 	return nil, err
	// }
	// fileSize := info.Size()
	// if fileSize > 0 {
	// 	if (fileSize % int64(s.pageSize)) > 0 {
	// 		return nil, fmt.Errorf("file size %d is not a multiple of page size %d",
	// 			fileSize, s.pageSize)
	// 	}
	// 	s.basePagesCount = int(fileSize) / s.pageSize
	// }
	return s, nil
}

func (s *FreeList) Pages() int {
	return s.basePagesCount + s.deltaPagesCount
}

func (s *FreeList) AddPageNumbers(pageNumbers []uint32) (err error) {
	if len(pageNumbers) == 0 {
		return
	}
	s.free = append(s.free, pageNumbers...)

	for len(s.free) > s.flushTreshold {
		err = s.save()
		if err != nil {
			return
		}
		// викидаю номери сторінок, які записав на диск
		copy(s.free, s.free[s.pointersOnPage:])
		s.free = s.free[:len(s.free)-s.pointersOnPage]
		fmt.Println("free:", s.free)
	}
	return
}

func (s *FreeList) save() error {
	fmt.Println("save:", s.free[:s.pointersOnPage])
	buf := make([]byte, s.pageSize)
	i := crcSize
	for _, pageNo := range s.free[:s.pointersOnPage] {
		bin.PutUint32(buf[i:], pageNo)
		i += ptrSize
	}
	bin.PutUint32(buf, util.CalculateCRC32(buf[crcSize:]))
	//
	var (
		file *os.File
		off  int64
	)
	if s.deltaPagesCount > 0 || s.basePagesCount < s.frozenPageCount {
		file = s.deltaFile
		off = int64(s.deltaPagesCount * s.pageSize)
		s.deltaPagesCount++
	} else {
		file = s.baseFile
		off = int64(s.basePagesCount * s.pageSize)
		s.basePagesCount++
	}
	fmt.Println("write at:", off)
	n, err := file.WriteAt(buf, off)
	if err != nil {
		return err
	}
	if n != s.pageSize {
		return fmt.Errorf("written size %d bytes not equal page size %d", n, s.pageSize)
	}
	err = file.Sync()
	if err != nil {
		return err
	}
	return nil
}

func (s *FreeList) loadLastPage() error {
	var (
		file *os.File
		off  int64
		buf  = make([]byte, s.pageSize)
	)
	if s.deltaPagesCount > 0 {
		file = s.deltaFile
		off = int64((s.deltaPagesCount - 1) * s.pageSize)
	} else {
		file = s.baseFile
		off = int64((s.basePagesCount - 1) * s.pageSize)
	}

	n, err := file.ReadAt(buf, off)
	if err != nil {
		return fmt.Errorf("file.Seek: %s", err)
	}
	if n != s.pageSize {
		return fmt.Errorf("read size %d bytes not equal page size %d", n, s.pageSize)
	}
	// check crc
	checksum, _ := bin.GetUint32(buf[0:])
	if util.CalculateCRC32(buf[crcSize:]) != checksum {
		return fmt.Errorf("page %d is corrupted", s.basePagesCount)
	}
	for i := crcSize; i < len(buf); i += ptrSize {
		pageNo, _ := bin.GetUint32(buf[i:])
		s.free = append(s.free, pageNo)
	}
	if s.deltaPagesCount > 0 {
		s.deltaPagesCount--
		err = file.Truncate(off)
		if err != nil {
			return err
		}
		return file.Sync()
	} else {
		s.basePagesCount--
		if s.basePagesCount >= s.frozenPageCount {
			err = s.baseFile.Truncate(off)
			if err != nil {
				return err
			}
			return file.Sync()
		}
	}
	return nil
}

func (s *FreeList) GetPageNumber() (pageNo uint32, err error) {
	if len(s.free) == 0 {
		if s.basePagesCount > 0 || s.deltaPagesCount > 0 {
			// якщо немає вільних сторінок у буфері, але є у freelist файлах -
			// завантажую сторінку із диску
			err = s.loadLastPage()
			if err != nil {
				return
			}
		} else {
			return
		}
	}
	if len(s.free) > 0 {
		lastIdx := len(s.free) - 1
		pageNo = s.free[lastIdx]
		s.free = s.free[:lastIdx]
	}
	return
}

// для відновлення недозаповненнох сторінки із снапшота
// func (s *FreeList) Restore(serialized []byte) error {
// 	if (len(serialized) % ptrSize) != 0 {
// 		return fmt.Errorf("wrong size")
// 	}
// 	for i := 0; i < len(serialized); i += ptrSize {
// 		s.free = append(s.free, bin.GetUint32(serialized[i:]))
// 	}
// 	fmt.Println(s.free)
// 	return nil
// }

// для запису в snapshot
// func (s *FreeList) Serialize() ([]byte, error) {
// 	w := bytes.NewBuffer(nil)
// 	for _, pageNo := range s.free {
// 		bin.WriteUint32(w, pageNo)
// 	}
// 	return w.Bytes(), nil
// }

func (s *FreeList) Cached() []uint32 {
	return s.free
}

func (s *FreeList) Merge() (err error) {
	var (
		off       = int64(s.basePagesCount * s.pageSize)
		deltaSize = int64(s.deltaPagesCount * s.pageSize)
	)
	if s.deltaPagesCount > 0 {
		var (
			ret    int64
			copied int64
		)
		ret, err = s.baseFile.Seek(off, 0)
		if err != nil {
			return
		}
		if ret != off {
			return fmt.Errorf("Seek failed")
		}
		copied, err = io.Copy(s.baseFile, s.deltaFile)
		if err != nil {
			return
		}
		if copied != deltaSize {
			return fmt.Errorf("copy failed")
		}
		err = s.baseFile.Sync()
		if err != nil {
			return
		}
		err = s.deltaFile.Truncate(0)
		if err != nil {
			return
		}
		s.basePagesCount += s.deltaPagesCount
		s.deltaPagesCount = 0
	}
	if s.basePagesCount < s.frozenPageCount {
		off = int64(s.basePagesCount * s.pageSize)
		err = s.baseFile.Truncate(off)
		if err != nil {
			return
		}
	}
	s.frozenPageCount = s.basePagesCount
	return nil
}
