package compiler

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

	"c65gm/internal/preproc"
)

// Compiler orchestrates the compilation process
type Compiler struct {
	ctx      *CompilerContext
	registry *CommandRegistry
}

// NewCompiler creates a new compiler with initialized context and registry
func NewCompiler(pragma *preproc.Pragma) *Compiler {
	return &Compiler{
		ctx:      NewCompilerContext(pragma),
		registry: NewCommandRegistry(),
	}
}

// Context returns the compiler context (for registering commands that need it)
func (c *Compiler) Context() *CompilerContext {
	return c.ctx
}

// Registry returns the command registry (for registering commands)
func (c *Compiler) Registry() *CommandRegistry {
	return c.registry
}

// Compile processes preprocessed lines and generates assembly output
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
	var codeOutput []string
	var lastKind = preproc.Source
	var scriptBuffer []string
	var scriptIsLibrary bool
	var macroBuffer []string
	var currentMacroName string
	var currentMacroParams []string

	for i, line := range lines {
		// Detect kind transitions and emit markers
		if line.Kind != lastKind {
			// Execute and close previous Script or ScriptLibrary block
			if lastKind == preproc.Script || lastKind == preproc.ScriptLibrary {
				scriptOutput, err := executeScript(scriptBuffer, c.ctx, scriptIsLibrary)
				if err != nil {
					return nil, fmt.Errorf("script execution failed: %w", err)
				}
				codeOutput = append(codeOutput, scriptOutput...)
				scriptBuffer = nil
				if scriptIsLibrary {
					codeOutput = append(codeOutput, "; ENDSCRIPT LIBRARY")
				} else {
					codeOutput = append(codeOutput, "; ENDSCRIPT")
				}
			}

			// Store previous ScriptMacroDef block
			if lastKind == preproc.ScriptMacroDef {
				if currentMacroName != "" {
					c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{
						Name:   currentMacroName,
						Params: currentMacroParams,
						Body:   macroBuffer,
					}
					codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
				}
				macroBuffer = nil
				currentMacroName = ""
				currentMacroParams = nil
			}

			// Close previous Assembler block
			if lastKind == preproc.Assembler {
				codeOutput = append(codeOutput, "; ENDASM")
			}

			// Open new block
			if line.Kind == preproc.Assembler {
				codeOutput = append(codeOutput, "; ASM")
			} else if line.Kind == preproc.Script {
				codeOutput = append(codeOutput, "; SCRIPT")
				scriptIsLibrary = false
			} else if line.Kind == preproc.ScriptLibrary {
				codeOutput = append(codeOutput, "; SCRIPT LIBRARY")
				scriptIsLibrary = true
			} else if line.Kind == preproc.ScriptMacroDef {
				// First line is the header - parse it
				name, params, err := parseMacroHeader(line.Text)
				if err != nil {
					c.printErrorWithContext(lines, i, err)
					return nil, fmt.Errorf("compilation failed")
				}
				currentMacroName = name
				currentMacroParams = params
				codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
			}

			lastKind = line.Kind
		}

		// Handle non-source lines
		if line.Kind != preproc.Source {
			if line.Kind == preproc.Assembler {
				text := line.Text

				// Find comment boundary - only process |...| patterns in the code portion
				commentPos := findAsmCommentStart(text)
				codePart := text
				commentPart := ""
				if commentPos != -1 {
					codePart = text[:commentPos]
					commentPart = text[commentPos:]
				}

				// Check for |@macro()| pattern first (only in code portion, outside strings)
				macroStart := findPipeOutsideStrings(codePart, 0)
				if macroStart != -1 && macroStart+1 < len(codePart) && codePart[macroStart+1] == '@' {
					macroEnd := findPipeOutsideStrings(codePart, macroStart+1)
					if macroEnd != -1 {
						invocation := codePart[macroStart+1 : macroEnd] // @name(args)

						macroName, args, err := ParseMacroInvocation(invocation)
						if err != nil {
							c.printErrorWithContext(lines, i, err)
							return nil, fmt.Errorf("compilation failed")
						}

						macroOutput, err := ExecuteMacro(macroName, args, c.ctx)
						if err != nil {
							c.printErrorWithContext(lines, i, fmt.Errorf("macro %s: %w", macroName, err))
							return nil, fmt.Errorf("compilation failed")
						}

						// Emit with comments showing invocation
						codeOutput = append(codeOutput, fmt.Sprintf("; %s", text))
						codeOutput = append(codeOutput, macroOutput...)
						codeOutput = append(codeOutput, fmt.Sprintf("; end @%s", macroName))
						continue
					}
				}

				// Expand |varname| -> scoped_varname for local variables in ASM blocks (only in code portion, outside strings)
				searchFrom := 0
				for {
					start := findPipeOutsideStrings(codePart, searchFrom)
					if start == -1 {
						break
					}
					end := findPipeOutsideStrings(codePart, start+1)
					if end == -1 {
						c.printErrorWithContext(lines, i, fmt.Errorf("unclosed | in assembler line"))
						return nil, fmt.Errorf("compilation failed")
					}

					varName := codePart[start+1 : end]
					expandedName := c.ctx.SymbolTable.ExpandName(varName, c.ctx.CurrentScope())
					codePart = codePart[:start] + expandedName + codePart[end+1:]
					// Continue searching after the replacement
					searchFrom = start + len(expandedName)
				}
				codeOutput = append(codeOutput, codePart+commentPart)
			} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
				// Collect script lines for execution
				scriptBuffer = append(scriptBuffer, line.Text)
			} else if line.Kind == preproc.ScriptMacroDef {
				// Skip the header line (already parsed in transition)
				if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
					continue
				}
				// Collect macro body lines
				macroBuffer = append(macroBuffer, line.Text)
			}
			continue
		}

		// Skip empty/whitespace-only lines
		if strings.TrimSpace(line.Text) == "" {
			continue
		}

		// Find handler for this line
		cmd, found := c.registry.FindHandler(line)
		if !found {
			c.printErrorWithContext(lines, i, fmt.Errorf("unhandled line (no matching command)"))
			return nil, fmt.Errorf("compilation failed")
		}

		// Interpret the line
		if err := cmd.Interpret(line, c.ctx); err != nil {
			c.printErrorWithContext(lines, i, err)
			return nil, fmt.Errorf("compilation failed")
		}

		// Generate assembly
		asmLines, err := cmd.Generate(c.ctx)
		if err != nil {
			c.printErrorWithContext(lines, i, err)
			return nil, fmt.Errorf("compilation failed")
		}

		codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text))
		codeOutput = append(codeOutput, asmLines...)
	}

	// Close any open block
	if lastKind == preproc.Assembler {
		return nil, fmt.Errorf("Unclosed ASM block.")
	} else if lastKind == preproc.Script {
		return nil, fmt.Errorf("Unclosed SCRIPT block.")
	} else if lastKind == preproc.ScriptLibrary {
		return nil, fmt.Errorf("Unclosed SCRIPT LIBRARY block.")
	} else if lastKind == preproc.ScriptMacroDef {
		return nil, fmt.Errorf("Unclosed SCRIPT MACRO block.")
	}

	// Analyze for overlapping absolute addresses in function call chains
	c.checkAbsoluteOverlaps()

	// Check for unused variables and print warnings
	warnings := c.ctx.SymbolTable.CheckUnused()
	for _, warning := range warnings {
		_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
	}

	// Check for unused functions and print warnings
	funcWarnings := c.ctx.FunctionHandler.CheckUnusedFunctions()
	for _, warning := range funcWarnings {
		_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
	}

	// Remove unused functions with _P_REMOVE_UNUSED pragma
	codeOutput = c.removeUnusedFunctions(codeOutput)

	// Assemble final output with headers and footers
	return c.assembleOutput(codeOutput), nil
}

