// Copyright 2019 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.

// Writing Go object files.

// This file is a modified version of cmd/internal/obj/objfile2.go

package goobj2

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/Binject/debug/goobj2/internal/bio"
	"github.com/Binject/debug/goobj2/internal/goobj2"
)

// Write writes the contents of the parsed archive to disk.
func (pkg *Package) Write(path string) (err error) {
	b, err := bio.Create(path)
	if err != nil {
		return fmt.Errorf("error creating object file: %v", err)
	}
	defer func() {
		closeErr := b.Close()
		if closeErr != nil && err == nil {
			err = closeErr
		}
	}()

	// Archive headers
	b.Write(archiveHeader)
	var arhdr [archiveHeaderLen]byte
	var curArHdrOff, curObjStartOff int64
	for i := range pkg.ArchiveMembers {
		ctxt := &pkg.ArchiveMembers[i]
		ar := ctxt.ArchiveHeader
		curArHdrOff = b.Offset()

		copy(arhdr[:], fmt.Sprintf("%-16s%-12s%-6s%-6s%-8s%-10d`\n", ar.Name, ar.Date, ar.UID, ar.GID, ar.Mode, ar.Size))
		b.Write(arhdr[:])
		curObjStartOff = b.Offset()
		b.Write(ar.Data)
		if ctxt.IsDataObj {
			continue
		}

		genFuncInfoSyms(ctxt)

		w := writer{
			Writer: goobj2.NewWriter(b),
			ctxt:   ctxt,
		}

		start := b.Offset()

		// Header
		// We just reserve the space. We'll fill in the offsets later.
		ctxt.ObjHeader.Write(w.Writer)

		// String table
		w.StringTable()

		// Autolib
		ctxt.ObjHeader.Offsets[goobj2.BlkAutolib] = w.Offset()
		for i := range ctxt.Imports {
			ctxt.Imports[i].Write(w.Writer)
		}

		// Package references
		ctxt.ObjHeader.Offsets[goobj2.BlkPkgIdx] = w.Offset()
		w.StringRef("")
		for _, pkg := range ctxt.Packages {
			w.StringRef(pkg)
		}

		// DWARF file table
		ctxt.ObjHeader.Offsets[goobj2.BlkDwarfFile] = w.Offset()
		for _, f := range ctxt.DWARFFileList {
			w.StringRef(filepath.ToSlash(f))
		}

		// Symbol definitions
		ctxt.ObjHeader.Offsets[goobj2.BlkSymdef] = w.Offset()
		for _, s := range ctxt.SymDefs {
			w.Sym(s)
		}

		// Non-pkg symbol definitions
		ctxt.ObjHeader.Offsets[goobj2.BlkNonpkgdef] = w.Offset()
		for _, s := range ctxt.NonPkgSymDefs {
			w.Sym(s)
		}

		// Non-pkg symbol references
		ctxt.ObjHeader.Offsets[goobj2.BlkNonpkgref] = w.Offset()
		for _, s := range ctxt.NonPkgSymRefs {
			w.Sym(s)
		}

		// Reloc indexes
		ctxt.ObjHeader.Offsets[goobj2.BlkRelocIdx] = w.Offset()
		nreloc := uint32(0)
		lists := [][]*Sym{ctxt.SymDefs, ctxt.NonPkgSymDefs}
		for _, list := range lists {
			for _, s := range list {
				w.Uint32(nreloc)
				nreloc += uint32(len(s.Reloc))
			}
		}
		w.Uint32(nreloc)

		// Symbol Info indexes
		ctxt.ObjHeader.Offsets[goobj2.BlkAuxIdx] = w.Offset()
		naux := uint32(0)
		for _, list := range lists {
			for _, s := range list {
				w.Uint32(naux)
				naux += uint32(nAuxSym(s))
			}
		}
		w.Uint32(naux)

		// Data indexes
		ctxt.ObjHeader.Offsets[goobj2.BlkDataIdx] = w.Offset()
		dataOff := uint32(0)
		for _, list := range lists {
			for _, s := range list {
				w.Uint32(dataOff)
				dataOff += uint32(len(s.Data))
			}
		}
		w.Uint32(dataOff)

		// Relocs
		ctxt.ObjHeader.Offsets[goobj2.BlkReloc] = w.Offset()
		for _, list := range lists {
			for _, s := range list {
				for i := range s.Reloc {
					w.Reloc(&s.Reloc[i])
				}
			}
		}

		// Aux symbol info
		ctxt.ObjHeader.Offsets[goobj2.BlkAux] = w.Offset()
		for _, list := range lists {
			for _, s := range list {
				w.Aux(s)
			}
		}

		// Data
		ctxt.ObjHeader.Offsets[goobj2.BlkData] = w.Offset()
		for _, list := range lists {
			for _, s := range list {
				w.Bytes(s.Data)
			}
		}

		// Pcdata
		ctxt.ObjHeader.Offsets[goobj2.BlkPcdata] = w.Offset()
		for _, ts := range ctxt.textSyms {
			w.Bytes(ts.Func.PCSP)
			w.Bytes(ts.Func.PCFile)
			w.Bytes(ts.Func.PCLine)
			w.Bytes(ts.Func.PCInline)
			for i := range ts.Func.PCData {
				w.Bytes(ts.Func.PCData[i])
			}
		}

		// Referenced symbol names from other packages
		ctxt.ObjHeader.Offsets[goobj2.BlkRefName] = w.Offset()
		for _, ref := range ctxt.SymRefs {
			var o goobj2.RefName
			o.SetSym(ref.SymRef)
			o.SetName(ref.Name, w.Writer)
			o.Write(w.Writer)
		}

		objEnd := w.Offset()
		ctxt.ObjHeader.Offsets[goobj2.BlkEnd] = objEnd

		// If the object size is odd, make it even by adding an
		// extra null byte as padding
		size := int64(objEnd) + (start - curObjStartOff)
		end := start + int64(w.Offset())
		if size%2 != 0 {
			b.WriteByte(0x00)
			end++
		}

		// Fix size field of the last archive header
		b.MustSeek(curArHdrOff+48, 0)
		b.WriteString(fmt.Sprintf("%-10d", size))

		// Fix up block offsets in the object header
		b.MustSeek(start, 0)
		ctxt.ObjHeader.Write(w.Writer)
		b.MustSeek(end, 0)
	}

	return nil
}

