// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package rpmpack

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"sort"
)

const (
	signatures = 0x3e
	immutable  = 0x3f

	typeInt16       = 0x03
	typeInt32       = 0x04
	typeString      = 0x06
	typeBinary      = 0x07
	typeStringArray = 0x08
)

// Only integer types are aligned. This is not just an optimization - some versions
// of rpm fail when integers are not aligned. Other versions fail when non-integers are aligned.
var boundaries = map[int]int{
	typeInt16: 2,
	typeInt32: 4,
}

type IndexEntry struct {
	rpmtype, count int
	data           []byte
}

func (e IndexEntry) indexBytes(tag, contentOffset int) []byte {
	b := &bytes.Buffer{}
	if err := binary.Write(b, binary.BigEndian, []int32{int32(tag), int32(e.rpmtype), int32(contentOffset), int32(e.count)}); err != nil {
		// binary.Write can fail if the underlying Write fails, or the types are invalid.
		// bytes.Buffer's write never error out, it can only panic with OOM.
		panic(err)
	}
	return b.Bytes()
}

func intEntry(rpmtype, size int, value interface{}) IndexEntry {
	b := &bytes.Buffer{}
	if err := binary.Write(b, binary.BigEndian, value); err != nil {
		// binary.Write can fail if the underlying Write fails, or the types are invalid.
		// bytes.Buffer's write never error out, it can only panic with OOM.
		panic(err)
	}
	return IndexEntry{rpmtype, size, b.Bytes()}
}

func EntryInt16(value []int16) IndexEntry {
	return intEntry(typeInt16, len(value), value)
}
func EntryUint16(value []uint16) IndexEntry {
	return intEntry(typeInt16, len(value), value)
}
func EntryInt32(value []int32) IndexEntry {
	return intEntry(typeInt32, len(value), value)
}
func EntryUint32(value []uint32) IndexEntry {
	return intEntry(typeInt32, len(value), value)
}
func EntryString(value string) IndexEntry {
	return IndexEntry{typeString, 1, append([]byte(value), byte(00))}
}
func EntryBytes(value []byte) IndexEntry {
	return IndexEntry{typeBinary, len(value), value}
}

func EntryStringSlice(value []string) IndexEntry {
	b := [][]byte{}
	for _, v := range value {
		b = append(b, []byte(v))
	}
	bb := append(bytes.Join(b, []byte{00}), byte(00))
	return IndexEntry{typeStringArray, len(value), bb}
}

type index struct {
	entries map[int]IndexEntry
	h       int
}

func newIndex(h int) *index {
	return &index{entries: make(map[int]IndexEntry), h: h}
}
func (i *index) Add(tag int, e IndexEntry) {
	i.entries[tag] = e
}
func (i *index) AddEntries(m map[int]IndexEntry) {
	for t, e := range m {
		i.Add(t, e)
	}
}

func (i *index) sortedTags() []int {
	t := []int{}
	for k := range i.entries {
		t = append(t, k)
	}
	sort.Ints(t)
	return t
}

func pad(w *bytes.Buffer, rpmtype, offset int) {
	// We need to align integer entries...
	if b, ok := boundaries[rpmtype]; ok && offset%b != 0 {
		if _, err := w.Write(make([]byte, b-offset%b)); err != nil {
			// binary.Write can fail if the underlying Write fails, or the types are invalid.
			// bytes.Buffer's write never error out, it can only panic with OOM.
			panic(err)
		}
	}
}

// Bytes returns the bytes of the index.
func (i *index) Bytes() ([]byte, error) {
	w := &bytes.Buffer{}
	// Even the header has three parts: The lead, the index entries, and the entries.
	// Because of alignment, we can only tell the actual size and offset after writing
	// the entries.
	entryData := &bytes.Buffer{}
	tags := i.sortedTags()
	offsets := make([]int, len(tags))
	for ii, tag := range tags {
		e := i.entries[tag]
		pad(entryData, e.rpmtype, entryData.Len())
		offsets[ii] = entryData.Len()
		entryData.Write(e.data)
	}
	entryData.Write(i.eigenHeader().data)

	// 4 magic and 4 reserved
	w.Write([]byte{0x8e, 0xad, 0xe8, 0x01, 0, 0, 0, 0})
	// 4 count and 4 size
	// We add the pseudo-entry "eigenHeader" to count.
	if err := binary.Write(w, binary.BigEndian, []int32{int32(len(i.entries)) + 1, int32(entryData.Len())}); err != nil {
		return nil, fmt.Errorf("failed to write eigenHeader: %w", err)
	}
	// Write the eigenHeader index entry
	w.Write(i.eigenHeader().indexBytes(i.h, entryData.Len()-0x10))
	// Write all of the other index entries
	for ii, tag := range tags {
		e := i.entries[tag]
		w.Write(e.indexBytes(tag, offsets[ii]))
	}
	w.Write(entryData.Bytes())
	return w.Bytes(), nil
}

// the eigenHeader is a weird entry. Its index entry is sorted first, but its content
// is last. The content is a 16 byte index entry, which is almost the same as the index
// entry except for the offset. The offset here is ... minus the length of the index entry region.
// Which is always 0x10 * number of entries.
// I kid you not.
func (i *index) eigenHeader() IndexEntry {
	b := &bytes.Buffer{}
	if err := binary.Write(b, binary.BigEndian, []int32{int32(i.h), int32(typeBinary), -int32(0x10 * (len(i.entries) + 1)), int32(0x10)}); err != nil {
		// binary.Write can fail if the underlying Write fails, or the types are invalid.
		// bytes.Buffer's write never error out, it can only panic with OOM.
		panic(err)
	}

	return EntryBytes(b.Bytes())
}

func lead(name, fullVersion string) []byte {
	// RPM format = 0xedabeedb
	// version 3.0 = 0x0300
	// type binary = 0x0000
	// machine archnum (i386?) = 0x0001
	// name ( 66 bytes, with null termination)
	// osnum (linux?) = 0x0001
	// sig type (header-style) = 0x0005
	// reserved 16 bytes of 0x00
	n := []byte(fmt.Sprintf("%s-%s", name, fullVersion))
	if len(n) > 65 {
		n = n[:65]
	}
	n = append(n, make([]byte, 66-len(n))...)
	b := []byte{0xed, 0xab, 0xee, 0xdb, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01}
	b = append(b, n...)
	b = append(b, []byte{0x00, 0x01, 0x00, 0x05}...)
	b = append(b, make([]byte, 16)...)
	return b
}