// printErrorWithContext prints an error with source code context to stderr
func (c *Compiler) printErrorWithContext(lines []preproc.Line, lineIndex int, err error) {
	if lineIndex < 0 || lineIndex >= len(lines) {
		// Shouldn't happen, but be safe
		_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		return
	}

	line := lines[lineIndex]
	const contextLines = 3

	// Print error header
	_, _ = fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
	_, _ = fmt.Fprintf(os.Stderr, "  --> %s:%d\n\n", line.Filename, line.LineNo)

	// Find all lines from the same file for context
	// Group consecutive lines from the same file
	fileLines := make(map[string][]int)
	for i, l := range lines {
		if l.Filename == line.Filename {
			fileLines[l.Filename] = append(fileLines[l.Filename], i)
		}
	}

	// Get context range (lineIndex within our lines array)
	startIdx := lineIndex - contextLines
	if startIdx < 0 {
		startIdx = 0
	}
	endIdx := lineIndex + contextLines
	if endIdx >= len(lines) {
		endIdx = len(lines) - 1
	}

	// Calculate width for line numbers based on actual source line numbers
	maxLineNo := 0
	for i := startIdx; i <= endIdx; i++ {
		if lines[i].LineNo > maxLineNo {
			maxLineNo = lines[i].LineNo
		}
	}
	maxLineNumWidth := len(fmt.Sprintf("%d", maxLineNo))

	// Print context
	for i := startIdx; i <= endIdx; i++ {
		l := lines[i]

		// Skip lines from different files
		if l.Filename != line.Filename {
			continue
		}

		if i == lineIndex {
			// Error line - highlight it
			_, _ = fmt.Fprintf(os.Stderr, ">> %*d | %s\n", maxLineNumWidth, l.LineNo, l.Text)
		} else {
			// Context line
			_, _ = fmt.Fprintf(os.Stderr, "   %*d | %s\n", maxLineNumWidth, l.LineNo, l.Text)
		}
	}

	_, _ = fmt.Fprintf(os.Stderr, "\n")
}

