package redo

import (
	"errors"
	"fmt"
	"hash"
	"hash/crc32"
	"os"
	"path/filepath"

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

const (
	FlagReused  byte = 1 // сторінка із FreeList
	FlagNewRoot byte = 2 // новая страница
)

type PageToWrite struct {
	PageNo uint32
	Data   []byte
}

type Writer struct {
	metricID          uint32
	timestamp         uint32
	value             float64
	tmp               []byte
	fileName          string
	file              *os.File
	hasher            hash.Hash32
	isDataPageReused  bool
	dataPageNo        uint32
	isRootChanged     bool
	newRootPageNo     uint32
	indexPages        []uint32
	reusedIndexPages  []uint32
	indexPagesToWrite []PageToWrite
}

type WriterOptions struct {
	Dir              string
	MetricID         uint32
	Value            float64
	Timestamp        uint32
	IsDataPageReused bool
	DataPageNo       uint32
	Page             []byte
}

// dataPage можно записати 1 раз. Щоб не заплутувати інтерфейс - передаю data сторінку
// через Options. Index сторінок може бути від 1 до N, тому виділяю окремий метод
func NewWriter(opt WriterOptions) (*Writer, error) {
	if opt.Dir == "" {
		return nil, errors.New("Dir option is required")
	}
	if opt.MetricID == 0 {
		return nil, errors.New("MetricID option is required")
	}
	if opt.DataPageNo == 0 {
		return nil, errors.New("DataPageNo option is required")
	}
	// if len(opt.Page) != octopus.DataPageSize {
	// 	return nil, fmt.Errorf("bug: wrong data page size %d", len(opt.Page))
	// }

	s := &Writer{
		fileName:         JoinREDOFileName(opt.Dir, opt.MetricID),
		metricID:         opt.MetricID,
		timestamp:        opt.Timestamp,
		value:            opt.Value,
		tmp:              make([]byte, 21),
		isDataPageReused: opt.IsDataPageReused,
		dataPageNo:       opt.DataPageNo,
		hasher:           crc32.NewIEEE(),
	}

	var err error
	s.file, err = os.OpenFile(s.fileName, os.O_CREATE|os.O_WRONLY, 0770)
	if err != nil {
		return nil, err
	}

	err = s.init(opt.Page)
	if err != nil {
		return nil, err
	}
	return s, nil
}

/*
Формат:
4b  metricID
8b  value
4b  timestamp
1b  flags (reused)
4b  dataPageNo
8KB dataPage
*/
func (s *Writer) init(dataPage []byte) error {
	bin.PutUint32(s.tmp[0:], s.metricID)
	bin.PutUint32(s.tmp[4:], s.timestamp)
	bin.PutFloat64(s.tmp[8:], s.value)
	if s.isDataPageReused {
		s.tmp[16] = 1
	}
	bin.PutUint32(s.tmp[17:], s.dataPageNo)

	_, err := s.file.Write(s.tmp)
	if err != nil {
		return err
	}

	_, err = s.file.Write(dataPage)
	if err != nil {
		return err
	}

	s.hasher.Write(s.tmp)
	s.hasher.Write(dataPage)
	return nil
}

/*
Формат
1b  index page flags
4b  indexPageNo
Nb  indexPage
*/
func (s *Writer) AppendIndexPage(indexPageNo uint32, indexPage []byte, flags byte) error {
	s.tmp[0] = flags
	bin.PutUint32(s.tmp[1:], indexPageNo)
	_, err := s.file.Write(s.tmp[:5])
	if err != nil {
		return err
	}
	_, err = s.file.Write(indexPage)
	if err != nil {
		return err
	}

	s.hasher.Write(s.tmp[:5])
	s.hasher.Write(indexPage)

	s.indexPages = append(s.indexPages, indexPageNo)

	if (flags & FlagReused) == FlagReused {
		s.reusedIndexPages = append(s.reusedIndexPages, indexPageNo)
	}

	if (flags & FlagNewRoot) == FlagNewRoot {
		s.newRootPageNo = indexPageNo
		s.isRootChanged = true
	}

	s.indexPagesToWrite = append(s.indexPagesToWrite,
		PageToWrite{
			PageNo: indexPageNo,
			Data:   indexPage,
		})
	return nil
}

func (s *Writer) IndexPagesToWrite() []PageToWrite {
	return s.indexPagesToWrite
}

func (s *Writer) Close() (err error) {
	// финализирую запись
	bin.PutUint32(s.tmp, s.hasher.Sum32())
	_, err = s.file.Write(s.tmp[:4])
	if err != nil {
		return err
	}
	err = s.file.Sync()
	if err != nil {
		return
	}
	return s.file.Close()
}

type Report struct {
	FileName         string
	IsDataPageReused bool
	DataPageNo       uint32
	IsRootChanged    bool
	NewRootPageNo    uint32
	ReusedIndexPages []uint32
}

func (s *Writer) GetReport() Report {
	return Report{
		FileName:         s.fileName,
		IsDataPageReused: s.isDataPageReused,
		DataPageNo:       s.dataPageNo,
		//IndexPages:       s.indexPages,
		IsRootChanged:    s.isRootChanged,
		NewRootPageNo:    s.newRootPageNo,
		ReusedIndexPages: s.reusedIndexPages,
	}
}

// HELPERS

func JoinREDOFileName(dir string, metricID uint32) string {
	return filepath.Join(dir, fmt.Sprintf("m%d.redo", metricID))
}