type writer struct {
	*goobj2.Writer
	ctxt *ArchiveMember
}

func (w *writer) StringTable() {
	w.AddString("")
	for _, p := range w.ctxt.Imports {
		w.AddString(p.Pkg)
	}
	for _, pkg := range w.ctxt.Packages {
		w.AddString(pkg)
	}

	writeSymStrings := func(s *Sym) {
		w.AddString(s.Name)

		for _, r := range s.Reloc {
			w.AddString(r.Name)
		}
		if s.Type != nil {
			w.AddString(s.Name)
		}

		if s.Kind == STEXT && s.Func != nil {
			for _, d := range s.Func.FuncData {
				w.AddString(d.Sym.Name)
			}
			for _, f := range s.Func.File {
				w.AddString(filepath.ToSlash(f.Name))
			}
			for _, call := range s.Func.InlTree {
				w.AddString(call.File.Name)
				w.AddString(call.Func.Name)
			}

			dwsyms := []*SymRef{s.Func.DwarfRanges, s.Func.DwarfLoc, s.Func.DwarfDebugLines, s.Func.FuncInfo}
			for _, dws := range dwsyms {
				if dws != nil {
					w.AddString(dws.Name)
				}
			}
		}
	}

	// Symbols of type STEXT (that have functions) are written first
	for _, ts := range w.ctxt.textSyms {
		writeSymStrings(ts)
	}

	syms := [][]*Sym{w.ctxt.NonPkgSymDefs, w.ctxt.SymDefs, w.ctxt.NonPkgSymRefs}
	for _, list := range syms {
		for _, s := range list {
			if s.Kind == STEXT {
				continue
			}

			writeSymStrings(s)
		}
	}
	for _, r := range w.ctxt.SymRefs {
		w.AddString(r.Name)
	}

	for _, f := range w.ctxt.DWARFFileList {
		w.AddString(filepath.ToSlash(f))
	}
}

func (w *writer) Sym(s *Sym) {
	name := s.Name
	if strings.HasPrefix(name, "gofile..") {
		name = filepath.ToSlash(name)
	}

	var o goobj2.Sym
	o.SetName(name, w.Writer)
	o.SetABI(s.ABI)
	o.SetType(uint8(s.Kind))
	o.SetFlag(s.Flag)
	o.SetSiz(s.Size)
	o.SetAlign(s.Align)
	o.Write(w.Writer)
}

