package main

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

	"github.com/marzeq/qk/codegen/llvm"
	"github.com/marzeq/qk/ir"
	"github.com/marzeq/qk/loader"
	"github.com/marzeq/qk/parser"
	"github.com/marzeq/qk/sema"
	"github.com/marzeq/qk/shared"
	"github.com/marzeq/qk/tokeniser"
	"github.com/marzeq/qk/types"
)

func main() {
	args, err := parseArgs()
	check(err)

	searchPaths := buildSearchPaths(args.baseDir)

	files, err := collectSourceFiles(searchPaths, args.excludeDirs)
	check(err)

	if len(files) == 0 {
		fmt.Println("no source files found")
		os.Exit(1)
	}

	var partials []*loader.PartialModuleInfo

	for _, file := range files {
		ast, err := parseFile(file)
		check(err)

		info, err := loader.CollectModuleInfo(ast)
		check(err)

		partials = append(partials, info)
	}

	if args.verbose {
		fmt.Println("parsed and collected modules")
	}

	modules, err := loader.BuildModules(partials)
	check(err)

	analyser := sema.NewAnalyser()

	order, errs := loader.ComputeModuleOrder(modules)
	checkErrs(errs)

	errs = loader.RunSemanticPipeline(modules, analyser, order, args.verbose, args.debug)
	checkErrs(errs)

	if args.verbose {
		fmt.Println("semantic analysis completed successfully")
	}

	irModules, errs := loader.GenerateIRModules(modules, order, args.verbose)
	checkErrs(errs)

	llvmOutputs := buildLLVMModules(irModules, order, args.outputType == OutputExecutable)

	if args.dumpIR {
		dumpIRModules(irModules)
	}

	if args.dumpLLVM {
		dumpLLVMModules(llvmOutputs, order)
	}

	if _, ok := modules["main"]; !ok {
		fmt.Println("main module not found")
		os.Exit(1)
	}

	foundMain := false
	mainModule := modules["main"]
	for _, root := range mainModule.Roots {
		for _, stmt := range root.Body {
			switch fn := stmt.(type) {
			case *parser.FunctionDefNode:
				if fn.Name == "main" {
					if len(fn.Args) != 0 {
						fmt.Println(shared.NewError(fn.Loc, "main function must not have arguments"))
						os.Exit(1)
					}
					if fn.Body == nil {
						fmt.Println(shared.NewError(fn.Loc, "main function must have a body"))
						os.Exit(1)
					}
					if fn.Symbol.Signature.ReturnType != types.PrimitiveVoid {
						fmt.Println(shared.NewError(fn.Loc, "main function must return void"))
					}
					foundMain = true
					break
				}
			}
		}
	}
	if !foundMain && args.outputType == OutputExecutable {
		fmt.Println("main function not found in main module")
		os.Exit(1)
	}

	buildDir, err := emitLLVMFiles(llvmOutputs, order)
	check(err)
	if !args.keepBuildDir {
		defer os.RemoveAll(buildDir)
	}

	if args.verbose {
		fmt.Printf("emitted LLVM files to %s\n", buildDir)
	}

	objFiles, err := compileLLVMModules(buildDir, order, args.optLevel, args.verbose, args.target, args.sysroot, args.ClangArgs)
	check(err)
	err = linkObjects(objFiles, args.output, args.outputType, args.static, args.verbose, args.target, args.sysroot, args.LinkArgs)
	check(err)

	if args.keepBuildDir {
		fmt.Printf("kept build directory: %s\n", buildDir)
	}
}

func parseFile(path string) (*parser.RootNode, error) {
	t, err := tokeniser.NewTokeniserFromFile(path)
	if err != nil {
		return nil, err
	}

	toks, err := t.Tokenise()
	if err != nil {
		return nil, err
	}

	p := parser.NewParser(toks)

	return p.Parse()
}

func collectSourceFiles(paths []string, exclude []string) ([]string, error) {
	seen := map[string]struct{}{}
	excluded := map[string]struct{}{}

	for _, e := range exclude {
		abs, _ := filepath.Abs(e)
		excluded[abs] = struct{}{}
	}

	var files []string

	for _, root := range paths {
		info, err := os.Stat(root)
		if err != nil || !info.IsDir() {
			continue
		}

		filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
			if err != nil {
				return err
			}

			abs, _ := filepath.Abs(path)

			for ex := range excluded {
				if strings.HasPrefix(abs, ex) {
					if d.IsDir() {
						return filepath.SkipDir
					}
					return nil
				}
			}

			if !d.IsDir() && filepath.Ext(path) == ".qk" {
				if _, ok := seen[abs]; !ok {
					seen[abs] = struct{}{}
					files = append(files, abs)
				}
			}

			return nil
		})
	}

	return files, nil
}

func buildSearchPaths(baseDir string) []string {
	var paths []string

	paths = append(paths, baseDir)

	home, err := os.UserHomeDir()
	if err == nil {
		paths = append(paths,
			filepath.Join(home, ".local", "share", "qk"),
		)
	}

	paths = append(paths, filepath.Join("/usr", "local", "lib", "qk"))

	paths = append(paths, filepath.Join("/usr", "lib", "qk"))

	return paths
}

