// Copyright (c) 2022+ Klaus Post. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package s2

import (
	
	
	
	
	
	
)

const (
	S2IndexHeader   = "s2idx\x00"
	S2IndexTrailer  = "\x00xdi2s"
	maxIndexEntries = 1 << 16
	// If distance is less than this, we do not add the entry.
	minIndexDist = 1 << 20
)

// Index represents an S2/Snappy index.
type Index struct {
	TotalUncompressed int64 // Total Uncompressed size if known. Will be -1 if unknown.
	TotalCompressed   int64 // Total Compressed size if known. Will be -1 if unknown.
	info              []struct {
		compressedOffset   int64
		uncompressedOffset int64
	}
	estBlockUncomp int64
}

func ( *Index) ( int) {
	.estBlockUncomp = int64()
	.TotalCompressed = -1
	.TotalUncompressed = -1
	if len(.info) > 0 {
		.info = .info[:0]
	}
}

// allocInfos will allocate an empty slice of infos.
func ( *Index) ( int) {
	if  > maxIndexEntries {
		panic("n > maxIndexEntries")
	}
	.info = make([]struct {
		   int64
		 int64
	}, 0, )
}

// add an uncompressed and compressed pair.
// Entries must be sent in order.
func ( *Index) (,  int64) error {
	if  == nil {
		return nil
	}
	 := len(.info) - 1
	if  >= 0 {
		 := .info[]
		if .uncompressedOffset ==  {
			// Uncompressed didn't change, don't add entry,
			// but update start index.
			.compressedOffset = 
			.info[] = 
			return nil
		}
		if .uncompressedOffset >  {
			return fmt.Errorf("internal error: Earlier uncompressed received (%d > %d)", .uncompressedOffset, )
		}
		if .compressedOffset >  {
			return fmt.Errorf("internal error: Earlier compressed received (%d > %d)", .uncompressedOffset, )
		}
		if .uncompressedOffset+minIndexDist >  {
			// Only add entry if distance is large enough.
			return nil
		}
	}
	.info = append(.info, struct {
		   int64
		 int64
	}{: , : })
	return nil
}

// Find the offset at or before the wanted (uncompressed) offset.
// If offset is 0 or positive it is the offset from the beginning of the file.
// If the uncompressed size is known, the offset must be within the file.
// If an offset outside the file is requested io.ErrUnexpectedEOF is returned.
// If the offset is negative, it is interpreted as the distance from the end of the file,
// where -1 represents the last byte.
// If offset from the end of the file is requested, but size is unknown,
// ErrUnsupported will be returned.
func ( *Index) ( int64) (,  int64,  error) {
	if .TotalUncompressed < 0 {
		return 0, 0, ErrCorrupt
	}
	if  < 0 {
		 = .TotalUncompressed + 
		if  < 0 {
			return 0, 0, io.ErrUnexpectedEOF
		}
	}
	if  > .TotalUncompressed {
		return 0, 0, io.ErrUnexpectedEOF
	}
	if len(.info) > 200 {
		 := sort.Search(len(.info), func( int) bool {
			return .info[].uncompressedOffset > 
		})
		if  == 0 {
			 = 1
		}
		return .info[-1].compressedOffset, .info[-1].uncompressedOffset, nil
	}
	for ,  := range .info {
		if .uncompressedOffset >  {
			break
		}
		 = .compressedOffset
		 = .uncompressedOffset
	}
	return , , nil
}

// reduce to stay below maxIndexEntries
func ( *Index) () {
	if len(.info) < maxIndexEntries && .estBlockUncomp >= minIndexDist {
		return
	}

	// Algorithm, keep 1, remove removeN entries...
	 := (len(.info) + 1) / maxIndexEntries
	 := .info
	 := 0

	// Each block should be at least 1MB, but don't reduce below 1000 entries.
	for .estBlockUncomp*(int64()+1) < minIndexDist && len(.info)/(+1) > 1000 {
		++
	}
	for  := 0;  < len(); ++ {
		.info[] = []
		++
		 += 
	}
	.info = .info[:]
	// Update maxblock estimate.
	.estBlockUncomp += .estBlockUncomp * int64()
}