func (w *writer) Reloc(r *Reloc) {
	var o goobj2.Reloc
	o.SetOff(int32(r.Offset))
	o.SetSiz(uint8(r.Size))
	o.SetType(uint8(r.Type))
	o.SetAdd(r.Add)
	o.SetSym(r.Sym)
	o.Write(w.Writer)
}

func (w *writer) aux1(typ uint8, rs goobj2.SymRef) {
	var o goobj2.Aux
	o.SetType(typ)
	o.SetSym(rs)
	o.Write(w.Writer)
}

func (w *writer) Aux(s *Sym) {
	if s.Type != nil {
		w.aux1(goobj2.AuxGotype, s.Type.SymRef)
	}
	if s.Func != nil {
		w.aux1(goobj2.AuxFuncInfo, s.Func.FuncInfo.SymRef)

		for _, d := range s.Func.FuncData {
			w.aux1(goobj2.AuxFuncdata, d.Sym.SymRef)
		}

		if s.Func.DwarfInfo != nil {
			w.aux1(goobj2.AuxDwarfInfo, s.Func.DwarfInfo.SymRef)
		}
		if s.Func.DwarfLoc != nil {
			w.aux1(goobj2.AuxDwarfLoc, s.Func.DwarfLoc.SymRef)
		}
		if s.Func.DwarfRanges != nil {
			w.aux1(goobj2.AuxDwarfRanges, s.Func.DwarfRanges.SymRef)
		}
		if s.Func.DwarfDebugLines != nil {
			w.aux1(goobj2.AuxDwarfLines, s.Func.DwarfDebugLines.SymRef)
		}
	}
}

// return the number of aux symbols s have.
func nAuxSym(s *Sym) int {
	n := 0
	if s.Type != nil {
		n++
	}
	if s.Func != nil {
		// FuncInfo is an aux symbol, each Funcdata is an aux symbol
		n += 1 + len(s.Func.FuncData)
		if s.Func.DwarfInfo != nil {
			n++
		}
		if s.Func.DwarfLoc != nil {
			n++
		}
		if s.Func.DwarfRanges != nil {
			n++
		}
		if s.Func.DwarfDebugLines != nil {
			n++
		}
	}
	return n
}

// generate symbols for FuncInfo.
func genFuncInfoSyms(ctxt *ArchiveMember) {
	var pcdataoff uint32
	var b bytes.Buffer
	for _, s := range ctxt.textSyms {
		if s.Func == nil {
			continue
		}

		o := goobj2.FuncInfo{
			Args:   uint32(s.Func.Args),
			Locals: uint32(s.Func.Frame),
		}
		o.Pcsp = pcdataoff
		pcdataoff += uint32(len(s.Func.PCSP))
		o.Pcfile = pcdataoff
		pcdataoff += uint32(len(s.Func.PCFile))
		o.Pcline = pcdataoff
		pcdataoff += uint32(len(s.Func.PCLine))
		o.Pcinline = pcdataoff
		pcdataoff += uint32(len(s.Func.PCInline))
		o.Pcdata = make([]uint32, len(s.Func.PCData))
		for i, pcd := range s.Func.PCData {
			o.Pcdata[i] = pcdataoff
			pcdataoff += uint32(len(pcd))
		}
		o.PcdataEnd = pcdataoff
		o.Funcdataoff = make([]uint32, len(s.Func.FuncData))
		for i, x := range s.Func.FuncData {
			o.Funcdataoff[i] = x.Offset
		}
		o.File = make([]goobj2.SymRef, len(s.Func.File))
		for i, f := range s.Func.File {
			o.File[i] = f.SymRef
		}
		o.InlTree = make([]goobj2.InlTreeNode, len(s.Func.InlTree))
		for i, inl := range s.Func.InlTree {
			o.InlTree[i] = goobj2.InlTreeNode{
				Parent:   inl.Parent,
				File:     inl.File.SymRef,
				Line:     inl.Line,
				Func:     inl.Func.SymRef,
				ParentPC: inl.ParentPC,
			}
		}

		o.Write(&b)
		ctxt.symMap[s.Func.dataSymIdx].Data = append([]byte(nil), b.Bytes()...)
		b.Reset()
	}
}
