// Copyright 2018 The CUE Authors
//
// 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 format implements standard formatting of CUE configurations.
package format

// TODO: this package is in need of a rewrite. When doing so, the API should
// allow for reformatting an AST, without actually writing bytes.
//
// In essence, formatting determines the relative spacing to tokens. It should
// be possible to have an abstract implementation providing such information
// that can be used to either format or update an AST in a single walk.

import (
	"bytes"
	"fmt"
	"text/tabwriter"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/parser"
	"cuelang.org/go/cue/token"
)

// An Option sets behavior of the formatter.
type Option func(c *config)

// Simplify allows the formatter to simplify output, such as removing
// unnecessary quotes.
func Simplify() Option {
	return func(c *config) { c.simplify = true }
}

// UseSpaces specifies that tabs should be converted to spaces and sets the
// default tab width.
func UseSpaces(tabwidth int) Option {
	return func(c *config) {
		c.UseSpaces = true
		c.Tabwidth = tabwidth
	}
}

// TabIndent specifies whether to use tabs for indentation independent of
// UseSpaces.
func TabIndent(indent bool) Option {
	return func(c *config) { c.TabIndent = indent }
}

// IndentPrefix specifies the number of tabstops to use as a prefix for every
// line.
func IndentPrefix(n int) Option {
	return func(c *config) { c.Indent = n }
}

// TODO: make public
// sortImportsOption causes import declarations to be sorted.
func sortImportsOption() Option {
	return func(c *config) { c.sortImports = true }
}

// TODO: other options:
//
// const (
// 	RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored
// 	TabIndent                  // use tabs for indentation independent of UseSpaces
// 	UseSpaces                  // use spaces instead of tabs for alignment
// 	SourcePos                  // emit //line comments to preserve original source positions
// )

// Node formats node in canonical cue fmt style and writes the result to dst.
//
// The node type must be [*ast.File], [][ast.Decl], [ast.Expr], [ast.Decl], or
// [ast.Spec]. Node does not modify node. Imports are not sorted for nodes
// representing partial source files (for instance, if the node is not an
// *ast.File).
//
// The function may return early (before the entire result is written) and
// return a formatting error, for instance due to an incorrect AST.
func Node(node ast.Node, opt ...Option) ([]byte, error) {
	cfg := newConfig(opt)
	return cfg.fprint(node)
}

// Source formats src in canonical cue fmt style and returns the result or an
// (I/O or syntax) error. src is expected to be a syntactically correct CUE
// source file, or a list of CUE declarations or statements.
//
// If src is a partial source file, the leading and trailing space of src is
// applied to the result (such that it has the same leading and trailing space
// as src), and the result is indented by the same amount as the first line of
// src containing code. Imports are not sorted for partial source files.
//
// Caution: Tools relying on consistent formatting based on the installed
// version of cue (for instance, such as for presubmit checks) should execute
// that cue binary instead of calling Source.
func Source(b []byte, opt ...Option) ([]byte, error) {
	cfg := newConfig(opt)

	f, err := parser.ParseFile("", b, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("parse: %s", err)
	}

	// print AST
	return cfg.fprint(f)
}

type config struct {
	UseSpaces bool
	TabIndent bool
	Tabwidth  int // default: 4
	Indent    int // default: 0 (all code is indented at least by this much)

	simplify    bool
	sortImports bool
}

func newConfig(opt []Option) *config {
	cfg := &config{
		Tabwidth:  8,
		TabIndent: true,
		UseSpaces: true,
	}
	for _, o := range opt {
		o(cfg)
	}
	return cfg
}