func ( *Index) ( []byte, ,  int64) []byte {
	.reduce()
	var  [binary.MaxVarintLen64]byte

	 := len()
	// We make the start a skippable header+size.
	 = append(, ChunkTypeIndex, 0, 0, 0)
	 = append(, []byte(S2IndexHeader)...)
	// Total Uncompressed size
	 := binary.PutVarint([:], )
	 = append(, [:]...)
	// Total Compressed size
	 = binary.PutVarint([:], )
	 = append(, [:]...)
	// Put EstBlockUncomp size
	 = binary.PutVarint([:], .estBlockUncomp)
	 = append(, [:]...)
	// Put length
	 = binary.PutVarint([:], int64(len(.info)))
	 = append(, [:]...)

	// Check if we should add uncompressed offsets
	var  byte
	for ,  := range .info {
		if  == 0 {
			if .uncompressedOffset != 0 {
				 = 1
				break
			}
			continue
		}
		if .uncompressedOffset != .info[-1].uncompressedOffset+.estBlockUncomp {
			 = 1
			break
		}
	}
	 = append(, )

	// Add each entry
	if  == 1 {
		for ,  := range .info {
			 := .uncompressedOffset
			if  > 0 {
				 := .info[-1]
				 -= .uncompressedOffset + (.estBlockUncomp)
			}
			 = binary.PutVarint([:], )
			 = append(, [:]...)
		}
	}

	// Initial compressed size estimate.
	 := .estBlockUncomp / 2

	for ,  := range .info {
		 := .compressedOffset
		if  > 0 {
			 := .info[-1]
			 -= .compressedOffset + 
			// Update compressed size prediction, with half the error.
			 +=  / 2
		}
		 = binary.PutVarint([:], )
		 = append(, [:]...)
	}

	// Add Total Size.
	// Stored as fixed size for easier reading.
	binary.LittleEndian.PutUint32([:], uint32(len()-+4+len(S2IndexTrailer)))
	 = append(, [:4]...)
	// Trailer
	 = append(, []byte(S2IndexTrailer)...)

	// Update size
	 := len() -  - skippableFrameHeader
	[+1] = uint8( >> 0)
	[+2] = uint8( >> 8)
	[+3] = uint8( >> 16)
	//fmt.Printf("chunklen: 0x%x Uncomp:%d, Comp:%d\n", chunkLen, uncompTotal, compTotal)
	return 
}

// Load a binary index.
// A zero value Index can be used or a previous one can be reused.
func ( *Index) ( []byte) ([]byte, error) {
	if len() <= 4+len(S2IndexHeader)+len(S2IndexTrailer) {
		return , io.ErrUnexpectedEOF
	}
	if [0] != ChunkTypeIndex {
		return , ErrCorrupt
	}
	 := int([1]) | int([2])<<8 | int([3])<<16
	 = [4:]

	// Validate we have enough...
	if len() <  {
		return , io.ErrUnexpectedEOF
	}
	if !bytes.Equal([:len(S2IndexHeader)], []byte(S2IndexHeader)) {
		return , ErrUnsupported
	}
	 = [len(S2IndexHeader):]

	// Total Uncompressed
	if ,  := binary.Varint();  <= 0 ||  < 0 {
		return , ErrCorrupt
	} else {
		.TotalUncompressed = 
		 = [:]
	}

	// Total Compressed
	if ,  := binary.Varint();  <= 0 {
		return , ErrCorrupt
	} else {
		.TotalCompressed = 
		 = [:]
	}

	// Read EstBlockUncomp
	if ,  := binary.Varint();  <= 0 {
		return , ErrCorrupt
	} else {
		if  < 0 {
			return , ErrCorrupt
		}
		.estBlockUncomp = 
		 = [:]
	}

	var  int
	if ,  := binary.Varint();  <= 0 {
		return , ErrCorrupt
	} else {
		if  < 0 ||  > maxIndexEntries {
			return , ErrCorrupt
		}
		 = int()
		 = [:]
	}
	if cap(.info) <  {
		.allocInfos()
	}
	.info = .info[:]

	if len() < 1 {
		return , io.ErrUnexpectedEOF
	}
	 := [0]
	 = [1:]
	if &1 !=  {
		return , ErrCorrupt
	}

	// Add each uncompressed entry
	for  := range .info {
		var  int64
		if  != 0 {
			// Load delta
			if ,  := binary.Varint();  <= 0 {
				return , ErrCorrupt
			} else {
				 = 
				 = [:]
			}
		}

		if  > 0 {
			 := .info[-1].uncompressedOffset
			 +=  + (.estBlockUncomp)
			if  <=  {
				return , ErrCorrupt
			}
		}
		if  < 0 {
			return , ErrCorrupt
		}
		.info[].uncompressedOffset = 
	}

	// Initial compressed size estimate.
	 := .estBlockUncomp / 2

	// Add each compressed entry
	for  := range .info {
		var  int64
		if ,  := binary.Varint();  <= 0 {
			return , ErrCorrupt
		} else {
			 = 
			 = [:]
		}

		if  > 0 {
			// Update compressed size prediction, with half the error.
			 :=  + /2

			 := .info[-1].compressedOffset
			 +=  + 
			if  <=  {
				return , ErrCorrupt
			}
			 = 
		}
		if  < 0 {
			return , ErrCorrupt
		}
		.info[].compressedOffset = 
	}
	if len() < 4+len(S2IndexTrailer) {
		return , io.ErrUnexpectedEOF
	}
	// Skip size...
	 = [4:]

	// Check trailer...
	if !bytes.Equal([:len(S2IndexTrailer)], []byte(S2IndexTrailer)) {
		return , ErrCorrupt
	}
	return [len(S2IndexTrailer):], nil
}

