// 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

import (
	"fmt"
	"slices"
	"strings"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/literal"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal"
)

func printNode(node interface{}, f *printer) error {
	s := newFormatter(f)

	ls := labelSimplifier{scope: map[string]bool{}}

	// format node
	f.allowed = nooverride // gobble initial whitespace.
	switch x := node.(type) {
	case *ast.File:
		if f.cfg.simplify {
			ls.markReferences(x)
		}
		s.file(x)
	case ast.Expr:
		if f.cfg.simplify {
			ls.markReferences(x)
		}
		s.expr(x)
	case ast.Decl:
		if f.cfg.simplify {
			ls.markReferences(x)
		}
		s.decl(x)
	// case ast.Node: // TODO: do we need this?
	// 	s.walk(x)
	case []ast.Decl:
		if f.cfg.simplify {
			ls.processDecls(x)
		}
		s.walkDeclList(x)
	default:
		goto unsupported
	}

	return s.errs

unsupported:
	return fmt.Errorf("cue/format: unsupported node type %T", node)
}

// Helper functions for common node lists. They may be empty.

func nestDepth(f *ast.Field) int {
	d := 1
	if s, ok := f.Value.(*ast.StructLit); ok {
		switch {
		case len(s.Elts) != 1:
			d = 0
		default:
			if f, ok := s.Elts[0].(*ast.Field); ok {
				d += nestDepth(f)
			}
		}
	}
	return d
}

// TODO: be more accurate and move to astutil
func hasDocComments(d ast.Decl) bool {
	if len(ast.Comments(d)) > 0 {
		return true
	}
	switch x := d.(type) {
	case *ast.Field:
		return len(ast.Comments(x.Label)) > 0
	case *ast.Alias:
		return len(ast.Comments(x.Ident)) > 0
	case *ast.LetClause:
		return len(ast.Comments(x.Ident)) > 0
	}
	return false
}

// hasNoSignificantComments checks if an import spec has no comments that
// would require formatting with parentheses. Trailing comments (Position > 1)
// that appear after the import statement don't require parentheses.
func hasNoSignificantComments(spec *ast.ImportSpec) bool {
	for _, cg := range ast.Comments(spec) {
		if cg.Position <= 1 {
			return false
		}
	}
	return true
}

func (f *formatter) walkDeclList(list []ast.Decl) {
	f.before(nil)
	d := 0
	var ellipsis ast.Decl
	for i, x := range list {
		if i > 0 {
			f.print(declcomma)
			nd := 0
			if f, ok := x.(*ast.Field); ok {
				nd = nestDepth(f)
			}
			if f.current.parentSep == newline && (d == 0 || nd != d) {
				f.print(f.formfeed())
			}
			if hasDocComments(x) {
				switch x := list[i-1].(type) {
				case *ast.Field:
					if internal.IsDefinition(x.Label) {
						f.print(newsection)
					}

				default:
					f.print(newsection)
				}
			}
		}
		if f.printer.cfg.simplify && isEllipsis(x) {
			ellipsis = x
			continue
		}
		f.decl(x)
		d = 0
		if f, ok := x.(*ast.Field); ok {
			d = nestDepth(f)
		}
		if j := i + 1; j < len(list) {
			switch x := list[j].(type) {
			case *ast.Field:
				switch x := x.Value.(type) {
				case *ast.StructLit:
					// TODO: not entirely correct: could have multiple elements,
					// not have a valid Lbrace, and be marked multiline. This
					// cannot occur for ASTs resulting from a parse, though.
					if x.Lbrace.IsValid() || len(x.Elts) != 1 {
						f.print(f.formfeed())
						continue
					}
				case *ast.ListLit:
					f.print(f.formfeed())
					continue
				}
			}
		}
		f.print(f.current.parentSep)
	}
	if ellipsis != nil {
		// ensure that comments associated with the original ellipsis are preserved
		n := &ast.Ellipsis{}
		ast.SetComments(n, ast.Comments(ellipsis))
		f.decl(n)
		f.print(f.current.parentSep)
	}
	f.after(nil)
}

