package pkglint

// When files are read in by pkglint, they are interpreted in terms of
// lines. For Makefiles, line continuations are handled properly, allowing
// multiple raw lines to end in a single logical line. For other files
// there is a 1:1 translation.
//
// A difference between the raw and the logical lines is that the
// raw lines include the line end sequence, whereas the logical lines
// do not.
//
// Some methods allow modification of the raw lines contained in the
// logical line but leave the Text field untouched. These methods are
// used in the --autofix mode.

import (
	"netbsd.org/pkglint/regex"
	"strconv"
	"strings"
)

type RawLine struct {
	// The line as read in from the file, including newline;
	// can never be empty. Only in the very last line of each file,
	// the trailing newline might be missing.
	orignl string
}

func (rline *RawLine) Orig() string {
	return strings.TrimSuffix(rline.orignl, "\n")
}

type Location struct {
	Filename CurrPath

	// zero means the whole file, -1 means EOF, normal lines start with 1
	lineno int
}

func NewLocation(filename CurrPath, lineno int) Location {
	return Location{filename, lineno}
}

func (loc *Location) Lineno(rawIndex int) int {
	return loc.lineno + rawIndex
}

// File resolves the given path relative to the directory of this
// location.
func (loc *Location) File(rel RelPath) CurrPath {
	return loc.Filename.Dir().JoinNoClean(rel)
}

// Line represents a line of text from a file.
type Line struct {
	Location Location
	Basename RelPath // the last component of the Filename

	// the text of the line, without the trailing newline character;
	// in Makefiles, also contains the text from the continuation lines,
	// joined by single spaces
	Text string

	raw  []*RawLine // contains the original text including trailing newline
	fix  *Autofix   // any changes that pkglint would like to apply to the line
	once Once
}

func NewLine(filename CurrPath, lineno int, text string, rawLine *RawLine) *Line {
	assert(rawLine != nil) // Use NewLineMulti for creating a Line with no RawLine attached to it.
	return NewLineMulti(filename, lineno, text, []*RawLine{rawLine})
}

// NewLineMulti is for logical Makefile lines that end with backslash.
func NewLineMulti(filename CurrPath, firstLine int, text string, rawLines []*RawLine) *Line {
	return &Line{NewLocation(filename, firstLine), filename.Base(), text, rawLines, nil, Once{}}
}

// NewLineEOF creates a dummy line for logging, with the "line number" EOF.
func NewLineEOF(filename CurrPath) *Line {
	return NewLineMulti(filename, -1, "", nil)
}

// NewLineWhole creates a dummy line for logging messages that affect a file as a whole.
func NewLineWhole(filename CurrPath) *Line {
	return NewLineMulti(filename, 0, "", nil)
}

func (line *Line) Filename() CurrPath { return line.Location.Filename }

// File resolves the given path relative to the directory where this line
// appears in.
func (line *Line) File(rel RelPath) CurrPath { return line.Location.File(rel) }

func (line *Line) Linenos() string {
	first := line.Location.lineno
	if first == -1 {
		return "EOF"
	}
	if first == 0 {
		return ""
	}

	n := len(line.raw)
	if n == 1 {
		return strconv.Itoa(first)
	}
	return sprintf("%d--%d", first, first+n-1)
}

// RelLine returns a reference to another line,
// which can be in the same file or in a different file.
func (line *Line) RelLine(other *Line) string {
	assert(other.Location.Lineno(0) >= 1)

	if line.Filename() != other.Filename() {
		return line.Rel(other.Filename()).String() + ":" + other.Linenos()
	}
	return "line " + other.Linenos()
}

func (line *Line) RelLocation(other Location) string {
	lineno := other.Lineno(0)
	assert(lineno >= 1)

	if line.Filename() != other.Filename {
		return sprintf("%s:%d", line.Rel(other.Filename).String(), lineno)
	}
	return sprintf("line %d", lineno)
}

// Rel returns the relative path from this line to the given file path.
// This is typically used for arguments in diagnostics, which should always be
// relative to the line with which the diagnostic is associated.
func (line *Line) Rel(other CurrPath) RelPath {
	return G.Pkgsrc.Relpath(line.Filename().Dir(), other)
}

func (line *Line) IsMultiline() bool { return len(line.raw) > 1 }

// RawText returns the raw text from the given physical line,
// excluding \n, including any previous autofixes.
func (line *Line) RawText(rawIndex int) string {
	var textnl string
	if line.fix != nil {
		textnl = line.fix.texts[rawIndex]
	} else {
		textnl = line.raw[rawIndex].orignl
	}
	return strings.TrimSuffix(textnl, "\n")
}

func (line *Line) IsCvsID(prefixRe regex.Pattern) (found bool, expanded bool) {
	m, exp := match1(line.Text, `^`+prefixRe+`\$`+`NetBSD(:[^\$]+)?\$$`)
	return m, exp != ""
}

func (line *Line) Errorf(format string, args ...interface{}) {
	G.Logger.Diag(line, Error, format, args...)
}

func (line *Line) Warnf(format string, args ...interface{}) {
	G.Logger.Diag(line, Warn, format, args...)
}

func (line *Line) Notef(format string, args ...interface{}) {
	G.Logger.Diag(line, Note, format, args...)
}

func (*Line) Explain(explanation ...string) { G.Logger.Explain(explanation...) }

func (line *Line) String() string {
	return sprintf("%s:%s: %s", line.Filename(), line.Linenos(), line.Text)
}

// Autofix returns the autofix instance belonging to the line.
//
// Usage:
//
//  fix := line.Autofix()
//
//  fix.Errorf("Must not be ...")
//  fix.Warnf("Should not be ...")
//  fix.Notef("It is also possible ...")
//  fix.Silent()
//
//  fix.Explain(
//      "Explanation ...",
//      "... end of explanation.")
//
//  fix.Replace("from", "to")
//  fix.ReplaceAfter("prefix", "from", "to")
//  fix.InsertAbove("new line")
//  fix.InsertBelow("new line")
//  fix.Delete()
//  fix.Custom(func(showAutofix, autofix bool) {})
//
//  fix.Apply()
func (line *Line) Autofix() *Autofix {
	if line.fix == nil {
		line.fix = NewAutofix(line)
	} else {
		// This assertion fails if an Autofix is reused before
		// its Apply method is called.
		assert(line.fix.autofixShortTerm.diagFormat == "")
	}
	return line.fix
}