// LoadStream will load an index from the end of the supplied stream.
// ErrUnsupported will be returned if the signature cannot be found.
// ErrCorrupt will be returned if unexpected values are found.
// io.ErrUnexpectedEOF is returned if there are too few bytes.
// IO errors are returned as-is.
func ( *Index) ( io.ReadSeeker) error {
	// Go to end.
	,  := .Seek(-10, io.SeekEnd)
	if  != nil {
		return 
	}
	var  [10]byte
	_,  = io.ReadFull(, [:])
	if  != nil {
		return 
	}
	// Check trailer...
	if !bytes.Equal([4:4+len(S2IndexTrailer)], []byte(S2IndexTrailer)) {
		return ErrUnsupported
	}
	 := binary.LittleEndian.Uint32([:4])
	if  > maxChunkSize+skippableFrameHeader {
		return ErrCorrupt
	}
	_,  = .Seek(-int64(), io.SeekEnd)
	if  != nil {
		return 
	}

	// Read index.
	 := make([]byte, )
	_,  = io.ReadFull(, )
	if  != nil {
		return 
	}
	_,  = .Load()
	return 
}

// IndexStream will return an index for a stream.
// The stream structure will be checked, but
// data within blocks is not verified.
// The returned index can either be appended to the end of the stream
// or stored separately.
func ( io.Reader) ([]byte, error) {
	var  Index
	var  [maxChunkSize]byte
	var  bool
	for {
		,  := io.ReadFull(, [:4])
		if  != nil {
			if  == io.EOF {
				return .appendTo(nil, .TotalUncompressed, .TotalCompressed), nil
			}
			return nil, 
		}
		// Start of this chunk.
		 := .TotalCompressed
		.TotalCompressed += 4

		 := [0]
		if ! {
			if  != chunkTypeStreamIdentifier {
				return nil, ErrCorrupt
			}
			 = true
		}
		 := int([1]) | int([2])<<8 | int([3])<<16
		if  < checksumSize {
			return nil, ErrCorrupt
		}

		.TotalCompressed += int64()
		_,  = io.ReadFull(, [:])
		if  != nil {
			return nil, io.ErrUnexpectedEOF
		}
		// The chunk types are specified at
		// https://github.com/google/snappy/blob/master/framing_format.txt
		switch  {
		case chunkTypeCompressedData:
			// Section 4.2. Compressed data (chunk type 0x00).
			// Skip checksum.
			,  := DecodedLen([checksumSize:])
			if  != nil {
				return nil, 
			}
			if  > maxBlockSize {
				return nil, ErrCorrupt
			}
			if .estBlockUncomp == 0 {
				// Use first block for estimate...
				.estBlockUncomp = int64()
			}
			 = .add(, .TotalUncompressed)
			if  != nil {
				return nil, 
			}
			.TotalUncompressed += int64()
			continue
		case chunkTypeUncompressedData:
			 :=  - checksumSize
			if  > maxBlockSize {
				return nil, ErrCorrupt
			}
			if .estBlockUncomp == 0 {
				// Use first block for estimate...
				.estBlockUncomp = int64()
			}
			 = .add(, .TotalUncompressed)
			if  != nil {
				return nil, 
			}
			.TotalUncompressed += int64()
			continue
		case chunkTypeStreamIdentifier:
			// Section 4.1. Stream identifier (chunk type 0xff).
			if  != len(magicBody) {
				return nil, ErrCorrupt
			}

			if string([:len(magicBody)]) != magicBody {
				if string([:len(magicBody)]) != magicBodySnappy {
					return nil, ErrCorrupt
				}
			}

			continue
		}

		if  <= 0x7f {
			// Section 4.5. Reserved unskippable chunks (chunk types 0x02-0x7f).
			return nil, ErrUnsupported
		}
		if  > maxChunkSize {
			return nil, ErrUnsupported
		}
		// Section 4.4 Padding (chunk type 0xfe).
		// Section 4.6. Reserved skippable chunks (chunk types 0x80-0xfd).
	}
}