// isEllipsis reports whether the declaration can be represented as an ellipsis.
func isEllipsis(x ast.Decl) bool {
	// ...
	if _, ok := x.(*ast.Ellipsis); ok {
		return true
	}

	// [string]: _ or [_]: _
	f, ok := x.(*ast.Field)
	if !ok {
		return false
	}
	v, ok := f.Value.(*ast.Ident)
	if !ok || v.Name != "_" {
		return false
	}
	l, ok := f.Label.(*ast.ListLit)
	if !ok || len(l.Elts) != 1 {
		return false
	}
	i, ok := l.Elts[0].(*ast.Ident)
	if !ok {
		return false
	}
	return i.Name == "string" || i.Name == "_"
}

func (f *formatter) walkSpecList(list []*ast.ImportSpec) {
	f.before(nil)
	for _, x := range list {
		f.before(x)
		f.importSpec(x)
		f.after(x)
	}
	f.after(nil)
}

func (f *formatter) walkClauseList(list []ast.Clause, ws whiteSpace) {
	f.before(nil)
	for i, x := range list {
		f.before(x)
		// Only print the whitespace between the clauses.
		if i > 0 {
			f.print(ws)
		}
		f.clause(x)
		f.after(x)
	}
	f.after(nil)
}

func fallbackKeyword(n *ast.Comprehension) token.Token {
	if len(n.Clauses) > 1 {
		return token.OTHERWISE
	} else if _, ok := n.Clauses[0].(*ast.ForClause); ok {
		return token.OTHERWISE
	}
	return token.ELSE
}

func (f *formatter) walkListElems(list []ast.Expr) {
	f.before(nil)
	for _, x := range list {
		f.before(x)

		// Collect comments that appear after the element's start position.
		// These need to be printed after the comma, not before it.
		var commentsAfter []*ast.CommentGroup
		splitComments := x.Pos().IsValid()
		if splitComments {
			for _, cg := range ast.Comments(x) {
				if x.Pos().Compare(cg.Pos()) < 0 {
					commentsAfter = append(commentsAfter, cg)
				}
			}
		}

		if splitComments {
			f.current.cg = nil
		}
		switch n := x.(type) {
		case *ast.Comprehension:
			f.walkClauseList(n.Clauses, blank)
			f.print(blank, nooverride)
			f.expr(n.Value)
			if n.Fallback != nil {
				// Use FALLBACK keyword for 'for' comprehensions, ELSE for 'if'/'try'
				kw := fallbackKeyword(n)
				f.print(blank, n.Fallback.Fallback, kw, blank)
				f.expr(n.Fallback.Body)
			}

		case *ast.Ellipsis:
			// For ellipsis, also collect trailing comments from the type
			// since they're attached to the nested node, not the ellipsis itself.
			f.print(n.Ellipsis, token.ELLIPSIS)
			if n.Type != nil && !isTop(n.Type) {
				for _, cg := range ast.Comments(n.Type) {
					if n.Type.Pos().Compare(cg.Pos()) < 0 {
						commentsAfter = append(commentsAfter, cg)
					}
				}
				f.exprRaw(n.Type, token.LowestPrec, 1)
			}

		case *ast.Alias:
			f.expr(n.Ident)
			f.print(n.Equal, token.BIND)
			f.expr(n.Expr)

			// TODO: ast.CommentGroup: allows comment groups in ListLits.

		case ast.Expr:
			f.exprRaw(n, token.LowestPrec, 1)
		}
		f.print(comma, blank)

		if splitComments {
			f.current.cg = commentsAfter
		}
		f.after(x)
	}
	f.after(nil)
}

func (f *formatter) walkArgsList(list []ast.Expr, depth int) {
	f.before(nil)
	for _, x := range list {
		f.before(x)
		f.exprRaw(x, token.LowestPrec, depth)
		f.print(comma, blank)
		f.after(x)
	}
	f.after(nil)
}

func (f *formatter) file(file *ast.File) {
	f.before(file)
	f.walkDeclList(file.Decls)
	f.after(file)
	f.print(token.EOF)
}