// Config defines the output of Fprint.
func (cfg *config) fprint(node interface{}) (out []byte, err error) {
	var p printer
	p.init(cfg)
	if err = printNode(node, &p); err != nil {
		return p.output, err
	}

	padchar := byte('\t')
	if cfg.UseSpaces {
		padchar = byte(' ')
	}

	twmode := tabwriter.StripEscape | tabwriter.TabIndent | tabwriter.DiscardEmptyColumns
	if cfg.TabIndent {
		twmode |= tabwriter.TabIndent
	}

	buf := &bytes.Buffer{}
	tw := tabwriter.NewWriter(buf, 0, cfg.Tabwidth, 1, padchar, twmode)

	// write printer result via tabwriter/trimmer to output
	if _, err = tw.Write(p.output); err != nil {
		return
	}

	err = tw.Flush()
	if err != nil {
		return buf.Bytes(), err
	}

	b := buf.Bytes()
	if !cfg.TabIndent {
		b = bytes.ReplaceAll(b, []byte{'\t'}, bytes.Repeat([]byte{' '}, cfg.Tabwidth))
	}
	return b, nil
}

// A formatter walks an [ast.Node], interspersed with comments and spacing
// directives, in the order that they would occur in printed form.
type formatter struct {
	*printer

	stack    []frame
	current  frame
	nestExpr int
}

func newFormatter(p *printer) *formatter {
	f := &formatter{
		printer: p,
		current: frame{
			settings: settings{
				nodeSep:   newline,
				parentSep: newline,
			},
		},
	}
	return f
}

type whiteSpace int

const (
	_ whiteSpace = 0

	// write a space, or disallow it
	blank whiteSpace = 1 << iota
	vtab             // column marker
	noblank

	nooverride

	comma      // print a comma, unless trailcomma overrides it
	trailcomma // print a trailing comma unless closed on same line
	declcomma  // write a comma when not at the end of line

	newline    // write a line in a table
	formfeed   // next line is not part of the table
	newsection // add two newlines

	indent   // request indent an extra level after the next newline
	unindent // unindent a level after the next newline
	indented // element was indented.
)

type frame struct {
	cg  []*ast.CommentGroup
	pos int8

	settings
}

type settings struct {
	// separator is blank if the current node spans a single line and newline
	// otherwise.
	nodeSep   whiteSpace
	parentSep whiteSpace
	override  whiteSpace
}

// suppress spurious linter warning: field is actually used.
func init() {
	s := settings{}
	_ = s.override
}

func (f *formatter) print(a ...interface{}) {
	for _, x := range a {
		f.Print(x)
		switch x.(type) {
		case string, token.Token: // , *ast.BasicLit, *ast.Ident:
			f.current.pos++
		}
	}
}

func (f *formatter) formfeed() whiteSpace {
	if f.current.nodeSep == blank {
		return blank
	}
	return formfeed
}

func (f *formatter) onOneLine(node ast.Node) bool {
	a := node.Pos()
	b := node.End()
	if a.IsValid() && b.IsValid() {
		return f.lineFor(a) == f.lineFor(b)
	}
	// TODO: walk and look at relative positions to determine the same?
	return false
}

func (f *formatter) before(node ast.Node) bool {
	f.stack = append(f.stack, f.current)
	f.current = frame{settings: f.current.settings}
	f.current.parentSep = f.current.nodeSep

	if node != nil {
		s, ok := node.(*ast.StructLit)
		if ok && len(s.Elts) <= 1 && f.current.nodeSep != blank && f.onOneLine(node) {
			f.current.nodeSep = blank
		}
		f.current.cg = ast.Comments(node)
		f.visitComments(f.current.pos)
		return true
	}
	return false
}

func (f *formatter) after(node ast.Node) {
	f.visitComments(127)
	p := len(f.stack) - 1
	f.current = f.stack[p]
	f.stack = f.stack[:p]
	f.current.pos++
	f.visitComments(f.current.pos)
}

func (f *formatter) visitComments(until int8) {
	c := &f.current

	printed := false
	for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
		if printed {
			f.Print(newsection)
		}
		printed = true
		f.printComment(c.cg[0])
	}
}

func (f *formatter) printComment(cg *ast.CommentGroup) {
	f.Print(cg)

	if cg.Doc && len(f.output) > 0 {
		f.Print(newline)
	}
	for _, c := range cg.List {
		if f.pos.Column > 1 {
			// Vertically align inline comments.
			f.Print(vtab)
		}
		f.Print(c.Slash)
		f.Print(c)
		f.printingComment = true
		f.Print(newline)
		if cg.Doc {
			f.Print(nooverride)
		}
	}
}
