// Copyright 2009 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 obj

import (
	"fmt"
	"path/filepath"
	"sort"
	"strings"
)

// A LineHist records the history of the file input stack, which maps the virtual line number,
// an incrementing count of lines processed in any input file and typically named lineno,
// to a stack of file:line pairs showing the path of inclusions that led to that position.
// The first line directive (//line in Go, #line in assembly) is treated as pushing
// a new entry on the stack, so that errors can report both the actual and translated
// line number.
//
// In typical use, the virtual lineno begins at 1, and file line numbers also begin at 1,
// but the only requirements placed upon the numbers by this code are:
//	- calls to Push, Update, and Pop must be monotonically increasing in lineno
//	- except as specified by those methods, virtual and file line number increase
//	  together, so that given (only) calls Push(10, "x.go", 1) and Pop(15),
//	  virtual line 12 corresponds to x.go line 3.
type LineHist struct {
	Top            *LineStack  // current top of stack
	Ranges         []LineRange // ranges for lookup
	Dir            string      // directory to qualify relative paths
	TrimPathPrefix string      // remove leading TrimPath from recorded file names
	GOROOT         string      // current GOROOT
	GOROOT_FINAL   string      // target GOROOT
}

// A LineStack is an entry in the recorded line history.
// Although the history at any given line number is a stack,
// the record for all line processed forms a tree, with common
// stack prefixes acting as parents.
type LineStack struct {
	Parent    *LineStack // parent in inclusion stack
	Lineno    int        // virtual line number where this entry takes effect
	File      string     // file name used to open source file, for error messages
	AbsFile   string     // absolute file name, for pcln tables
	FileLine  int        // line number in file at Lineno
	Directive bool
	Sym       *LSym // for linkgetline - TODO(rsc): remove
}

func (stk *LineStack) fileLineAt(lineno int) int {
	return stk.FileLine + lineno - stk.Lineno
}

// The span of valid linenos in the recorded line history can be broken
// into a set of ranges, each with a particular stack.
// A LineRange records one such range.
type LineRange struct {
	Start int        // starting lineno
	Stack *LineStack // top of stack for this range
}

// startRange starts a new range with the given top of stack.
func (h *LineHist) startRange(lineno int, top *LineStack) {
	h.Top = top
	h.Ranges = append(h.Ranges, LineRange{top.Lineno, top})
}

// setFile sets stk.File = file and also derives stk.AbsFile.
func (h *LineHist) setFile(stk *LineStack, file string) {
	// Note: The exclusion of stk.Directive may be wrong but matches what we've done before.
	// The check for < avoids putting a path prefix on "<autogenerated>".
	abs := file
	if h.Dir != "" && !filepath.IsAbs(file) && !strings.HasPrefix(file, "<") && !stk.Directive {
		abs = filepath.Join(h.Dir, file)
	}

	// Remove leading TrimPathPrefix, or else rewrite $GOROOT to literal $GOROOT.
	if h.TrimPathPrefix != "" && hasPathPrefix(abs, h.TrimPathPrefix) {
		if abs == h.TrimPathPrefix {
			abs = ""
		} else {
			abs = abs[len(h.TrimPathPrefix)+1:]
		}
	} else if hasPathPrefix(abs, h.GOROOT) {
		abs = "$GOROOT" + abs[len(h.GOROOT):]
	}
	if abs == "" {
		abs = "??"
	}
	abs = filepath.Clean(abs)
	stk.AbsFile = abs

	if file == "" {
		file = "??"
	}
	stk.File = file
}

// Does s have t as a path prefix?
// That is, does s == t or does s begin with t followed by a slash?
// For portability, we allow ASCII case folding, so that hasPathPrefix("a/b/c", "A/B") is true.
// Similarly, we allow slash folding, so that hasPathPrefix("a/b/c", "a\\b") is true.
// We do not allow full Unicode case folding, for fear of causing more confusion
// or harm than good. (For an example of the kinds of things that can go wrong,
// see http://article.gmane.org/gmane.linux.kernel/1853266.)
func hasPathPrefix(s string, t string) bool {
	if len(t) > len(s) {
		return false
	}
	var i int
	for i = 0; i < len(t); i++ {
		cs := int(s[i])
		ct := int(t[i])
		if 'A' <= cs && cs <= 'Z' {
			cs += 'a' - 'A'
		}
		if 'A' <= ct && ct <= 'Z' {
			ct += 'a' - 'A'
		}
		if cs == '\\' {
			cs = '/'
		}
		if ct == '\\' {
			ct = '/'
		}
		if cs != ct {
			return false
		}
	}
	return i >= len(s) || s[i] == '/' || s[i] == '\\'
}