func (f *formatter) inlineField(n *ast.Field) *ast.Field {
	regular := internal.IsRegularField(n)
	// shortcut single-element structs.
	// If the label has a valid position, we assume that an unspecified
	// Lbrace signals the intent to collapse fields.
	if !n.Label.Pos().IsValid() && !(f.printer.cfg.simplify && regular) {
		return nil
	}

	obj, ok := n.Value.(*ast.StructLit)
	if !ok || len(obj.Elts) != 1 ||
		(obj.Lbrace.IsValid() && !f.printer.cfg.simplify) ||
		(obj.Lbrace.IsValid() && hasDocComments(n)) ||
		len(n.Attrs) > 0 {
		return nil
	}

	mem, ok := obj.Elts[0].(*ast.Field)
	if !ok || len(mem.Attrs) > 0 {
		return nil
	}

	if hasDocComments(mem) {
		// TODO: this inserts curly braces even in spaces where this
		// may not be desirable, such as:
		// a:
		//   // foo
		//   b: 3
		return nil
	}
	return mem
}

func (f *formatter) decl(decl ast.Decl) {
	if decl == nil {
		return
	}
	defer f.after(decl)
	if !f.before(decl) {
		return
	}

	switch n := decl.(type) {
	case *ast.Field:
		// Format label without constraint (we'll add constraint after alias)
		f.label(n.Label, token.ILLEGAL)

		// Format postfix alias if present
		if a := n.Alias; a != nil {
			f.print(a.Tilde, token.TILDE, noblank)
			if a.Label != nil {
				// Dual form: ~(K,V)
				// Assumes that ILLEGAL tokens are no-ops.
				f.print(a.Lparen, token.LPAREN, noblank)
				f.expr(a.Label)
				f.print(a.Comma, token.COMMA, noblank)
				f.expr(a.Field)
				f.print(a.Rparen, token.RPAREN, noblank)
			} else {
				// Simple form: ~X
				f.expr(a.Field)
			}
		}

		// Format constraint marker (?, !) if present
		if n.Constraint != token.ILLEGAL {
			f.print(n.Constraint)
		}

		f.print(noblank, nooverride, n.TokenPos, token.COLON)
		f.visitComments(f.current.pos)

		if mem := f.inlineField(n); mem != nil {
			switch {
			default:
				fallthrough

			case f.cfg.simplify:
				f.print(blank, nooverride)
				f.decl(mem)

			case mem.Label.Pos().IsNewline():
				f.print(indent, formfeed)
				f.decl(mem)
				f.indent--
			}
			return
		}

		nextFF := f.nextNeedsFormfeed(n.Value)
		tab := vtab
		if nextFF || f.prevLbraceOnLine {
			tab = blank
		}

		f.print(tab)

		if n.Value != nil {
			switch n.Value.(type) {
			case *ast.ListLit, *ast.StructLit:
				f.expr(n.Value)
			default:
				f.print(indent)
				f.expr(n.Value)
				f.markUnindentLine()
			}
		} else {
			f.current.pos++
			f.visitComments(f.current.pos)
		}

		space := tab
		for _, a := range n.Attrs {
			if f.before(a) {
				f.print(space, a.At, a)
			}
			f.after(a)
			space = blank
		}

		if nextFF {
			f.print(formfeed)
		}

	case *ast.BadDecl:
		f.print(n.From, "*bad decl*", declcomma)

	case *ast.Package:
		f.print(n.PackagePos, "package")
		f.print(blank, n.Name, newsection, nooverride)

	case *ast.ImportDecl:
		f.print(n.Import, "import")
		if len(n.Specs) == 0 {
			f.print(blank, n.Lparen, token.LPAREN, n.Rparen, token.RPAREN, newline)
			break
		}
		switch {
		case len(n.Specs) == 1 && hasNoSignificantComments(n.Specs[0]):
			if !n.Lparen.IsValid() {
				f.print(blank)
				f.walkSpecList(n.Specs)
				break
			}
			fallthrough
		default:
			f.print(blank, n.Lparen, token.LPAREN, newline, indent)
			f.walkSpecList(n.Specs)
			f.print(unindent, newline, n.Rparen, token.RPAREN, newline)
		}
		f.print(newsection, nooverride)

	case *ast.LetClause:
		if !decl.Pos().HasRelPos() || decl.Pos().RelPos() >= token.Newline {
			f.print(formfeed)
		}
		f.print(n.Let, token.LET, blank, nooverride)
		f.expr(n.Ident)
		f.print(blank, nooverride, n.Equal, token.BIND, blank)
		f.expr(n.Expr)
		f.print(declcomma) // implied

	case *ast.EmbedDecl:
		if !n.Pos().HasRelPos() || n.Pos().RelPos() >= token.Newline {
			f.print(formfeed)
		}
		f.expr(n.Expr)
		f.print(newline)

	case *ast.Attribute:
		f.print(n.At, n)

	case *ast.CommentGroup:
		f.printComment(n)
		f.print(newsection)

	case ast.Expr:
		f.embedding(n)
	}
}