// checkAbsoluteOverlaps analyzes and warns about overlapping absolute addresses
func (c *Compiler) checkAbsoluteOverlaps() {
	c.ctx.FunctionHandler.ReportAbsoluteOverlaps()
}

// parseMacroHeader parses "SCRIPT MACRO name(param1, param2, ...)" and returns name and params
func parseMacroHeader(header string) (string, []string, error) {
	// Expected format: "SCRIPT MACRO name(param1, param2, ...)"
	header = strings.TrimSpace(header)

	// Remove "SCRIPT MACRO " prefix
	if !strings.HasPrefix(header, "SCRIPT MACRO ") {
		return "", nil, fmt.Errorf("invalid macro header: %s", header)
	}
	rest := strings.TrimSpace(header[len("SCRIPT MACRO "):])

	// Find opening paren
	parenStart := strings.IndexByte(rest, '(')
	if parenStart == -1 {
		return "", nil, fmt.Errorf("macro header missing '(': %s", header)
	}

	// Find closing paren
	parenEnd := strings.LastIndexByte(rest, ')')
	if parenEnd == -1 || parenEnd < parenStart {
		return "", nil, fmt.Errorf("macro header missing ')': %s", header)
	}

	name := strings.TrimSpace(rest[:parenStart])
	if name == "" {
		return "", nil, fmt.Errorf("macro name is empty")
	}

	// Parse parameters
	paramStr := strings.TrimSpace(rest[parenStart+1 : parenEnd])
	var params []string
	if paramStr != "" {
		parts := strings.Split(paramStr, ",")
		for _, p := range parts {
			param := strings.TrimSpace(p)
			if param == "" {
				return "", nil, fmt.Errorf("empty parameter in macro definition")
			}
			params = append(params, param)
		}
	}

	return name, params, nil
}

// findAsmCommentStart finds the position of ';' that starts a comment in an ASM line,
// taking into account that ';' inside strings (single or double quoted) should be ignored.
// Supports ACME-style escape sequences (backslash escapes) inside strings.
// Returns -1 if no comment is found.
func findAsmCommentStart(line string) int {
	inString := false
	escaped := false
	var quoteChar rune

	for i, ch := range line {
		if escaped {
			escaped = false
			continue
		}
		if inString {
			if ch == '\\' {
				escaped = true
				continue
			}
			if ch == quoteChar {
				inString = false
			}
			continue
		}
		// Not in string
		if ch == '"' || ch == '\'' {
			inString = true
			quoteChar = ch
			continue
		}
		if ch == ';' {
			return i
		}
	}
	return -1
}

