package atree

import (
	"errors"
	"fmt"

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

type BackwardCursor struct {
	metricType            octopus.MetricType
	fracDigits            byte
	atree                 *Atree
	pageNo                uint32
	pageData              []byte
	timestampDecompressor octopus.TimestampDecompressor
	valueDecompressor     octopus.ValueDecompressor
}

type BackwardCursorOptions struct {
	MetricType octopus.MetricType
	FracDigits byte
	PageNo     uint32
	PageData   []byte
	Atree      *Atree
}

func NewBackwardCursor(opt BackwardCursorOptions) (*BackwardCursor, error) {
	switch opt.MetricType {
	case octopus.Instant, octopus.Cumulative:
		// ok
	default:
		return nil, fmt.Errorf("MetricType option has wrong value: %d", opt.MetricType)
	}
	if opt.FracDigits > octopus.MaxFracDigits {
		return nil, errors.New("FracDigits option is required")
	}
	if opt.Atree == nil {
		return nil, errors.New("Atree option is required")
	}
	if opt.PageNo == 0 {
		return nil, errors.New("PageNo option is required")
	}
	if len(opt.PageData) == 0 {
		return nil, errors.New("PageData option is required")
	}

	s := &BackwardCursor{
		metricType: opt.MetricType,
		fracDigits: opt.FracDigits,
		atree:      opt.Atree,
		pageNo:     opt.PageNo,
		pageData:   opt.PageData,
	}
	err := s.makeDecompressors()
	if err != nil {
		return nil, err
	}
	return s, nil
}

// timestamp, value, done, error
func (s *BackwardCursor) Prev() (uint32, float64, bool, error) {
	var (
		timestamp uint32
		value     float64
		done      bool
		err       error
	)

	timestamp, done = s.timestampDecompressor.NextValue()
	if !done {
		value, done = s.valueDecompressor.NextValue()
		if done {
			return 0, 0, false,
				fmt.Errorf("corrupted data page %d: has timestamp, no value",
					s.pageNo)
		}
		return timestamp, value, false, nil
	}

	prevPageNo := bin.GetUint32(s.pageData[prevPageIdx:])
	if prevPageNo == 0 {
		return 0, 0, true, nil
	}
	s.atree.releasePage(s.pageNo)

	s.pageNo = prevPageNo
	s.pageData, err = s.atree.fetchDataPage(s.pageNo)
	if err != nil {
		return 0, 0, false, fmt.Errorf("atree.fetchDataPage(%d): %s", s.pageNo, err)
	}

	err = s.makeDecompressors()
	if err != nil {
		return 0, 0, false, err
	}

	timestamp, done = s.timestampDecompressor.NextValue()
	if done {
		return 0, 0, false,
			fmt.Errorf("corrupted data page %d: no timestamps",
				s.pageNo)
	}
	value, done = s.valueDecompressor.NextValue()
	if done {
		return 0, 0, false,
			fmt.Errorf("corrupted data page %d: no values",
				s.pageNo)
	}
	return timestamp, value, false, nil
}

func (s *BackwardCursor) Close() {
	s.atree.releasePage(s.pageNo)
}

// HELPER

func (s *BackwardCursor) makeDecompressors() error {
	timestampsPayloadSize := bin.GetUint16(s.pageData[timestampsSizeIdx:])
	valuesPayloadSize := bin.GetUint16(s.pageData[valuesSizeIdx:])

	payloadSize := timestampsPayloadSize + valuesPayloadSize

	if payloadSize > dataFooterIdx {
		return fmt.Errorf("corrupted data page %d: timestamps + values size %d gt payload size",
			s.pageNo, payloadSize)
	}

	s.timestampDecompressor = enc.NewReverseTimeDeltaOfDeltaDecompressor(
		s.pageData[:timestampsPayloadSize],
	)

	vbuf := s.pageData[timestampsPayloadSize : timestampsPayloadSize+valuesPayloadSize]

	switch s.metricType {
	case octopus.Instant:
		s.valueDecompressor = enc.NewReverseInstantDeltaDecompressor(
			vbuf, s.fracDigits)

	case octopus.Cumulative:
		s.valueDecompressor = enc.NewReverseCumulativeDeltaDecompressor(
			vbuf, s.fracDigits)

	default:
		return fmt.Errorf("bug: wrong metricType %d", s.metricType)
	}
	return nil
}

func makeDecompressors(pageData []byte, metricType octopus.MetricType, fracDigits byte) (
	octopus.TimestampDecompressor, octopus.ValueDecompressor, error,
) {
	timestampsPayloadSize := bin.GetUint16(pageData[timestampsSizeIdx:])
	valuesPayloadSize := bin.GetUint16(pageData[valuesSizeIdx:])

	payloadSize := timestampsPayloadSize + valuesPayloadSize

	if payloadSize > dataFooterIdx {
		return nil, nil, fmt.Errorf("corrupted: timestamps + values size %d > payload size",
			payloadSize)
	}

	timestampDecompressor := enc.NewReverseTimeDeltaOfDeltaDecompressor(
		pageData[:timestampsPayloadSize],
	)

	vbuf := pageData[timestampsPayloadSize : timestampsPayloadSize+valuesPayloadSize]

	var valueDecompressor octopus.ValueDecompressor
	switch metricType {
	case octopus.Instant:
		valueDecompressor = enc.NewReverseInstantDeltaDecompressor(
			vbuf, fracDigits)

	case octopus.Cumulative:
		valueDecompressor = enc.NewReverseCumulativeDeltaDecompressor(
			vbuf, fracDigits)

	default:
		return nil, nil, fmt.Errorf("bug: wrong metricType %d", metricType)
	}
	return timestampDecompressor, valueDecompressor, nil
}