func (f *formatter) embedding(decl ast.Expr) {
	switch n := decl.(type) {
	case *ast.Comprehension:
		if !n.Pos().HasRelPos() || n.Pos().RelPos() >= token.Newline {
			f.print(formfeed)
		}
		f.walkClauseList(n.Clauses, blank)
		f.print(blank, nooverride)
		f.expr(n.Value)
		if n.Fallback != nil {
			// Use FALLBACK keyword for 'for' comprehensions, ELSE for 'if'/'try'
			kw := fallbackKeyword(n)
			f.print(blank, n.Fallback.Fallback, kw, blank)
			f.expr(n.Fallback.Body)
		}

	case *ast.Ellipsis:
		f.ellipsis(n)

	case *ast.Alias:
		if !decl.Pos().HasRelPos() || decl.Pos().RelPos() >= token.Newline {
			f.print(formfeed)
		}
		f.expr(n.Ident)
		f.print(blank, n.Equal, token.BIND, blank)
		f.expr(n.Expr)
		f.print(declcomma) // implied

		// TODO: ast.CommentGroup: allows comment groups in ListLits.

	case ast.Expr:
		f.exprRaw(n, token.LowestPrec, 1)
	}
}

func (f *formatter) nextNeedsFormfeed(n ast.Expr) bool {
	switch x := n.(type) {
	case *ast.StructLit:
		return true
	case *ast.BasicLit:
		return strings.IndexByte(x.Value, '\n') >= 0
	case *ast.ListLit:
		return true
	case *ast.ParenExpr:
		return f.nextNeedsFormfeed(x.X)
	case *ast.UnaryExpr:
		return f.nextNeedsFormfeed(x.X)
	case *ast.PostfixExpr:
		return f.nextNeedsFormfeed(x.X)
	case *ast.BinaryExpr:
		return f.nextNeedsFormfeed(x.X) || f.nextNeedsFormfeed(x.Y)
	case *ast.IndexExpr:
		return f.nextNeedsFormfeed(x.X)
	case *ast.SelectorExpr:
		return f.nextNeedsFormfeed(x.X)
	case *ast.CallExpr:
		if slices.ContainsFunc(x.Args, f.nextNeedsFormfeed) {
			return true
		}
	}
	return false
}

func (f *formatter) importSpec(x *ast.ImportSpec) {
	if x.Name != nil {
		f.label(x.Name, token.ILLEGAL)
		f.print(blank)
	} else {
		f.current.pos++
		f.visitComments(f.current.pos)
	}
	f.expr(x.Path)
	f.print(newline)
}

func (f *formatter) label(l ast.Label, constraint token.Token) {
	f.before(l)
	defer f.after(l)
	switch n := l.(type) {
	case *ast.Alias:
		f.expr(n)

	case *ast.Ident:
		// Escape an identifier that has invalid characters. This may happen,
		// if the AST is not generated by the parser.
		name := n.Name
		if !ast.IsValidIdent(name) {
			name = literal.Label.Quote(name)
		}
		f.print(n.NamePos, name)

	case *ast.BasicLit:
		str := n.Value
		// Allow any CUE string in the AST, but ensure it is formatted
		// according to spec.
		if strings.HasPrefix(str, `"""`) || strings.HasPrefix(str, "#") {
			if u, err := literal.Unquote(str); err == nil {
				str = literal.Label.Quote(u)
			}
		}
		f.print(n.ValuePos, str)

	case *ast.ListLit:
		f.expr(n)

	case *ast.ParenExpr:
		f.expr(n)

	case *ast.Interpolation:
		f.expr(n)

	default:
		panic(fmt.Sprintf("unknown label type %T", n))
	}
	if constraint != token.ILLEGAL {
		f.print(constraint)
	}
}

func (f *formatter) ellipsis(x *ast.Ellipsis) {
	f.print(x.Ellipsis, token.ELLIPSIS)
	if x.Type != nil && !isTop(x.Type) {
		f.expr(x.Type)
	}
}