// findPipeOutsideStrings finds the position of '|' that is not inside a string.
// Supports ACME-style escape sequences (backslash escapes) inside strings.
// Starts searching from 'startFrom' position.
// Returns -1 if no such '|' is found.
func findPipeOutsideStrings(line string, startFrom int) int {
	inString := false
	escaped := false
	var quoteChar rune

	for i, ch := range line {
		if i < startFrom {
			// Track string state even before startFrom
			if escaped {
				escaped = false
				continue
			}
			if inString {
				if ch == '\\' {
					escaped = true
					continue
				}
				if ch == quoteChar {
					inString = false
				}
				continue
			}
			if ch == '"' || ch == '\'' {
				inString = true
				quoteChar = ch
			}
			continue
		}

		if escaped {
			escaped = false
			continue
		}
		if inString {
			if ch == '\\' {
				escaped = true
				continue
			}
			if ch == quoteChar {
				inString = false
			}
			continue
		}
		// Not in string
		if ch == '"' || ch == '\'' {
			inString = true
			quoteChar = ch
			continue
		}
		if ch == '|' {
			return i
		}
	}
	return -1
}

// removeUnusedFunctions removes functions marked with _P_REMOVE_UNUSED pragma from assembly output
func (c *Compiler) removeUnusedFunctions(codeLines []string) []string {
	toRemove := c.ctx.FunctionHandler.GetFunctionsToRemove()
	if len(toRemove) == 0 {
		return codeLines
	}

	var result []string
	i := 0
	for i < len(codeLines) {
		line := codeLines[i]

		// Check for function start marker
		if strings.HasPrefix(line, "; @@FUNC_BEGIN ") {
			parts := strings.Fields(line)
			if len(parts) >= 3 && toRemove[parts[2]] {
				funcName := parts[2]
				
				// Find the function declaration to get file/line info
				var filename string
				var lineNo int
				for _, funcDecl := range c.ctx.FunctionHandler.functions {
					if funcDecl.Name == funcName {
						filename = funcDecl.Line.Filename
						lineNo = funcDecl.Line.LineNo
						break
					}
				}
				
				// Print info message to stdout
				if filename != "" {
					baseFilename := filepath.Base(filename)
					fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName)
				} else {
					fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName)
				}

				// Skip everything until matching @@FUNC_END
				foundEnd := false
				for i < len(codeLines) {
					if strings.HasPrefix(codeLines[i], "; @@FUNC_END ") {
						// Check if this is the exact function end marker
						parts := strings.Fields(codeLines[i])
						if len(parts) >= 3 && parts[2] == funcName {
							foundEnd = true
							break
						}
					}
					i++
				}
				// Skip the END marker line too if found
				if foundEnd && i < len(codeLines) {
					i++
				}
				// If we didn't find the end marker, we've reached end of file
				continue
			}
		}

		result = append(result, line)
		i++
	}
	return result
}

// assembleOutput combines all generated sections into final assembly
func (c *Compiler) assembleOutput(codeLines []string) []string {
	var output []string

	// Header comment
	output = append(output, ";Generated by c65gm")
	output = append(output, "")

	// Constants section
	if constLines := GenerateConstants(c.ctx.SymbolTable); len(constLines) > 0 {
		output = append(output, constLines...)
	}

	// Absolute addresses section
	if absLines := GenerateAbsolutes(c.ctx.SymbolTable); len(absLines) > 0 {
		output = append(output, absLines...)
	}

	// Main code section
	output = append(output, ";Main code")
	output = append(output, "")
	output = append(output, codeLines...)
	output = append(output, "")

	// Variables section
	if varLines := GenerateVariables(c.ctx.SymbolTable); len(varLines) > 0 {
		output = append(output, varLines...)
	}

	// Constant strings section
	if strLines := c.ctx.ConstStrHandler.GenerateConstStrDecls(); len(strLines) > 0 {
		output = append(output, ";Constant strings (from c65gm)")
		output = append(output, "")
		output = append(output, strLines...)
		output = append(output, "")
	}

	return output
}
