/*
Copyright 2019 Google Inc. All rights reserved.

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 formatter

import (
	"bytes"
	"fmt"

	"github.com/google/go-jsonnet/ast"
)

type unparser struct {
	buf     bytes.Buffer
	options Options
}

func (u *unparser) write(str string) {
	u.buf.WriteString(str)
}

// fill Pretty-prints fodder.
// The crowded and separateToken params control whether single whitespace
// characters are added to keep tokens from joining together in the output.
// The intuition of crowded is that the caller passes true for crowded if the
// last thing printed would crowd whatever we're printing here.  For example, if
// we just printed a ',' then crowded would be true.  If we just printed a '('
// then crowded would be false because we don't want the space after the '('.
//
// If crowded is true, a space is printed after any fodder, unless
// separateToken is false or the fodder ended with a newline.
// If crowded is true and separateToken is false and the fodder begins with
// an interstitial, then the interstitial is prefixed with a single space, but
// there is no space after the interstitial.
// If crowded is false and separateToken is true then a space character
// is only printed when the fodder ended with an interstitial comment (which
// creates a crowded situation where there was not one before).
// If crowded is false and separateToken is false then no space is printed
// after or before the fodder, even if the last fodder was an interstitial.
func (u *unparser) fodderFill(fodder ast.Fodder, crowded bool, separateToken bool, final bool) {
	var lastIndent int
	for i, fod := range fodder {
		skipTrailing := final && (i == (len(fodder) - 1))
		switch fod.Kind {
		case ast.FodderParagraph:
			for i, l := range fod.Comment {
				// Do not indent empty lines (note: first line is never empty).
				if len(l) > 0 {
					// First line is already indented by previous fod.
					if i > 0 {
						for i := 0; i < lastIndent; i++ {
							u.write(" ")
						}
					}
					u.write(l)
				}
				u.write("\n")
			}
			if !skipTrailing {
				for i := 0; i < fod.Blanks; i++ {
					u.write("\n")
				}
				for i := 0; i < fod.Indent; i++ {
					u.write(" ")
				}
			}
			lastIndent = fod.Indent
			crowded = false

		case ast.FodderLineEnd:
			if len(fod.Comment) > 0 {
				u.write("  ")
				u.write(fod.Comment[0])
			}
			u.write("\n")
			if !skipTrailing {
				for i := 0; i < fod.Blanks; i++ {
					u.write("\n")
				}
				for i := 0; i < fod.Indent; i++ {
					u.write(" ")
				}
			}
			lastIndent = fod.Indent
			crowded = false

		case ast.FodderInterstitial:
			if crowded {
				u.write(" ")
			}
			u.write(fod.Comment[0])
			crowded = true
		}
	}
	if separateToken && crowded {
		u.write(" ")
	}
}

func (u *unparser) fill(fodder ast.Fodder, crowded bool, separateToken bool) {
	u.fodderFill(fodder, crowded, separateToken, false)
}

func (u *unparser) fillFinal(fodder ast.Fodder, crowded bool, separateToken bool) {
	u.fodderFill(fodder, crowded, separateToken, true)
}

func (u *unparser) unparseSpecs(spec *ast.ForSpec) {
	if spec.Outer != nil {
		u.unparseSpecs(spec.Outer)
	}
	u.fill(spec.ForFodder, true, true)
	u.write("for")
	u.fill(spec.VarFodder, true, true)
	u.write(string(spec.VarName))
	u.fill(spec.InFodder, true, true)
	u.write("in")
	u.unparse(spec.Expr, true)
	for _, cond := range spec.Conditions {
		u.fill(cond.IfFodder, true, true)
		u.write("if")
		u.unparse(cond.Expr, true)
	}
}

func (u *unparser) unparseParams(fodderL ast.Fodder, params []ast.Parameter, trailingComma bool, fodderR ast.Fodder) {
	u.fill(fodderL, false, false)
	u.write("(")
	first := true
	for _, param := range params {
		if !first {
			u.write(",")
		}
		u.fill(param.NameFodder, !first, true)
		u.unparseID(param.Name)
		if param.DefaultArg != nil {
			u.fill(param.EqFodder, false, false)
			u.write("=")
			u.unparse(param.DefaultArg, false)
		}
		u.fill(param.CommaFodder, false, false)
		first = false
	}
	if trailingComma {
		u.write(",")
	}
	u.fill(fodderR, false, false)
	u.write(")")
}

func (u *unparser) unparseFieldParams(field ast.ObjectField) {
	m := field.Method
	if m != nil {
		u.unparseParams(m.ParenLeftFodder, m.Parameters, m.TrailingComma,
			m.ParenRightFodder)
	}
}

func (u *unparser) unparseFields(fields ast.ObjectFields, crowded bool) {
	first := true
	for _, field := range fields {
		if !first {
			u.write(",")
		}

		// An aux function so we don't repeat ourselves for the 3 kinds of
		// basic field.
		unparseFieldRemainder := func(field ast.ObjectField) {
			u.unparseFieldParams(field)
			u.fill(field.OpFodder, false, false)
			if field.SuperSugar {
				u.write("+")
			}
			switch field.Hide {
			case ast.ObjectFieldInherit:
				u.write(":")
			case ast.ObjectFieldHidden:
				u.write("::")
			case ast.ObjectFieldVisible:
				u.write(":::")
			}
			u.unparse(field.Expr2, true)
		}

		switch field.Kind {
		case ast.ObjectLocal:
			u.fill(field.Fodder1, !first || crowded, true)
			u.write("local")
			u.fill(field.Fodder2, true, true)
			u.unparseID(*field.Id)
			u.unparseFieldParams(field)
			u.fill(field.OpFodder, true, true)
			u.write("=")
			u.unparse(field.Expr2, true)

		case ast.ObjectFieldID:
			u.fill(field.Fodder1, !first || crowded, true)
			u.unparseID(*field.Id)
			unparseFieldRemainder(field)

		case ast.ObjectFieldStr:
			u.unparse(field.Expr1, !first || crowded)
			unparseFieldRemainder(field)

		case ast.ObjectFieldExpr:
			u.fill(field.Fodder1, !first || crowded, true)
			u.write("[")
			u.unparse(field.Expr1, false)
			u.fill(field.Fodder2, false, false)
			u.write("]")
			unparseFieldRemainder(field)

		case ast.ObjectAssert:
			u.fill(field.Fodder1, !first || crowded, true)
			u.write("assert")
			u.unparse(field.Expr2, true)
			if field.Expr3 != nil {
				u.fill(field.OpFodder, true, true)
				u.write(":")
				u.unparse(field.Expr3, true)
			}
		}

		first = false
		u.fill(field.CommaFodder, false, false)
	}

}

func (u *unparser) unparseID(id ast.Identifier) {
	u.write(string(id))
}

func (u *unparser) unparse(expr ast.Node, crowded bool) {

	if leftRecursive(expr) == nil {
		u.fill(*expr.OpenFodder(), crowded, true)
	}

	switch node := expr.(type) {
	case *ast.Apply:
		u.unparse(node.Target, crowded)
		u.fill(node.FodderLeft, false, false)
		u.write("(")
		first := true
		for _, arg := range node.Arguments.Positional {
			if !first {
				u.write(",")
			}
			space := !first
			u.unparse(arg.Expr, space)
			u.fill(arg.CommaFodder, false, false)
			first = false
		}
		for _, arg := range node.Arguments.Named {
			if !first {
				u.write(",")
			}
			space := !first
			u.fill(arg.NameFodder, space, true)
			u.unparseID(arg.Name)
			space = false
			u.write("=")
			u.unparse(arg.Arg, space)
			u.fill(arg.CommaFodder, false, false)
			first = false
		}
		if node.TrailingComma {
			u.write(",")
		}
		u.fill(node.FodderRight, false, false)
		u.write(")")
		if node.TailStrict {
			u.fill(node.TailStrictFodder, true, true)
			u.write("tailstrict")
		}

	case *ast.ApplyBrace:
		u.unparse(node.Left, crowded)
		u.unparse(node.Right, true)

	case *ast.Array:
		u.write("[")
		first := true
		for _, element := range node.Elements {
			if !first {
				u.write(",")
			}
			u.unparse(element.Expr, !first || u.options.PadArrays)
			u.fill(element.CommaFodder, false, false)
			first = false
		}
		if node.TrailingComma {
			u.write(",")
		}
		u.fill(node.CloseFodder, len(node.Elements) > 0, u.options.PadArrays)
		u.write("]")

	case *ast.ArrayComp:
		u.write("[")
		u.unparse(node.Body, u.options.PadArrays)
		u.fill(node.TrailingCommaFodder, false, false)
		if node.TrailingComma {
			u.write(",")
		}
		u.unparseSpecs(&node.Spec)
		u.fill(node.CloseFodder, true, u.options.PadArrays)
		u.write("]")

	case *ast.Assert:
		u.write("assert")
		u.unparse(node.Cond, true)
		if node.Message != nil {
			u.fill(node.ColonFodder, true, true)
			u.write(":")
			u.unparse(node.Message, true)
		}
		u.fill(node.SemicolonFodder, false, false)
		u.write(";")
		u.unparse(node.Rest, true)

	case *ast.Binary:
		u.unparse(node.Left, crowded)
		u.fill(node.OpFodder, true, true)
		u.write(node.Op.String())
		u.unparse(node.Right, true)

	case *ast.Conditional:
		u.write("if")
		u.unparse(node.Cond, true)
		u.fill(node.ThenFodder, true, true)
		u.write("then")
		u.unparse(node.BranchTrue, true)
		if node.BranchFalse != nil {
			u.fill(node.ElseFodder, true, true)
			u.write("else")
			u.unparse(node.BranchFalse, true)
		}

	case *ast.Dollar:
		u.write("$")

	case *ast.Error:
		u.write("error")
		u.unparse(node.Expr, true)

	case *ast.Function:
		u.write("function")
		u.unparseParams(node.ParenLeftFodder, node.Parameters, node.TrailingComma, node.ParenRightFodder)
		u.unparse(node.Body, true)

	case *ast.Import:
		u.write("import")
		u.unparse(node.File, true)

	case *ast.ImportStr:
		u.write("importstr")
		u.unparse(node.File, true)

	case *ast.ImportBin:
		u.write("importbin")
		u.unparse(node.File, true)

	case *ast.Index:
		u.unparse(node.Target, crowded)
		u.fill(node.LeftBracketFodder, false, false) // Can also be DotFodder
		if node.Id != nil {
			u.write(".")
			u.fill(node.RightBracketFodder, false, false) // IdFodder
			u.unparseID(*node.Id)
		} else {
			u.write("[")
			u.unparse(node.Index, false)
			u.fill(node.RightBracketFodder, false, false)
			u.write("]")
		}

	case *ast.Slice:
		u.unparse(node.Target, crowded)
		u.fill(node.LeftBracketFodder, false, false)
		u.write("[")
		if node.BeginIndex != nil {
			u.unparse(node.BeginIndex, false)
		}
		u.fill(node.EndColonFodder, false, false)
		u.write(":")
		if node.EndIndex != nil {
			u.unparse(node.EndIndex, false)
		}
		if node.Step != nil || len(node.StepColonFodder) > 0 {
			u.fill(node.StepColonFodder, false, false)
			u.write(":")
			if node.Step != nil {
				u.unparse(node.Step, false)
			}
		}
		u.fill(node.RightBracketFodder, false, false)
		u.write("]")

	case *ast.InSuper:
		u.unparse(node.Index, true)
		u.fill(node.InFodder, true, true)
		u.write("in")
		u.fill(node.SuperFodder, true, true)
		u.write("super")

	case *ast.Local:
		u.write("local")
		if len(node.Binds) == 0 {
			panic("INTERNAL ERROR: local with no binds")
		}
		first := true
		for _, bind := range node.Binds {
			if !first {
				u.write(",")
			}
			first = false
			u.fill(bind.VarFodder, true, true)
			u.unparseID(bind.Variable)
			if bind.Fun != nil {
				u.unparseParams(bind.Fun.ParenLeftFodder,
					bind.Fun.Parameters,
					bind.Fun.TrailingComma,
					bind.Fun.ParenRightFodder)
			}
			u.fill(bind.EqFodder, true, true)
			u.write("=")
			u.unparse(bind.Body, true)
			u.fill(bind.CloseFodder, false, false)
		}
		u.write(";")
		u.unparse(node.Body, true)

	case *ast.LiteralBoolean:
		if node.Value {
			u.write("true")
		} else {
			u.write("false")
		}

	case *ast.LiteralNumber:
		u.write(node.OriginalString)

	case *ast.LiteralString:
		switch node.Kind {
		case ast.StringDouble:
			u.write("\"")
			// The original escape codes are still in the string.
			u.write(node.Value)
			u.write("\"")
		case ast.StringSingle:
			u.write("'")
			// The original escape codes are still in the string.
			u.write(node.Value)
			u.write("'")
		case ast.StringBlock:
			u.write("|||\n")
			if node.Value[0] != '\n' {
				u.write(node.BlockIndent)
			}
			for i, r := range node.Value {
				// Formatter always outputs in unix mode.
				if r == '\r' {
					continue
				}
				u.write(string(r))
				if r == '\n' && (i+1 < len(node.Value)) && node.Value[i+1] != '\n' {
					u.write(node.BlockIndent)
				}
			}
			u.write(node.BlockTermIndent)
			u.write("|||")
		case ast.VerbatimStringDouble:
			u.write("@\"")
			// Escapes were processed by the parser, so put them back in.
			for _, r := range node.Value {
				if r == '"' {
					u.write("\"\"")
				} else {
					u.write(string(r))
				}
			}
			u.write("\"")
		case ast.VerbatimStringSingle:
			u.write("@'")
			// Escapes were processed by the parser, so put them back in.
			for _, r := range node.Value {
				if r == '\'' {
					u.write("''")
				} else {
					u.write(string(r))
				}
			}
			u.write("'")
		}

	case *ast.LiteralNull:
		u.write("null")

	case *ast.Object:
		u.write("{")
		u.unparseFields(node.Fields, u.options.PadObjects)
		if node.TrailingComma {
			u.write(",")
		}
		u.fill(node.CloseFodder, len(node.Fields) > 0, u.options.PadObjects)
		u.write("}")

	case *ast.ObjectComp:
		u.write("{")
		u.unparseFields(node.Fields, u.options.PadObjects)
		if node.TrailingComma {
			u.write(",")
		}
		u.unparseSpecs(&node.Spec)
		u.fill(node.CloseFodder, true, u.options.PadObjects)
		u.write("}")

	case *ast.Parens:
		u.write("(")
		u.unparse(node.Inner, false)
		u.fill(node.CloseFodder, false, false)
		u.write(")")

	case *ast.Self:
		u.write("self")

	case *ast.SuperIndex:
		u.write("super")
		u.fill(node.DotFodder, false, false)
		if node.Id != nil {
			u.write(".")
			u.fill(node.IDFodder, false, false)
			u.unparseID(*node.Id)
		} else {
			u.write("[")
			u.unparse(node.Index, false)
			u.fill(node.IDFodder, false, false)
			u.write("]")
		}
	case *ast.Var:
		u.unparseID(node.Id)

	case *ast.Unary:
		u.write(node.Op.String())
		u.unparse(node.Expr, false)

	default:
		panic(fmt.Sprintf("INTERNAL ERROR: Unknown AST: %T", expr))
	}
}

func (u *unparser) string() string {
	return u.buf.String()
}