func (f *formatter) expr(x ast.Expr) {
	const depth = 1
	f.expr1(x, token.LowestPrec, depth)
}

func (f *formatter) expr0(x ast.Expr, depth int) {
	f.expr1(x, token.LowestPrec, depth)
}

func (f *formatter) expr1(expr ast.Expr, prec1, depth int) {
	if f.before(expr) {
		f.exprRaw(expr, prec1, depth)
	}
	f.after(expr)
}

func (f *formatter) exprRaw(expr ast.Expr, prec1, depth int) {
	switch x := expr.(type) {
	case *ast.BadExpr:
		f.print(x.From, "_|_")

	case *ast.BottomLit:
		f.print(x.Bottom, token.BOTTOM)

	case *ast.Alias:
		// Aliases in expression positions are printed in short form.
		f.label(x.Ident, token.ILLEGAL)
		f.print(x.Equal, token.BIND)
		f.expr(x.Expr)

	case *ast.Ident:
		f.print(x.NamePos, x)

	case *ast.BinaryExpr:
		if depth < 1 {
			f.internalError("depth < 1:", depth)
			depth = 1
		}
		f.binaryExpr(x, prec1, cutoff(x, depth), depth)

	case *ast.UnaryExpr:
		const prec = token.UnaryPrec
		if prec < prec1 {
			// parenthesis needed
			f.print(token.LPAREN, nooverride)
			f.expr(x)
			f.print(token.RPAREN)
		} else {
			// no parenthesis needed
			f.print(x.OpPos, x.Op, nooverride)
			f.expr1(x.X, prec, depth)
		}

	case *ast.PostfixExpr:
		f.expr1(x.X, token.HighestPrec, depth)
		f.print(x.Op)

	case *ast.BasicLit:
		f.print(x.ValuePos, x)

	case *ast.Interpolation:
		f.before(nil)
		for _, x := range x.Elts {
			f.expr0(x, depth+1)
		}
		f.after(nil)

	case *ast.ParenExpr:
		if _, hasParens := x.X.(*ast.ParenExpr); hasParens {
			// don't print parentheses around an already parenthesized expression
			// TODO: consider making this more general and incorporate precedence levels
			f.expr0(x.X, depth)
		} else {
			f.print(x.Lparen, token.LPAREN)
			f.expr0(x.X, reduceDepth(depth)) // parentheses undo one level of depth
			f.print(x.Rparen, token.RPAREN)
		}

	case *ast.SelectorExpr:
		f.selectorExpr(x, depth)

	case *ast.IndexExpr:
		f.expr1(x.X, token.HighestPrec, 1)
		f.print(x.Lbrack, token.LBRACK)
		f.expr0(x.Index, depth+1)
		f.print(x.Rbrack, token.RBRACK)

	case *ast.SliceExpr:
		f.expr1(x.X, token.HighestPrec, 1)
		f.print(x.Lbrack, token.LBRACK)
		indices := []ast.Expr{x.Low, x.High}
		for i, y := range indices {
			if i > 0 {
				// blanks around ":" if both sides exist and either side is a binary expression
				x := indices[i-1]
				if depth <= 1 && x != nil && y != nil && (isBinary(x) || isBinary(y)) {
					f.print(blank, token.COLON, blank)
				} else {
					f.print(token.COLON)
				}
			}
			if y != nil {
				f.expr0(y, depth+1)
			}
		}
		f.print(x.Rbrack, token.RBRACK)

	case *ast.CallExpr:
		if len(x.Args) > 1 {
			depth++
		}
		wasIndented := f.possibleSelectorExpr(x.Fun, token.HighestPrec, depth)
		f.print(x.Lparen, token.LPAREN)
		f.walkArgsList(x.Args, depth)
		f.print(trailcomma, noblank, x.Rparen, token.RPAREN)
		if wasIndented {
			f.print(unindent)
		}

	case *ast.StructLit:
		var l line
		ws := noblank
		ff := f.formfeed()

		switch {
		case len(x.Elts) == 0:
			// collapse curly braces if the body is empty.
			ffAlt := blank | nooverride
			for _, c := range ast.Comments(x) {
				if c.Position == 1 {
					ffAlt = ff
					break
				}
			}
			ff = ffAlt
		case !x.Rbrace.HasRelPos() || !x.Elts[0].Pos().HasRelPos():
			ws |= newline | nooverride
		}
		f.print(x.Lbrace, token.LBRACE, &l, ws, ff, indent)
		f.prevLbraceOnLine = l == f.lineout

		f.walkDeclList(x.Elts)
		f.matchUnindent()

		ws = noblank
		if f.lineout != l {
			ws |= newline
			if f.lastTok != token.RBRACE && f.lastTok != token.RBRACK {
				ws |= nooverride
			}
		}
		f.print(ws, x.Rbrace, token.RBRACE)

	case *ast.ListLit:
		ws := noblank | indent
		if len(x.Elts) == 0 {
			// collapse square brackets if the body is empty.
			collapseWs := blank | nooverride
			for _, c := range ast.Comments(x) {
				if c.Position == 1 {
					collapseWs = ws
					break
				}
			}
			ws |= collapseWs
		}

		f.print(x.Lbrack, token.LBRACK, ws)
		f.walkListElems(x.Elts)
		f.print(trailcomma, noblank)
		f.visitComments(f.current.pos)
		f.matchUnindent()
		f.print(noblank, x.Rbrack, token.RBRACK)

	case *ast.Ellipsis:
		f.ellipsis(x)

	default:
		panic(fmt.Sprintf("unimplemented type %T", x))
	}
}

