// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package modindex

import (
	"cmd/go/internal/base"
	"encoding/binary"
	"go/token"
	"sort"
)

const indexVersion = "go index v1" // 11 bytes (plus \n), to align uint32s in index

// encodeModuleBytes produces the encoded representation of the module index.
// encodeModuleBytes may modify the packages slice.
func encodeModuleBytes(packages []*rawPackage) []byte {
	e := newEncoder()
	e.Bytes([]byte(indexVersion + "\n"))
	stringTableOffsetPos := e.Pos() // fill this at the end
	e.Uint32(0)                     // string table offset
	sort.Slice(packages, func(i, j int) bool {
		return packages[i].dir < packages[j].dir
	})
	e.Int(len(packages))
	packagesPos := e.Pos()
	for _, p := range packages {
		e.String(p.dir)
		e.Int(0)
	}
	for i, p := range packages {
		e.IntAt(e.Pos(), packagesPos+8*i+4)
		encodePackage(e, p)
	}
	e.IntAt(e.Pos(), stringTableOffsetPos)
	e.Bytes(e.stringTable)
	e.Bytes([]byte{0xFF}) // end of string table marker
	return e.b
}

func encodePackageBytes(p *rawPackage) []byte {
	return encodeModuleBytes([]*rawPackage{p})
}

func encodePackage(e *encoder, p *rawPackage) {
	e.String(p.error)
	e.String(p.dir)
	e.Int(len(p.sourceFiles))      // number of source files
	sourceFileOffsetPos := e.Pos() // the pos of the start of the source file offsets
	for range p.sourceFiles {
		e.Int(0)
	}
	for i, f := range p.sourceFiles {
		e.IntAt(e.Pos(), sourceFileOffsetPos+4*i)
		encodeFile(e, f)
	}
}

func encodeFile(e *encoder, f *rawFile) {
	e.String(f.error)
	e.String(f.parseError)
	e.String(f.synopsis)
	e.String(f.name)
	e.String(f.pkgName)
	e.Bool(f.ignoreFile)
	e.Bool(f.binaryOnly)
	e.String(f.cgoDirectives)
	e.String(f.goBuildConstraint)

	e.Int(len(f.plusBuildConstraints))
	for _, s := range f.plusBuildConstraints {
		e.String(s)
	}

	e.Int(len(f.imports))
	for _, m := range f.imports {
		e.String(m.path)
		e.Position(m.position)
	}

	e.Int(len(f.embeds))
	for _, embed := range f.embeds {
		e.String(embed.pattern)
		e.Position(embed.position)
	}
}

func newEncoder() *encoder {
	e := &encoder{strings: make(map[string]int)}

	// place the empty string at position 0 in the string table
	e.stringTable = append(e.stringTable, 0)
	e.strings[""] = 0

	return e
}

func (e *encoder) Position(position token.Position) {
	e.String(position.Filename)
	e.Int(position.Offset)
	e.Int(position.Line)
	e.Int(position.Column)
}

type encoder struct {
	b           []byte
	stringTable []byte
	strings     map[string]int
}

func (e *encoder) Pos() int {
	return len(e.b)
}

func (e *encoder) Bytes(b []byte) {
	e.b = append(e.b, b...)
}

func (e *encoder) String(s string) {
	if n, ok := e.strings[s]; ok {
		e.Int(n)
		return
	}
	pos := len(e.stringTable)
	e.strings[s] = pos
	e.Int(pos)
	e.stringTable = binary.AppendUvarint(e.stringTable, uint64(len(s)))
	e.stringTable = append(e.stringTable, []byte(s)...)
}

func (e *encoder) Bool(b bool) {
	if b {
		e.Uint32(1)
	} else {
		e.Uint32(0)
	}
}

func (e *encoder) Uint32(n uint32) {
	e.b = binary.LittleEndian.AppendUint32(e.b, n)
}

// Int encodes n. Note that all ints are written to the index as uint32s,
// and to avoid problems on 32-bit systems we require fitting into a 32-bit int.
func (e *encoder) Int(n int) {
	if n < 0 || int(int32(n)) != n {
		base.Fatalf("go: attempting to write an int to the index that overflows int32")
	}
	e.Uint32(uint32(n))
}

func (e *encoder) IntAt(n int, at int) {
	if n < 0 || int(int32(n)) != n {
		base.Fatalf("go: attempting to write an int to the index that overflows int32")
	}
	binary.LittleEndian.PutUint32(e.b[at:], uint32(n))
}
