package modules

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

	"github.com/marzeq/quokka/parser"
	"github.com/marzeq/quokka/tokeniser"
)

type FileNode struct {
	Path    string
	Imports []*FileNode
}

type DepGraph struct {
	Files map[string]*FileNode
	ASTs  map[string]*parser.RootNode
}

func NewDepGraph(path string, rn *parser.RootNode) (*DepGraph, error) {
	dg := &DepGraph{
		Files: make(map[string]*FileNode),
		ASTs:  make(map[string]*parser.RootNode),
	}

	err := dg.Construct(path, rn)
	if err != nil {
		return nil, err
	}

	return dg, nil
}

var moduleSearchPaths = []string{
	".",
	"/usr/share/quokka/lib",
	"/usr/local/share/quokka/lib",
	filepath.Join(os.Getenv("HOME"), ".local/share/quokka/lib"),
}

func (g *DepGraph) Construct(path string, rn *parser.RootNode) error {
	path, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	if _, ok := g.ASTs[path]; ok {
		return nil
	}

	node := &FileNode{
		Path:    path,
		Imports: []*FileNode{},
	}
	g.Files[path] = node
	g.ASTs[path] = rn

	modules := getImports(rn)
	for _, moduleName := range modules {
		files, err := findFilesForModule(moduleName)
		if err != nil {
			return err
		}

		modules := getImports(rn)
		for _, moduleName := range modules {
			files, err := findFilesForModule(moduleName)
			if err != nil {
				return err
			}

			if len(files) == 0 {
				return fmt.Errorf("module not found: %s", moduleName)
			}

			for _, f := range files {
				childNode, ok := g.Files[f]
				if !ok || childNode == nil {
					continue
				}
				node.Imports = append(node.Imports, childNode)
			}
		}

		for _, f := range files {
			t, err := tokeniser.NewTokeniserFromFile(f)
			if err != nil {
				return err
			}
			toks, err := t.Tokenise()
			if err != nil {
				return err
			}
			p := parser.NewParser(toks)
			ast, err := p.Parse()
			if err != nil {
				return err
			}

			firstModule := ""
			if len(ast.Body) > 0 {
				if mn, ok := ast.Body[0].(*parser.ModuleNode); ok {
					firstModule = mn.Name
				}
			}
			if firstModule != moduleName {
				continue
			}

			if err := g.Construct(f, ast); err != nil {
				return err
			}
			node.Imports = append(node.Imports, g.Files[f])
		}
	}

	return nil
}

func getImports(rn *parser.RootNode) []string {
	modules := []string{}
	for _, n := range rn.Body {
		if imp, ok := n.(*parser.ImportNode); ok {
			modules = append(modules, imp.Modules...)
		}
	}
	return modules
}

func findFilesForModule(moduleName string) ([]string, error) {
	var result []string
	seen := map[string]struct{}{}

	for _, dir := range moduleSearchPaths {
		err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if err != nil || info.IsDir() {
				return nil
			}
			if _, ok := seen[path]; ok {
				return nil
			}

			if filepath.Ext(path) != ".qk" {
				return nil
			}

			t, err := tokeniser.NewTokeniserFromFile(path)
			if err != nil {
				return nil
			}
			toks, err := t.Tokenise()
			if err != nil {
				return nil
			}
			p := parser.NewParser(toks)
			ast, err := p.Parse()
			if err != nil || len(ast.Body) == 0 {
				return nil
			}
			if mn, ok := ast.Body[0].(*parser.ModuleNode); ok && mn.Name == moduleName {
				result = append(result, path)
				seen[path] = struct{}{}
			}
			return nil
		})
		if err != nil {
			return nil, err
		}
	}

	return result, nil
}

func (g *DepGraph) TopoSort() ([]string, error) {
	visited := map[string]bool{}
	temp := map[string]bool{}
	result := []string{}

	var visit func(n *FileNode) error
	visit = func(n *FileNode) error {
		if n == nil {
			return nil
		}
		if temp[n.Path] {
			return fmt.Errorf("cycle detected at file: %s", n.Path)
		}
		if visited[n.Path] {
			return nil
		}

		temp[n.Path] = true
		for _, child := range n.Imports {
			if err := visit(child); err != nil {
				return err
			}
		}
		temp[n.Path] = false

		visited[n.Path] = true
		result = append(result, n.Path)
		return nil
	}

	for _, n := range g.Files {
		if !visited[n.Path] {
			if err := visit(n); err != nil {
				return nil, err
			}
		}
	}

	for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
		result[i], result[j] = result[j], result[i]
	}

	return result, nil
}