package preproc

import (
	"fmt"
	"path/filepath"
	"strings"
)

type LineKind int

const (
	Source LineKind = iota
	Assembler
	Script
	ScriptLibrary
	ScriptMacroDef
)

// Line represents one post-processed source line and its provenance.
type Line struct {
	RawText        string   // original line text before any processing
	Text           string   // post-preprocessor line text (after define replacement, comment stripping)
	Filename       string   // file the line came from (after resolving includes)
	LineNo         int      // 1-based line number in Filename
	Kind           LineKind // Source, Assembler, or Script
	PragmaSetIndex int      // index into Pragma stack for this line
}

// HaltError is returned when a `#HALT` directive is encountered.
type HaltError struct{}

func (HaltError) Error() string { return "preprocessor HALT" }

// PreProcess processes the given root source file and returns the flattened, define-expanded
// lines. Directive lines are not emitted.
func PreProcess(rootFilename string, reader ...FileReader) ([]Line, *Pragma, error) {
	var r FileReader
	if len(reader) > 0 && reader[0] != nil {
		r = reader[0]
	} else {
		r = NewDefaultFileReader()
	}
	pp := newPreproc(r)
	lines, err := pp.run(rootFilename)
	return lines, pp.pragma, err
}

// -------------------- internal --------------------

type preproc struct {
	defs            *DefineList // from definelist.go
	pragma          *Pragma     // pragma handler
	cond            []bool      // conditional stack; a line is active if all are true
	inAsm           bool        // true when inside ASM/ENDASM block
	inScript        bool        // true when inside SCRIPT/ENDSCRIPT block
	inScriptLibrary bool        // true when inside SCRIPT LIBRARY/ENDSCRIPT block
	inScriptMacro   bool        // true when inside SCRIPT MACRO/ENDSCRIPT block
	reader          FileReader  // file reader abstraction
}

func newPreproc(reader FileReader) *preproc {
	return &preproc{
		defs:     NewDefineList(),
		pragma:   NewPragma(),
		cond:     []bool{},
		inAsm:    false,
		inScript: false,
		reader:   reader,
	}
}