// Push records that at that lineno a new file with the given name was pushed onto the input stack.
func (h *LineHist) Push(lineno int, file string) {
	stk := &LineStack{
		Parent:   h.Top,
		Lineno:   lineno,
		FileLine: 1,
	}
	h.setFile(stk, file)
	h.startRange(lineno, stk)
}

// Pop records that at lineno the current file was popped from the input stack.
func (h *LineHist) Pop(lineno int) {
	top := h.Top
	if top == nil {
		return
	}
	if top.Directive && top.Parent != nil { // pop #line level too
		top = top.Parent
	}
	next := top.Parent
	if next == nil {
		h.Top = nil
		h.Ranges = append(h.Ranges, LineRange{lineno, nil})
		return
	}

	// Popping included file. Update parent offset to account for
	// the virtual line number range taken by the included file.
	// Cannot modify the LineStack directly, or else lookups
	// for the earlier line numbers will get the wrong answers,
	// so make a new one.
	stk := new(LineStack)
	*stk = *next
	stk.Lineno = lineno
	stk.FileLine = next.fileLineAt(top.Lineno)
	h.startRange(lineno, stk)
}

// Update records that at lineno the file name and line number were changed using
// a line directive (//line in Go, #line in assembly).
func (h *LineHist) Update(lineno int, file string, line int) {
	top := h.Top
	if top == nil {
		return // shouldn't happen
	}
	var stk *LineStack
	if top.Directive {
		// Update existing entry, except make copy to avoid changing earlier history.
		stk = new(LineStack)
		*stk = *top
	} else {
		// Push new entry.
		stk = &LineStack{
			Parent:    top,
			Directive: true,
		}
	}
	stk.Lineno = lineno
	if stk.File != file {
		h.setFile(stk, file) // only retain string if needed
	}
	stk.FileLine = line
	h.startRange(lineno, stk)
}

// AddImport adds a package to the list of imported packages.
func (ctxt *Link) AddImport(pkg string) {
	ctxt.Imports = append(ctxt.Imports, pkg)
}

// At returns the input stack in effect at lineno.
func (h *LineHist) At(lineno int) *LineStack {
	i := sort.Search(len(h.Ranges), func(i int) bool {
		return h.Ranges[i].Start > lineno
	})
	// Found first entry beyond lineno.
	if i == 0 {
		return nil
	}
	return h.Ranges[i-1].Stack
}

// LineString returns a string giving the file and line number
// corresponding to lineno, for use in error messages.
func (h *LineHist) LineString(lineno int) string {
	stk := h.At(lineno)
	if stk == nil {
		return "<unknown line number>"
	}

	text := fmt.Sprintf("%s:%d", stk.File, stk.fileLineAt(lineno))
	if stk.Directive && stk.Parent != nil {
		stk = stk.Parent
		text += fmt.Sprintf("[%s:%d]", stk.File, stk.fileLineAt(lineno))
	}
	const showFullStack = false // was used by old C compilers
	if showFullStack {
		for stk.Parent != nil {
			lineno = stk.Lineno - 1
			stk = stk.Parent
			text += fmt.Sprintf(" %s:%d", stk.File, stk.fileLineAt(lineno))
			if stk.Directive && stk.Parent != nil {
				stk = stk.Parent
				text += fmt.Sprintf("[%s:%d]", stk.File, stk.fileLineAt(lineno))
			}
		}
	}
	return text
}

// FileLine returns the file name and line number
// at the top of the stack for the given lineno.
func (h *LineHist) FileLine(lineno int) (file string, line int) {
	stk := h.At(lineno)
	if stk == nil {
		return "??", 0
	}
	return stk.File, stk.fileLineAt(lineno)
}

// AbsFileLine returns the absolute file name and line number
// at the top of the stack for the given lineno.
func (h *LineHist) AbsFileLine(lineno int) (file string, line int) {
	stk := h.At(lineno)
	if stk == nil {
		return "??", 0
	}
	return stk.AbsFile, stk.fileLineAt(lineno)
}

// This is a simplified copy of linklinefmt above.
// It doesn't allow printing the full stack, and it returns the file name and line number separately.
// TODO: Unify with linklinefmt somehow.
func linkgetline(ctxt *Link, lineno int32, f **LSym, l *int32) {
	stk := ctxt.LineHist.At(int(lineno))
	if stk == nil || stk.AbsFile == "" {
		*f = Linklookup(ctxt, "??", HistVersion)
		*l = 0
		return
	}
	if stk.Sym == nil {
		stk.Sym = Linklookup(ctxt, stk.AbsFile, HistVersion)
	}
	*f = stk.Sym
	*l = int32(stk.fileLineAt(int(lineno)))
}

func Linkprfile(ctxt *Link, line int) {
	fmt.Printf("%s ", ctxt.LineHist.LineString(line))
}