func (f *formatter) clause(clause ast.Clause) {
	switch n := clause.(type) {
	case *ast.ForClause:
		f.print(n.For, "for", blank)
		f.print(indent)
		if n.Key != nil {
			f.label(n.Key, token.ILLEGAL)
			f.print(n.Colon, token.COMMA, blank)
		} else {
			f.current.pos++
			f.visitComments(f.current.pos)
		}
		f.label(n.Value, token.ILLEGAL)
		f.print(blank, n.In, "in", blank)
		f.expr(n.Source)
		f.markUnindentLine()

	case *ast.IfClause:
		f.print(n.If, "if", blank)
		f.print(indent)
		f.expr(n.Condition)
		f.markUnindentLine()

	case *ast.LetClause:
		// TODO(mvdan): LetClause is handled in both the clause and decl methods,
		// because at the semantic level it is different in each case, but the code is repetitive.
		f.print(n.Let, token.LET, blank, nooverride)
		f.print(indent)
		f.expr(n.Ident)
		f.print(blank, nooverride, n.Equal, token.BIND, blank)
		f.expr(n.Expr)
		f.markUnindentLine()

	case *ast.TryClause:
		f.print(n.Try, token.TRY)
		if n.Ident != nil {
			// Assignment form: try x = expr
			f.print(blank, nooverride, indent)
			f.expr(n.Ident)
			f.print(blank, nooverride, n.Equal, token.BIND, blank)
			f.expr(n.Expr)
			f.markUnindentLine()
		}
		// Struct form: just "try" - body comes from Comprehension.Value

	default:
		panic("unknown clause type")
	}
}

func walkBinary(e *ast.BinaryExpr) (has6, has7, has8 bool, maxProblem int) {
	switch e.Op.Precedence() {
	case 6:
		has6 = true
	case 7:
		has7 = true
	case 8:
		has8 = true
	}

	switch l := e.X.(type) {
	case *ast.BinaryExpr:
		if l.Op.Precedence() < e.Op.Precedence() {
			// parens will be inserted.
			// pretend this is an *ast.ParenExpr and do nothing.
			break
		}
		h6, h7, h8, mp := walkBinary(l)
		has6 = has6 || h6
		has7 = has7 || h7
		has8 = has8 || h8
		if maxProblem < mp {
			maxProblem = mp
		}
	}

	switch r := e.Y.(type) {
	case *ast.BinaryExpr:
		if r.Op.Precedence() <= e.Op.Precedence() {
			// parens will be inserted.
			// pretend this is an *ast.ParenExpr and do nothing.
			break
		}
		h6, h7, h8, mp := walkBinary(r)
		has6 = has6 || h6
		has7 = has7 || h7
		has8 = has8 || h8
		if maxProblem < mp {
			maxProblem = mp
		}

	case *ast.UnaryExpr:
		switch e.Op.String() + r.Op.String() {
		case "/*":
			maxProblem = 8
		case "++", "--":
			if maxProblem < 6 {
				maxProblem = 6
			}
		}
	}
	return
}

