package format

import (
	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/astnav"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/scanner"
)

type FormattingContext struct {
	currentTokenSpan   TextRangeWithKind
	nextTokenSpan      TextRangeWithKind
	contextNode        *ast.Node
	currentTokenParent *ast.Node
	nextTokenParent    *ast.Node

	contextNodeAllOnSameLine    core.Tristate
	nextNodeAllOnSameLine       core.Tristate
	tokensAreOnSameLine         core.Tristate
	contextNodeBlockIsOnOneLine core.Tristate
	nextNodeBlockIsOnOneLine    core.Tristate

	SourceFile            *ast.SourceFile
	FormattingRequestKind FormatRequestKind
	Options               lsutil.FormatCodeSettings
}

func NewFormattingContext(file *ast.SourceFile, kind FormatRequestKind, options lsutil.FormatCodeSettings) *FormattingContext {
	res := &FormattingContext{
		SourceFile:            file,
		FormattingRequestKind: kind,
		Options:               options,
	}
	return res
}

func (this *FormattingContext) UpdateContext(cur TextRangeWithKind, curParent *ast.Node, next TextRangeWithKind, nextParent *ast.Node, commonParent *ast.Node) {
	if curParent == nil {
		panic("nil current range node parent in update context")
	}
	if nextParent == nil {
		panic("nil next range node parent in update context")
	}
	if commonParent == nil {
		panic("nil common parent node in update context")
	}
	this.currentTokenSpan = cur
	this.currentTokenParent = curParent
	this.nextTokenSpan = next
	this.nextTokenParent = nextParent
	this.contextNode = commonParent

	// drop cached results
	this.contextNodeAllOnSameLine = core.TSUnknown
	this.nextNodeAllOnSameLine = core.TSUnknown
	this.tokensAreOnSameLine = core.TSUnknown
	this.contextNodeBlockIsOnOneLine = core.TSUnknown
	this.nextNodeBlockIsOnOneLine = core.TSUnknown
}

func (this *FormattingContext) rangeIsOnOneLine(node core.TextRange) core.Tristate {
	if rangeIsOnOneLine(node, this.SourceFile) {
		return core.TSTrue
	}
	return core.TSFalse
}

func (this *FormattingContext) nodeIsOnOneLine(node *ast.Node) core.Tristate {
	return this.rangeIsOnOneLine(withTokenStart(node, this.SourceFile))
}

func withTokenStart(loc *ast.Node, file *ast.SourceFile) core.TextRange {
	startPos := scanner.GetTokenPosOfNode(loc, file, false)
	return core.NewTextRange(startPos, loc.End())
}

func (this *FormattingContext) blockIsOnOneLine(node *ast.Node) core.Tristate {
	openBrace := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, this.SourceFile)
	closeBrace := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, this.SourceFile)
	if openBrace != nil && closeBrace != nil {
		closeBraceStart := scanner.GetTokenPosOfNode(closeBrace, this.SourceFile, false)
		return this.rangeIsOnOneLine(core.NewTextRange(openBrace.End(), closeBraceStart))
	}
	return core.TSFalse
}

func (this *FormattingContext) ContextNodeAllOnSameLine() bool {
	if this.contextNodeAllOnSameLine == core.TSUnknown {
		this.contextNodeAllOnSameLine = this.nodeIsOnOneLine(this.contextNode)
	}
	return this.contextNodeAllOnSameLine == core.TSTrue
}

func (this *FormattingContext) NextNodeAllOnSameLine() bool {
	if this.nextNodeAllOnSameLine == core.TSUnknown {
		this.nextNodeAllOnSameLine = this.nodeIsOnOneLine(this.nextTokenParent)
	}
	return this.nextNodeAllOnSameLine == core.TSTrue
}

func (this *FormattingContext) TokensAreOnSameLine() bool {
	if this.tokensAreOnSameLine == core.TSUnknown {
		this.tokensAreOnSameLine = this.rangeIsOnOneLine(core.NewTextRange(this.currentTokenSpan.Loc.Pos(), this.nextTokenSpan.Loc.End()))
	}
	return this.tokensAreOnSameLine == core.TSTrue
}

func (this *FormattingContext) ContextNodeBlockIsOnOneLine() bool {
	if this.contextNodeBlockIsOnOneLine == core.TSUnknown {
		this.contextNodeBlockIsOnOneLine = this.blockIsOnOneLine(this.contextNode)
	}
	return this.contextNodeBlockIsOnOneLine == core.TSTrue
}

func (this *FormattingContext) NextNodeBlockIsOnOneLine() bool {
	if this.nextNodeBlockIsOnOneLine == core.TSUnknown {
		this.nextNodeBlockIsOnOneLine = this.blockIsOnOneLine(this.nextTokenParent)
	}
	return this.nextNodeBlockIsOnOneLine == core.TSTrue
}