// JSON returns the index as JSON text.
func ( *Index) () []byte {
	type  struct {
		   int64 `json:"compressed"`
		 int64 `json:"uncompressed"`
	}
	 := struct {
		 int64    `json:"total_uncompressed"` // Total Uncompressed size if known. Will be -1 if unknown.
		   int64    `json:"total_compressed"`   // Total Compressed size if known. Will be -1 if unknown.
		           [] `json:"offsets"`
		    int64    `json:"est_block_uncompressed"`
	}{
		: .TotalUncompressed,
		:   .TotalCompressed,
		:    .estBlockUncomp,
	}
	for ,  := range .info {
		. = append(., {: .compressedOffset, : .uncompressedOffset})
	}
	,  := json.MarshalIndent(, "", "  ")
	return 
}

// RemoveIndexHeaders will trim all headers and trailers from a given index.
// This is expected to save 20 bytes.
// These can be restored using RestoreIndexHeaders.
// This removes a layer of security, but is the most compact representation.
// Returns nil if headers contains errors.
// The returned slice references the provided slice.
func ( []byte) []byte {
	const  = 4 + len(S2IndexHeader) + len(S2IndexTrailer) + 4
	if len() <=  {
		return nil
	}
	if [0] != ChunkTypeIndex {
		return nil
	}
	 := int([1]) | int([2])<<8 | int([3])<<16
	 = [4:]

	// Validate we have enough...
	if len() <  {
		return nil
	}
	 = [:]

	if !bytes.Equal([:len(S2IndexHeader)], []byte(S2IndexHeader)) {
		return nil
	}
	 = [len(S2IndexHeader):]
	if !bytes.HasSuffix(, []byte(S2IndexTrailer)) {
		return nil
	}
	 = bytes.TrimSuffix(, []byte(S2IndexTrailer))

	if len() < 4 {
		return nil
	}
	return [:len()-4]
}

// RestoreIndexHeaders will index restore headers removed by RemoveIndexHeaders.
// No error checking is performed on the input.
// If a 0 length slice is sent, it is returned without modification.
func ( []byte) []byte {
	if len() == 0 {
		return 
	}
	 := make([]byte, 0, 4+len(S2IndexHeader)+len()+len(S2IndexTrailer)+4)
	 = append(, ChunkTypeIndex, 0, 0, 0)
	 = append(, []byte(S2IndexHeader)...)
	 = append(, ...)

	var  [4]byte
	binary.LittleEndian.PutUint32([:], uint32(len()+4+len(S2IndexTrailer)))
	 = append(, [:4]...)
	// Trailer
	 = append(, []byte(S2IndexTrailer)...)

	 := len() - skippableFrameHeader
	[1] = uint8( >> 0)
	[2] = uint8( >> 8)
	[3] = uint8( >> 16)
	return 
}