package hclsyntax

import (
    "bytes"
    "fmt"
    "path/filepath"
    "runtime"
    "strings"

    "Havoc/pkg/profile/yaotl"
)

// This is set to true at init() time in tests, to enable more useful output
// if a stack discipline error is detected. It should not be enabled in
// normal mode since there is a performance penalty from accessing the
// runtime stack to produce the traces, but could be temporarily set to
// true for debugging if desired.
var tracePeekerNewlinesStack = false

type peeker struct {
    Tokens    Tokens
    NextIndex int

    IncludeComments      bool
    IncludeNewlinesStack []bool

    // used only when tracePeekerNewlinesStack is set
    newlineStackChanges []peekerNewlineStackChange
}

// for use in debugging the stack usage only
type peekerNewlineStackChange struct {
    Pushing bool // if false, then popping
    Frame   runtime.Frame
    Include bool
}

func newPeeker(tokens Tokens, includeComments bool) *peeker {
    return &peeker{
        Tokens:          tokens,
        IncludeComments: includeComments,

        IncludeNewlinesStack: []bool{true},
    }
}

func (p *peeker) Peek() Token {
    ret, _ := p.nextToken()
    return ret
}

func (p *peeker) Read() Token {
    ret, nextIdx := p.nextToken()
    p.NextIndex = nextIdx
    return ret
}

func (p *peeker) NextRange() hcl.Range {
    return p.Peek().Range
}

func (p *peeker) PrevRange() hcl.Range {
    if p.NextIndex == 0 {
        return p.NextRange()
    }

    return p.Tokens[p.NextIndex-1].Range
}

func (p *peeker) nextToken() (Token, int) {
    for i := p.NextIndex; i < len(p.Tokens); i++ {
        tok := p.Tokens[i]
        switch tok.Type {
        case TokenComment:
            if !p.IncludeComments {
                // Single-line comment tokens, starting with # or //, absorb
                // the trailing newline that terminates them as part of their
                // bytes. When we're filtering out comments, we must as a
                // special case transform these to newline tokens in order
                // to properly parse newline-terminated block items.

                if p.includingNewlines() {
                    if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
                        fakeNewline := Token{
                            Type:  TokenNewline,
                            Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)],

                            // We use the whole token range as the newline
                            // range, even though that's a little... weird,
                            // because otherwise we'd need to go count
                            // characters again in order to figure out the
                            // column of the newline, and that complexity
                            // isn't justified when ranges of newlines are
                            // so rarely printed anyway.
                            Range: tok.Range,
                        }
                        return fakeNewline, i + 1
                    }
                }

                continue
            }
        case TokenNewline:
            if !p.includingNewlines() {
                continue
            }
        }

        return tok, i + 1
    }

    // if we fall out here then we'll return the EOF token, and leave
    // our index pointed off the end of the array so we'll keep
    // returning EOF in future too.
    return p.Tokens[len(p.Tokens)-1], len(p.Tokens)
}

func (p *peeker) includingNewlines() bool {
    return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1]
}

func (p *peeker) PushIncludeNewlines(include bool) {
    if tracePeekerNewlinesStack {
        // Record who called us so that we can more easily track down any
        // mismanagement of the stack in the parser.
        callers := []uintptr{0}
        runtime.Callers(2, callers)
        frames := runtime.CallersFrames(callers)
        frame, _ := frames.Next()
        p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
            true, frame, include,
        })
    }

    p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include)
}

func (p *peeker) PopIncludeNewlines() bool {
    stack := p.IncludeNewlinesStack
    remain, ret := stack[:len(stack)-1], stack[len(stack)-1]
    p.IncludeNewlinesStack = remain

    if tracePeekerNewlinesStack {
        // Record who called us so that we can more easily track down any
        // mismanagement of the stack in the parser.
        callers := []uintptr{0}
        runtime.Callers(2, callers)
        frames := runtime.CallersFrames(callers)
        frame, _ := frames.Next()
        p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
            false, frame, ret,
        })
    }

    return ret
}

// AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing
// panicking if it is not. This can be used to catch stack mismanagement that
// might otherwise just cause confusing downstream errors.
//
// This function is a no-op if the stack is empty when called.
//
// If newlines stack tracing is enabled by setting the global variable
// tracePeekerNewlinesStack at init time, a full log of all of the push/pop
// calls will be produced to help identify which caller in the parser is
// misbehaving.
func (p *peeker) AssertEmptyIncludeNewlinesStack() {
    if len(p.IncludeNewlinesStack) != 1 {
        // Should never happen; indicates mismanagement of the stack inside
        // the parser.
        if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above
            panic(fmt.Errorf(
                "non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s",
                len(p.IncludeNewlinesStack)-1,
                formatPeekerNewlineStackChanges(p.newlineStackChanges),
            ))
        } else {
            panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack))
        }
    }
}

func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string {
    indent := 0
    var buf bytes.Buffer
    for _, change := range changes {
        funcName := change.Frame.Function
        if idx := strings.LastIndexByte(funcName, '.'); idx != -1 {
            funcName = funcName[idx+1:]
        }
        filename := change.Frame.File
        if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 {
            filename = filename[idx+1:]
        }

        switch change.Pushing {

        case true:
            buf.WriteString(strings.Repeat("    ", indent))
            fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
            indent++

        case false:
            indent--
            buf.WriteString(strings.Repeat("    ", indent))
            fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)

        }
    }
    return buf.String()
}