func cutoff(e *ast.BinaryExpr, depth int) int {
	has6, has7, has8, maxProblem := walkBinary(e)
	if maxProblem > 0 {
		return maxProblem + 1
	}
	if (has6 || has7) && has8 {
		if depth == 1 {
			return 8
		}
		if has7 {
			return 7
		}
		return 6
	}
	if has6 && has7 {
		if depth == 1 {
			return 7
		}
		return 6
	}
	if depth == 1 {
		return 8
	}
	return 6
}

func diffPrec(expr ast.Expr, prec int) int {
	x, ok := expr.(*ast.BinaryExpr)
	if !ok || prec != x.Op.Precedence() {
		return 1
	}
	return 0
}

func reduceDepth(depth int) int {
	depth--
	if depth < 1 {
		depth = 1
	}
	return depth
}

// Format the binary expression: decide the cutoff and then format.
// Let's call depth == 1 Normal mode, and depth > 1 Compact mode.
// (Algorithm suggestion by Russ Cox.)
//
// The precedences are:
//
//	7             *  /  % quo rem div mod
//	6             +  -
//	5             ==  !=  <  <=  >  >=
//	4             &&
//	3             ||
//	2             &
//	1             |
//
// The only decision is whether there will be spaces around levels 6 and 7.
// There are never spaces at level 8 (unary), and always spaces at levels 5 and below.
//
// To choose the cutoff, look at the whole expression but excluding primary
// expressions (function calls, parenthesized exprs), and apply these rules:
//
//  1. If there is a binary operator with a right side unary operand
//     that would clash without a space, the cutoff must be (in order):
//
//     /*	8
//     ++	7 // not necessary, but to avoid confusion
//     --	7
//
//     (Comparison operators always have spaces around them.)
//
//  2. If there is a mix of level 7 and level 6 operators, then the cutoff
//     is 7 (use spaces to distinguish precedence) in Normal mode
//     and 6 (never use spaces) in Compact mode.
//
//  3. If there are no level 6 operators or no level 7 operators, then the
//     cutoff is 8 (always use spaces) in Normal mode
//     and 6 (never use spaces) in Compact mode.
func (f *formatter) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int) {
	f.nestExpr++
	defer func() { f.nestExpr-- }()

	prec := x.Op.Precedence()
	if prec < prec1 {
		// parenthesis needed
		// Note: The parser inserts a ast.ParenExpr node; thus this case
		//       can only occur if the AST is created in a different way.
		// defer p.pushComment(nil).pop()
		f.print(token.LPAREN, nooverride)
		f.expr0(x, reduceDepth(depth)) // parentheses undo one level of depth
		f.print(token.RPAREN)
		return
	}

	printBlank := prec < cutoff

	f.expr1(x.X, prec, depth+diffPrec(x.X, prec))
	f.print(nooverride)
	if printBlank {
		f.print(blank)
	}
	f.print(x.OpPos, x.Op)
	if x.Y.Pos().IsNewline() {
		// at least one line break, but respect an extra empty line
		// in the source
		f.print(formfeed)
		printBlank = false // no blank after line break
	} else {
		f.print(nooverride)
	}
	if printBlank {
		f.print(blank)
	}
	f.expr1(x.Y, prec+1, depth+1)
}

func isBinary(expr ast.Expr) bool {
	_, ok := expr.(*ast.BinaryExpr)
	return ok
}

func (f *formatter) possibleSelectorExpr(expr ast.Expr, prec1, depth int) bool {
	if x, ok := expr.(*ast.SelectorExpr); ok {
		return f.selectorExpr(x, depth)
	}
	f.expr1(expr, prec1, depth)
	return false
}

// selectorExpr handles an [*ast.SelectorExpr] node and returns whether x spans
// multiple lines.
func (f *formatter) selectorExpr(x *ast.SelectorExpr, depth int) bool {
	f.expr1(x.X, token.HighestPrec, depth)
	f.print(token.PERIOD)
	if x.Sel.Pos().IsNewline() {
		f.print(indent, formfeed)
		f.expr(x.Sel.(ast.Expr))
		f.print(unindent)
		return true
	}
	f.print(noblank)
	f.expr(x.Sel.(ast.Expr))
	return false
}

func isTop(e ast.Expr) bool {
	ident, ok := e.(*ast.Ident)
	return ok && ident.Name == "_"
}
