package enc

import (
	"io"
	"log"
	"math"

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

const (
	tmpValueSize = 19
)

type ValueDeltaCompressor struct {
	buf       []byte
	coef      float64
	pos       int // payload size
	baseValue float64
	lastDelta uint64
	// (baseValue, coef, value) => delta
	calcDelta func(float64, float64, float64) uint64
	// (value, coef) => uint64
	toUint64 func(float64, float64) uint64
	// (value, coef) => uint64
	toFloat64 func(uint64, float64) float64
	state     *qb.ValueDeltaCapturedState
}

// Після відновлення із снапшота
func NewValueDeltaCompressor(metricType qb.MetricType, fracDigits byte, buf []byte, payloadSize int) *ValueDeltaCompressor {
	s := &ValueDeltaCompressor{
		buf:  buf,
		coef: 1,
		pos:  payloadSize,
	}
	if fracDigits > 0 {
		s.coef = math.Pow(10, float64(fracDigits))
	}
	if metricType == qb.Cumulative {
		s.calcDelta = calcCumulativeDelta
		s.toUint64 = toCumulativeUint64
		s.toFloat64 = toCumulativeFloat64
	} else {
		s.calcDelta = calcInstantDelta
		s.toUint64 = toInstantUint64
		s.toFloat64 = toInstantFloat64
	}
	if payloadSize > 0 {
		u64, _, err := bin.GetVarUint64(s.buf)
		if err != nil {
			log.Fatalf("bug: get base value: %s", err)
		}
		s.baseValue = s.toFloat64(u64, s.coef)
		s.lastDelta, _, err = bin.ReverseGetVarUint64(s.buf[:s.pos-1]) // skip h byte
		if err != nil {
			log.Fatalf("bug: get last delta: %s", err)
		}
		//fmt.Printf("lastDelta: %d\n", s.lastDelta)
	}
	return s
}

// можна виділити буфер максимального розміру, що може бути змінено під час кодування
// закодувати на етапі evaluate значення і повернути buf + offset. Причому буфер може бути
// спільний на всі metrics
// arr -
func (s *ValueDeltaCompressor) Evaluate(tmp []byte, value float64) qb.ValueEvaluationReport {
	var (
		delta        uint64
		rewindOffset int
		i            int
	)
	if s.pos > 0 {
		delta = s.calcDelta(s.baseValue, s.coef, value)
		h := s.buf[s.pos-1]
		if h < 128 {
			// run
			if delta == s.lastDelta && h < 127 {
				// incrementRun
				tmp[i] = h + 1
				i++
				rewindOffset = 1 // перезапис h
			} else {
				// endSeries
				n, _ := bin.ReversePutVarUint64(tmp, delta)
				i += n
				tmp[i] = 128 // start new literal (length=1)
				i++
			}
		} else {
			// literal
			if delta != s.lastDelta {
				if h < 255 {
					// incrementLiteral
					n, _ := bin.ReversePutVarUint64(tmp, delta)
					i += n
					tmp[i] = h + 1
					i++
					rewindOffset = 1 // перезапис h
				} else {
					// endSeries
					n, _ := bin.ReversePutVarUint64(tmp, delta)
					i += n
					tmp[i] = 128 // start new literal (length=1)
					i++
				}
			} else {
				// startRun
				if h > 128 {
					tmp[i] = h - 1 // зменшую довжину попередньої серії на 1
					i++
					n, _ := bin.ReversePutVarUint64(tmp[i:], delta)
					i += n
					tmp[i] = 0 // start new run (length=2)
					i++
					rewindOffset = 1 + n // перезапис пари delta/h
				} else {
					tmp[i] = 0 // change literal (length=1) to run (length=2)
					i++
					rewindOffset = 1
				}
			}
		}
	} else {
		n, _ := bin.PutVarUint64(tmp, s.toUint64(value, s.coef))
		i += n
		n, _ = bin.ReversePutVarUint64(tmp[i:], 0) // delta
		i += n
		tmp[i] = 128 // start new literal (length=1)
		i++
	}
	return qb.ValueEvaluationReport{
		RewindOffset: rewindOffset,
		Offset:       s.pos - rewindOffset,
		ChangeSize:   i,
		TotalSpace:   s.pos - rewindOffset + i,
		Delta:        delta,
	}
}

// pos завжди вказує на h
func (s *ValueDeltaCompressor) Append(rewindOffset int, change []byte, value float64, delta uint64) {
	// fmt.Printf("append value:\n")
	// fmt.Printf("rewind offset: %d\n", rewindOffset)
	// fmt.Printf("change: % x\n", change)
	// fmt.Printf("value: %v\n", value)
	// fmt.Printf("bufsize: %d\n", len(s.buf))
	// fmt.Printf("s.pos: %d\n", s.pos)
	//fmt.Printf("delta: %d\n", delta)
	if s.pos > 0 {
		s.lastDelta = delta
	} else {
		s.baseValue = value
	}
	//fmt.Printf("buf before: % x\n", s.buf[:s.pos])
	copy(s.buf[s.pos-rewindOffset:], change)
	s.pos += len(change) - rewindOffset
	//fmt.Printf("buf after: % x\n", s.buf[:s.pos])
	//fmt.Printf("buf after: % x\n", s.buf[:s.pos])
}

func (s *ValueDeltaCompressor) DeleteLast() {

}

func (s *ValueDeltaCompressor) getState() qb.ValueDeltaCapturedState {
	if s.pos > 0 {
		// fmt.Println("getState")
		// fmt.Printf("pos: %d\n", s.pos)
		// fmt.Printf("buf: % x\n", s.buf[:s.pos])
		// fmt.Printf("h: %d\n", s.buf[s.pos-1])
		// fmt.Printf("lastDelta: %d\n", s.lastDelta)
		bound := s.pos - 1 - bin.CountVarUint64(s.lastDelta)
		return qb.ValueDeltaCapturedState{
			H:         s.buf[s.pos-1],
			LastDelta: s.lastDelta,
			Payload:   s.buf[:bound],
		}
	} else {
		return qb.ValueDeltaCapturedState{}
	}
}

func (s *ValueDeltaCompressor) CaptureState() {
	state := s.getState()
	s.state = &state
}

func (s *ValueDeltaCompressor) ForgetCapturedState() {
	s.state = nil
}

func (s *ValueDeltaCompressor) Tail(offset int) []byte {
	return s.buf[offset:s.pos]
}

// для зростання буфера під час вставки даних. State не цікавить
func (s *ValueDeltaCompressor) Size() int {
	return s.pos
}

func (s *ValueDeltaCompressor) CommitedSize() int {
	if s.state == nil {
		return s.pos
	} else {
		if len(s.state.Payload) > 0 {
			return bin.CountVarUint64(uint64(s.state.LastDelta)) + 1 + len(s.state.Payload)
		} else {
			return 0
		}
	}
}

// Snapshot - для створення снапшота.
// func (s *ValueDeltaCompressor) Payload() []byte {
// 	if s.state == nil {
// 		return s.buf[:s.pos]
// 	} else {
// 		return s.state.Payload
// 	}
// }

func (s *ValueDeltaCompressor) WriteCommitedTo(w io.Writer) (err error) {
	if s.state == nil {
		_, err = w.Write(s.buf[:s.pos])
		return
	} else {
		_, err = w.Write(s.state.Payload)
		if err != nil {
			return
		}
		_, err = bin.WriteVarUint64(w, s.state.LastDelta)
		if err != nil {
			return
		}
		_, err = w.Write([]byte{
			s.state.H,
		})
		return
	}
}

func (s *ValueDeltaCompressor) ReplaceBuffer(newbuf []byte) {
	//fmt.Printf("value replace buffer: new size %d\n", len(newbuf))
	s.buf = newbuf
	s.pos = 0
	s.baseValue = 0
	s.lastDelta = 0
}

func (s *ValueDeltaCompressor) LastValue() float64 {
	// FIX
	// var delta uint64
	// if s.state == nil {
	// 	delta = s.lastDelta
	// } else {
	// 	delta = s.state.LastDelta
	// }
	// return s.baseValue + s.toFloat64(delta, s.coef)
	return 0
}

// Decompressor читає виключно збережені на диск дані
func (s *ValueDeltaCompressor) CreateDecompressor(metricType qb.MetricType, fracDigits byte) qb.ValueDecompressor {
	if s.pos == 0 {
		return nil
	}
	d := NewValueDeltaDecompressor(metricType, fracDigits)
	if s.state != nil {
		d.RestoreFromState(*s.state)
	} else {
		d.RestoreFromState(s.getState())
	}
	return d
}

// DECOMPRESSOR

type ValueDeltaDecompressor struct {
	buf       []byte
	coef      float64
	pos       int
	bound     int
	baseValue float64
	lastValue float64
	isRun     bool
	pending   int
	done      bool
	// (value, coef) => uint64
	toFloat64 func(uint64, float64) float64
}

func NewValueDeltaDecompressor(metricType qb.MetricType, fracDigits byte) *ValueDeltaDecompressor {
	s := &ValueDeltaDecompressor{
		coef: 1,
	}
	if fracDigits > 0 {
		s.coef = math.Pow(10, float64(fracDigits))
	}
	if metricType == qb.Cumulative {
		s.toFloat64 = toCumulativeFloat64
	} else {
		s.toFloat64 = toInstantFloat64
	}
	return s
}

func (s *ValueDeltaDecompressor) RestoreFromEnd(buf []byte) {
	s.buf = buf
	s.pos = len(buf) // first free
	if len(buf) > 0 {
		s.readBaseValue()
		s.readHeader()
		s.readValue()
	} else {
		s.done = true
	}
}

func (s *ValueDeltaDecompressor) RestoreFromState(state qb.ValueDeltaCapturedState) {
	//fmt.Printf("%#v\n", state)
	s.buf = state.Payload
	s.pos = len(s.buf) // first free
	if len(s.buf) > 0 {
		s.readBaseValue()
		s.lastValue = s.baseValue + s.toFloat64(state.LastDelta, s.coef)
		s.decodeHeaderByte(state.H)
	} else {
		s.done = true
	}
}

func (s *ValueDeltaDecompressor) readBaseValue() {
	u64, n, err := bin.GetVarUint64(s.buf)
	if err != nil {
		log.Fatalf("bug: get base value: %s", err)
	}
	s.baseValue = s.toFloat64(u64, s.coef)
	s.bound = n
}

func (s *ValueDeltaDecompressor) NextValue() (value float64, done bool) {
	if s.done {
		return 0, true
	}
	value = s.lastValue
	s.pending--
	if s.pending > 0 {
		if !s.isRun {
			s.readValue()
		}
	} else if s.pos > s.bound {
		s.readHeader()
		s.readValue()
	} else {
		s.done = true
	}
	return value, false
}

func (s *ValueDeltaDecompressor) readHeader() {
	s.pos--
	h := s.buf[s.pos]
	s.decodeHeaderByte(h)
}

func (s *ValueDeltaDecompressor) readValue() {
	u64, n, err := bin.ReverseGetVarUint64(s.buf[:s.pos])
	if err != nil {
		log.Fatalln(err)
	}
	s.pos -= n
	s.lastValue = s.baseValue + s.toFloat64(u64, s.coef)
}

func (s *ValueDeltaDecompressor) decodeHeaderByte(h byte) {
	s.isRun = h < 128
	if s.isRun {
		s.pending = int(h) + 2
	} else {
		s.pending = int(h&127) + 1
	}
}

// HELPERS

func calcCumulativeDelta(baseValue float64, coef float64, value float64) uint64 {
	return uint64((value-baseValue)*coef + eps)
}

func calcInstantDelta(baseValue float64, coef float64, value float64) uint64 {
	f64 := (value - baseValue) * coef
	if f64 > 0 {
		f64 += eps
	} else {
		f64 -= eps
	}
	return bin.EncodeZigZag(int64(f64))
}

func toCumulativeUint64(value float64, coef float64) uint64 {
	return uint64(value * coef)
}

func toInstantUint64(value float64, coef float64) uint64 {
	return bin.EncodeZigZag(int64(value * coef))
}

func toCumulativeFloat64(value uint64, coef float64) float64 {
	return float64(value) / coef
}

func toInstantFloat64(value uint64, coef float64) float64 {
	return float64(bin.DecodeZigZag(value)) / coef
}

/*
Формат:
base value (var u64)
delta (var u64)
q (qty of previous deltas; msb=0 - series, msb=1 - non series, low 7 bits - qty 1..128)
delta
delta
delta
q

Дельта рахується від base value.
Декодування у зворотньому порядку.

Після base value слідують run або literal блоки.
Run блок - це delta + header byte в кінці.
Literal блок - це від одної до N дельт + header byte в кінці.
Спочатку створюється literal блок.
Якщо для останної дельти додається дублікат, literal блок модифікується -
лічильник зменшується до 1. А остання дельта переміщюється в новий run блок.
Причому лічильник 0 - означає 2 елементи. Приклад:
До:
v1 v2 v3 h-byte(literal, 3)  <- v3
Після:
v1 v2 h-byte(literal, 2) v3 h-byte(run, 2)
*/