func check(err error) {
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func checkErrs(errs []error) {
	if len(errs) > 0 {
		for _, err := range errs {
			fmt.Println(err)
		}
		os.Exit(1)
	}
}

func buildLLVMModules(mods map[string]*ir.Module, order []string, isExecutable bool) map[string]string {
	outputs := make(map[string]string, len(mods))

	for _, name := range order {
		mod := mods[name]
		if mod == nil {
			continue
		}

		emitter := &llvm.Emitter{ModuleName: name, Executable: isExecutable}
		var output strings.Builder
		emitter.EmitModule(&output, mod)
		outputs[name] = output.String()
	}

	return outputs
}

func dumpLLVMModules(mods map[string]string, order []string) {
	for i, name := range order {
		output, ok := mods[name]
		if !ok {
			continue
		}

		if i > 0 {
			fmt.Println()
		}
		fmt.Print(output)
		if !strings.HasSuffix(output, "\n") {
			fmt.Println()
		}
	}
}

func emitLLVMFiles(mods map[string]string, order []string) (string, error) {
	buildDir, err := os.MkdirTemp("/tmp", "qk-build-")
	if err != nil {
		return "", err
	}

	for _, name := range order {
		output, ok := mods[name]
		if !ok {
			continue
		}

		llPath := filepath.Join(buildDir, safeModuleFileName(name)+".ll")
		if err := os.WriteFile(llPath, []byte(output), 0o644); err != nil {
			return "", err
		}
	}

	return buildDir, nil
}

func compileLLVMModules(buildDir string, order []string, optLevel OptimisationLevel, verbose bool, target string, sysroot string, extraClangArgs []string) ([]string, error) {
	objFiles := make([]string, 0, len(order))

	for _, name := range order {
		llPath := filepath.Join(buildDir, safeModuleFileName(name)+".ll")
		if _, err := os.Stat(llPath); err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, err
		}

		objPath := filepath.Join(buildDir, safeModuleFileName(name)+".o")
		clangArgs := []string{"-c", llPath, "-o", objPath, fmt.Sprintf("-O%s", optLevel)}
		if target != "" {
			clangArgs = append([]string{"-target", target}, clangArgs...)
		}
		if sysroot != "" {
			clangArgs = append(clangArgs, "--sysroot="+sysroot)
		}
		if len(extraClangArgs) > 0 {
			clangArgs = append(clangArgs, extraClangArgs...)
		}

		if verbose {
			fmt.Printf("> clang %s\n", strings.Join(clangArgs, " "))
		}
		cmd := exec.Command("clang", clangArgs...)
		out, err := cmd.CombinedOutput()
		if err != nil {
			return nil, fmt.Errorf("clang failed for module %q: %w\n%s", name, err, string(out))
		}

		objFiles = append(objFiles, objPath)
	}

	if len(objFiles) == 0 {
		return nil, fmt.Errorf("no object files were produced")
	}

	return objFiles, nil
}

func linkObjects(objFiles []string, output string, outputType OutputType, static, verbose bool, target string, sysroot string, extraLinkArgs []string) error {
	args, err := buildLinkArgs(objFiles, output, outputType, static, target, sysroot, extraLinkArgs)
	if err != nil {
		return err
	}

	if verbose {
		fmt.Printf("> clang %s\n", strings.Join(args, " "))
	}
	cmd := exec.Command("clang", args...)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("linking failed: %w\n%s", err, string(out))
	}

	return nil
}

func buildLinkArgs(objFiles []string, output string, outputType OutputType, static bool, target string, sysroot string, extraLinkArgs []string) ([]string, error) {
	args := append([]string{}, objFiles...)

	switch outputType {
	case OutputExecutable:
	case OutputObject:
		args = append(args, "-r")
	case OutputSharedLib:
		if static {
			return nil, fmt.Errorf("cannot use --static with shared lib output")
		}
		args = append(args, "-shared", "-fPIC")
	default:
		return nil, fmt.Errorf("unknown output type")
	}

	if static {
		args = append(args, "-static")
	}

	if sysroot != "" {
		args = append([]string{"--sysroot=" + sysroot}, args...)
	}

	if target != "" {
		args = append([]string{"-target", target}, args...)
	}

	if len(extraLinkArgs) > 0 {
		args = append(args, extraLinkArgs...)
	}

	args = append(args, "-o", output)
	args = append(args, "-fuse-ld=lld")

	return args, nil
}

func safeModuleFileName(name string) string {
	if name == "" {
		return "module"
	}

	var b strings.Builder
	b.Grow(len(name))
	for i, r := range name {
		valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || (r >= '0' && r <= '9')
		if !valid {
			b.WriteByte('_')
			continue
		}
		if i == 0 && r >= '0' && r <= '9' {
			b.WriteByte('_')
		}
		b.WriteRune(r)
	}

	if b.Len() == 0 {
		return "module"
	}

	return b.String()
}