func (p *preproc) run(root string) ([]Line, error) {
	var out []Line

	type frame struct {
		path  string
		lines []string // file contents split into lines (no newline)
		idx   int      // next line index (0-based)
		line  int      // last emitted line number (1-based)
		dir   string
	}

	newFrame := func(spec string, curDir string) (*frame, error) {
		lines, absPath, err := p.reader.ReadLines(spec, curDir)
		if err != nil {
			return nil, err
		}
		return &frame{
			path:  absPath,
			lines: lines,
			idx:   0,
			line:  0,
			dir:   filepath.Dir(absPath),
		}, nil
	}

	var frameStack []*frame

	frRoot, err := newFrame(root, "")
	if err != nil {
		return nil, err
	}
	frameStack = append(frameStack, frRoot)

	for len(frameStack) > 0 {
		currFrame := frameStack[len(frameStack)-1]

		// if we've exhausted lines in this frame, pop it
		if currFrame.idx >= len(currFrame.lines) {
			frameStack = frameStack[:len(frameStack)-1]
			continue
		}

		// advance to next line
		raw := currFrame.lines[currFrame.idx]
		currFrame.idx++
		currFrame.line = currFrame.idx

		includeSource := p.shouldIncludeSource()
		tokens := strings.Fields(raw)

		// ASM mode handling
		if !p.inAsm && !p.inScript && !p.inScriptLibrary && !p.inScriptMacro {
			// Check for ASM entry
			if includeSource && len(tokens) > 0 && tokens[0] == "ASM" {
				p.inAsm = true
				continue // don't emit ASM marker
			}
			// Check for SCRIPT entry (SCRIPT LIBRARY, SCRIPT MACRO, or plain SCRIPT)
			if includeSource && len(tokens) > 0 && tokens[0] == "SCRIPT" {
				if len(tokens) > 1 && tokens[1] == "LIBRARY" {
					p.inScriptLibrary = true
				} else if len(tokens) > 1 && tokens[1] == "MACRO" {
					p.inScriptMacro = true
					// Emit the header line (SCRIPT MACRO name(params)) for compiler to parse
					out = append(out, Line{
						RawText:        raw,
						Text:           raw,
						Filename:       currFrame.path,
						LineNo:         currFrame.line,
						Kind:           ScriptMacroDef,
						PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
					})
				} else {
					p.inScript = true
				}
				continue // don't emit SCRIPT marker (except for MACRO which emits header)
			}
		} else if p.inAsm {
			// We're in ASM mode
			// Check for ENDASM
			if len(tokens) > 0 && tokens[0] == "ENDASM" {
				p.inAsm = false
				continue // don't emit ENDASM marker
			}
			// Otherwise emit line verbatim as Assembler
			out = append(out, Line{
				RawText:        raw,
				Text:           raw,
				Filename:       currFrame.path,
				LineNo:         currFrame.line,
				Kind:           Assembler,
				PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
			})
			continue
		} else if p.inScript || p.inScriptLibrary || p.inScriptMacro {
			// We're in SCRIPT, SCRIPT LIBRARY, or SCRIPT MACRO mode
			// Check for ENDSCRIPT
			if len(tokens) > 0 && tokens[0] == "ENDSCRIPT" {
				p.inScript = false
				p.inScriptLibrary = false
				p.inScriptMacro = false
				// Emit an empty Source line as a block boundary marker so the
				// compiler can distinguish consecutive SCRIPT blocks via kind
				// transitions. The compiler skips empty Source lines, so this
				// serves purely as a transition trigger.
				if includeSource {
					out = append(out, Line{
						RawText:        raw,
						Text:           "",
						Filename:       currFrame.path,
						LineNo:         currFrame.line,
						Kind:           Source,
						PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
					})
				}
				continue
			}
			// Determine the kind based on which mode we're in
			kind := Script
			if p.inScriptLibrary {
				kind = ScriptLibrary
			} else if p.inScriptMacro {
				kind = ScriptMacroDef
			}
			// Emit line verbatim with appropriate kind
			out = append(out, Line{
				RawText:        raw,
				Text:           raw,
				Filename:       currFrame.path,
				LineNo:         currFrame.line,
				Kind:           kind,
				PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
			})
			continue
		}

		trim := strings.TrimSpace(raw)
		isDirective := strings.HasPrefix(trim, "#")

		if isDirective {
			parts := strings.Fields(trim)
			if len(parts) == 0 {
				continue
			}
			switch strings.ToUpper(parts[0]) {
			case "#DEFINE":
				if includeSource && len(parts) >= 2 {
					name := parts[1]
					val := ""
					if len(parts) > 2 {
						start := 2
						if parts[2] == "=" {
							start = 3
						}
						if start < len(parts) {
							val = strings.Join(parts[start:], " ")
							val = p.defs.ReplaceDefines(val)
						}
					}
					p.defs.Add(name, val)
				}
				continue
			case "#UNDEF":
				if includeSource && len(parts) >= 2 {
					p.defs.Delete(parts[1])
				}
				continue
			case "#IFDEF":
				if len(parts) != 2 {
					return nil, fmt.Errorf("#IFDEF requires exactly one argument at %s:%d", currFrame.path, currFrame.line)
				}
				p.cond = append(p.cond, p.defs.Defined(parts[1]))
				continue

			case "#IFNDEF":
				if len(parts) != 2 {
					return nil, fmt.Errorf("#IFNDEF requires exactly one argument at %s:%d", currFrame.path, currFrame.line)
				}
				p.cond = append(p.cond, !p.defs.Defined(parts[1]))
				continue
			case "#IFEND":
				if len(p.cond) > 0 {
					p.cond = p.cond[:len(p.cond)-1]
				}
				continue
			case "#INCLUDE":
				if includeSource {
					if len(parts) < 2 {
						return nil, fmt.Errorf("#INCLUDE without path at %s:%d", currFrame.path, currFrame.line)
					}
					incPathRaw := parts[1]
					next, err := newFrame(incPathRaw, currFrame.dir)
					if err != nil {
						return nil, fmt.Errorf("include failed at %s:%d: %w", currFrame.path, currFrame.line, err)
					}
					frameStack = append(frameStack, next)
				}
				continue
			case "#PRINT":
				if includeSource {
					msg := strings.TrimSpace(strings.TrimPrefix(trim, "#PRINT"))
					msg = p.defs.ReplaceDefines(msg)
					fmt.Println(msg)
				}
				continue
			case "#HALT":
				if includeSource {
					return out, HaltError{}
				}
				continue
			case "#PRAGMA":
				if includeSource && len(parts) >= 2 {
					name := strings.ToUpper(parts[1])
					val := ""
					if len(parts) > 2 {
						val = strings.Join(parts[2:], " ")
						val = p.defs.ReplaceDefines(val)
					}
					p.pragma.AddPragma(name, val)
				}
				continue
			default:
				// Unknown directive: drop (do not emit)
				continue
			}
		}

		if !includeSource {
			continue
		}

		// Non-directive Source line: expand defines, strip comments, emit
		text := p.defs.ReplaceDefines(raw)

		// Strip comments (everything after //)
		if idx := strings.Index(text, "//"); idx >= 0 {
			text = text[:idx]
		}
		text = strings.TrimRight(text, " \t")

		out = append(out, Line{
			RawText:        raw,
			Text:           text,
			Filename:       currFrame.path,
			LineNo:         currFrame.line,
			Kind:           Source,
			PragmaSetIndex: p.pragma.GetCurrentPragmaSetIndex(),
		})
	}

	if len(p.cond) > 0 {
		return nil, fmt.Errorf("unbalanced conditionals: %d unclosed #IFDEF/#IFNDEF directives", len(p.cond))
	}

	return out, nil
}

func (p *preproc) shouldIncludeSource() bool {
	for _, v := range p.cond {
		if !v {
			return false
		}
	}
	return true
}
