/*
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 (
	"sort"

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

type importElem struct {
	key            string
	adjacentFodder ast.Fodder
	bind           ast.LocalBind
}

func sortGroup(imports []importElem) {
	if !duplicatedVariables(imports) {
		sort.Slice(imports, func(i, j int) bool {
			return imports[i].key < imports[j].key
		})
	}
}

// Check if `local` expression is used for importing.
func isGoodLocal(local *ast.Local) bool {
	for _, bind := range local.Binds {
		if bind.Fun != nil {
			return false
		}
		_, ok := bind.Body.(*ast.Import)
		if !ok {
			return false
		}
	}
	return true
}

func goodLocalOrNull(node ast.Node) *ast.Local {
	local, ok := node.(*ast.Local)
	if ok && isGoodLocal(local) {
		return local
	}
	return nil
}

/** Split fodder after the first new line / paragraph fodder,
 * leaving blank lines after the newline in the second half.
 *
 * The two returned fodders can be concatenated using concat_fodder to get the original fodder.
 *
 * It's a heuristic that given two consecutive tokens `prev_token`, `next_token`
 * with some fodder between them, decides which part of the fodder logically belongs
 * to `prev_token` and which part belongs to the `next_token`.
 *
 * Example:
 * prev_token // prev_token is awesome!
 *
 * // blah blah
 * next_token
 *
 * In such case "// prev_token is awesome!\n" part of the fodder belongs
 * to the `prev_token` and "\n//blah blah\n" to the `next_token`.
 */
func splitFodder(fodder ast.Fodder) (ast.Fodder, ast.Fodder) {
	var afterPrev, beforeNext ast.Fodder
	inSecondPart := false
	for _, fodderElem := range fodder {
		if inSecondPart {
			ast.FodderAppend(&beforeNext, fodderElem)
		} else {
			afterPrev = append(afterPrev, fodderElem)
		}
		if fodderElem.Kind != ast.FodderInterstitial && !inSecondPart {
			inSecondPart = true
			if fodderElem.Blanks > 0 {
				// If there are any blank lines at the end of afterPrev, move them
				// to beforeNext.
				afterPrev[len(afterPrev)-1].Blanks = 0
				if len(beforeNext) != 0 {
					panic("beforeNext should still be empty.")
				}
				beforeNext = append(beforeNext, ast.FodderElement{
					Kind:   ast.FodderLineEnd,
					Blanks: fodderElem.Blanks,
					Indent: fodderElem.Indent,
				})
			}
		}
	}
	return afterPrev, beforeNext
}

func extractImportElems(binds ast.LocalBinds, after ast.Fodder) []importElem {
	var result []importElem
	before := binds[0].VarFodder
	for i, bind := range binds {
		last := i == len(binds)-1
		var adjacent ast.Fodder
		var beforeNext ast.Fodder
		if !last {
			next := &binds[i+1]
			adjacent, beforeNext = splitFodder(next.VarFodder)
		} else {
			adjacent = after
		}
		ast.FodderEnsureCleanNewline(&adjacent)
		newBind := bind
		newBind.VarFodder = before
		theImport := bind.Body.(*ast.Import)
		result = append(result,
			importElem{theImport.File.Value, adjacent, newBind})
		before = beforeNext
	}
	return result
}

func buildGroupAST(imports []importElem, body ast.Node, groupOpenFodder ast.Fodder) ast.Node {
	for i := len(imports) - 1; i >= 0; i-- {
		theImport := &(imports)[i]
		var fodder ast.Fodder
		if i == 0 {
			fodder = groupOpenFodder
		} else {
			fodder = imports[i-1].adjacentFodder
		}
		local := &ast.Local{
			NodeBase: ast.NodeBase{Fodder: fodder},
			Binds:    []ast.LocalBind{theImport.bind},
			Body:     body}
		body = local
	}
	return body
}

func duplicatedVariables(elems []importElem) bool {
	idents := make(map[string]bool)
	for _, elem := range elems {
		idents[string(elem.bind.Variable)] = true
	}
	return len(idents) < len(elems)
}

func groupEndsAfter(local *ast.Local) bool {
	next := goodLocalOrNull(local.Body)
	if next == nil {
		return true
	}
	newlineReached := false
	for _, fodderElem := range *openFodder(next) {
		if newlineReached || fodderElem.Blanks > 0 {
			return true
		}
		if fodderElem.Kind != ast.FodderInterstitial {
			newlineReached = true
		}
	}
	return false
}

func topLevelImport(local *ast.Local, imports *[]importElem, groupOpenFodder ast.Fodder) ast.Node {
	if !isGoodLocal(local) {
		panic("topLevelImport called with bad local.")
	}
	adjacentCommentFodder, beforeNextFodder :=
		splitFodder(*openFodder(local.Body))
	ast.FodderEnsureCleanNewline(&adjacentCommentFodder)
	newImports := extractImportElems(local.Binds, adjacentCommentFodder)
	*imports = append(*imports, newImports...)

	if groupEndsAfter(local) {
		sortGroup(*imports)
		afterGroup := (*imports)[len(*imports)-1].adjacentFodder
		ast.FodderEnsureCleanNewline(&beforeNextFodder)
		nextOpenFodder := ast.FodderConcat(afterGroup, beforeNextFodder)
		var bodyAfterGroup ast.Node
		// Process the code after the current group:
		next := goodLocalOrNull(local.Body)
		if next != nil {
			// Another group of imports
			nextImports := make([]importElem, 0)
			bodyAfterGroup = topLevelImport(next, &nextImports, nextOpenFodder)
		} else {
			// Something else
			bodyAfterGroup = local.Body
			*openFodder(bodyAfterGroup) = nextOpenFodder
		}

		return buildGroupAST(*imports, bodyAfterGroup, groupOpenFodder)
	}

	if len(beforeNextFodder) > 0 {
		panic("Expected beforeNextFodder to be empty")
	}
	return topLevelImport(local.Body.(*ast.Local), imports, groupOpenFodder)
}

// SortImports sorts imports at the top of the file into alphabetical order
// by path.
//
// Top-level imports are `local x = import 'xxx.jsonnet` expressions
// that go before anything else in the file (more precisely all such imports
// that are either the root of AST or a direct child (body) of a top-level
// import.  Top-level imports are therefore more top-level than top-level
// functions.
//
// Grouping of imports is preserved. Groups of imports are separated by blank
// lines or lines containing comments.
func SortImports(file *ast.Node) {
	imports := make([]importElem, 0)
	local := goodLocalOrNull(*file)
	if local != nil {
		*file = topLevelImport(local, &imports, *openFodder(local))
	}
}
