// Copyright 2015, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

package xflate

const (
	unknownType = iota
	deflateType
	indexType
	footerType
)

type index struct {
	// Records is a list of records that indicate the location of all chunks
	// in the stream. However, rather than recording the starting offset of
	// each chunk, only the ending offsets are recorded.
	//
	// The starting record {0, 0} is not included since it is implied.
	// The last record effectively holds the total size of the stream.
	Records []record

	BackSize  int64 // Size of previous index when encoded
	IndexSize int64 // Size of this index when encoded
}

type record struct {
	CompOffset int64 // Offset in compressed stream where decompression can start from
	RawOffset  int64 // The uncompressed offset that CompOffset is associated with
	Type       int   // Type of the record
}

// Reset resets the index.
func (idx *index) Reset() {
	*idx = index{Records: idx.Records[:0]}
}

// AppendRecord appends a new record to the end of the index and reports whether
// the operation was successful or not.
func (idx *index) AppendRecord(compSize, rawSize int64, typ int) bool {
	if rawSize < 0 || compSize < 0 {
		return false // Invalid size
	}

	lastRec := idx.LastRecord()
	rec := record{
		CompOffset: lastRec.CompOffset + compSize,
		RawOffset:  lastRec.RawOffset + rawSize,
		Type:       typ,
	}
	if rec.CompOffset < lastRec.CompOffset || rec.RawOffset < lastRec.RawOffset {
		return false // Overflow detected
	}
	idx.Records = append(idx.Records, rec)
	return true
}

// AppendIndex appends the contents of another index onto the current receiver
// and reports whether the operation was successful or not.
func (idx *index) AppendIndex(other *index) bool {
	var preRec record
	for i, rec := range other.Records {
		csize, rsize := rec.CompOffset-preRec.CompOffset, rec.RawOffset-preRec.RawOffset
		if !idx.AppendRecord(csize, rsize, rec.Type) {
			idx.Records = idx.Records[:len(idx.Records)-i] // Ensure atomic append
			return false
		}
		preRec = rec
	}
	return true
}

// Search searches for the record that best matches the raw offset given.
// This search will return the location of the record with the lowest
// RawOffset that is still greater than the given offset.
// It return -1 if such a record does not exist.
//
// This method is intended to be used in conjunction with GetRecords,
// which returns a pair of records (prev, curr).
// With these records, the following can be computed:
//
//	// Where in the underlying reader the decompressor should start from.
//	compOffset := prev.CompOffset
//
//	// The total number of uncompressed bytes to discard to reach offset.
//	rawDiscard := offset - prev.RawOffset
//
//	// The total compressed size of the current block.
//	compSize := curr.CompOffset - prev.CompOffset
//
//	// The total uncompressed size of the current block.
//	rawSize := curr.RawOffset - prev.RawOffset
func (idx *index) Search(offset int64) int {
	recs := idx.Records
	i, imin, imax := -1, 0, len(recs)-1
	for imax >= imin && i == -1 {
		imid := (imin + imax) / 2
		gteCurr := bool(offset >= recs[imid].RawOffset)
		ltNext := bool(imid+1 >= len(recs) || offset < recs[imid+1].RawOffset)
		if gteCurr && ltNext {
			i = imid
		} else if gteCurr {
			imin = imid + 1
		} else {
			imax = imid - 1
		}
	}
	return i + 1
}

// GetRecords returns the previous and current records at the given position.
// This method will automatically bind the search position within the bounds
// of the index. Thus, this will return zero value records if the position is
// too low, and the last record if the value is too high.
func (idx *index) GetRecords(i int) (prev, curr record) {
	recs := idx.Records
	if i > len(recs) {
		i = len(recs)
	}
	if i-1 >= 0 && i-1 < len(recs) {
		prev = recs[i-1]
	}
	if i >= 0 && i < len(recs) {
		curr = recs[i]
	} else {
		curr = prev
		curr.Type = unknownType
	}
	return prev, curr
}

// LastRecord returns the last record if it exists, otherwise the zero value.
func (idx *index) LastRecord() record {
	var rec record
	if len(idx.Records) > 0 {
		rec = idx.Records[len(idx.Records)-1]
	}
	return rec
}
