package apkbuild

import (
	"bytes"
	"io"
	"iter"
	"strings"

	"github.com/moznion/go-optional"
	"mvdan.cc/sh/v3/expand"
	"mvdan.cc/sh/v3/syntax"
)

type Apkbuild struct {
	File            *syntax.File
	GlobalVariables VariableScope
	Functions       []Function
	Lines           []string
	Heredocs        []*syntax.Word
}

func (a Apkbuild) GetGlobal(name string) string {
	return a.GlobalVariables.GetValue(name)
}

func Parse(f io.Reader, filename string) (apkbuild *Apkbuild, err error) {
	buf := bytes.Buffer{}
	teeReader := io.TeeReader(f, &buf)

	parsedFile, err := syntax.NewParser(
		syntax.Variant(syntax.LangBash),
	).Parse(teeReader, filename)
	if err != nil {
		return nil, err
	}

	apkbuild = &Apkbuild{
		File:  parsedFile,
		Lines: strings.Split(buf.String(), "\n"),
	}

	apkbuild.GlobalVariables = parseAssignments(parsedFile.Stmts)
	apkbuild.Functions = parseFunctions(parsedFile.Stmts)
	apkbuild.Heredocs = ParseHeredocs(parsedFile.Stmts)

	return
}

// IterAssigns will call the provided function for each assignment in the file,
// both in the global scope, as in each function.
func (a *Apkbuild) IterAssigns(iterFunc func(Variable)) {
	a.GlobalVariables.Iter(iterFunc)
	for _, f := range a.Functions {
		f.Variables.Iter(iterFunc)
	}
}

func (a *Apkbuild) AssignsIterator() iter.Seq[Variable] {
	return func(yield func(Variable) bool) {
		for _, assign := range a.GlobalVariables {
			if !yield(assign) {
				return
			}
		}
	}
}

func (a *Apkbuild) FuncIterator() iter.Seq[Function] {
	return func(yield func(Function) bool) {
		for _, f := range a.Functions {
			if !yield(f) {
				return
			}
		}
	}
}

func (a *Apkbuild) StmtIterator() iter.Seq[*syntax.Stmt] {
	return func(yield func(*syntax.Stmt) bool) {
		for _, stmt := range a.File.Stmts {
			if !yield(stmt) {
				return
			}
		}

		for _, f := range a.Functions {
			for _, stmt := range f.Stmts {
				if !yield(stmt) {
					return
				}
			}
		}
	}
}

// parseAssignments takes a list of statements and extracts all variable
// assignments out of it. It recurses the AST, but it will not go into
// functions or subshells, which means it's limited to a single scope.
func parseAssignments(stmts []*syntax.Stmt) (variables VariableScope) {
	for _, stmt := range stmts {
		switch cmd := stmt.Cmd.(type) {
		case *syntax.CallExpr:
			if cmd.Assigns != nil {
				if len(cmd.Args) > 0 {
					// This is a command execution, not a variable assignment
					continue
				}
				for _, assignment := range cmd.Assigns {
					variable := parseAssignment(assignment, variables)
					variables = append(variables, variable)
				}
			}
		case *syntax.DeclClause:
			if cmd.Variant.Value != "export" && cmd.Variant.Value != "local" {
				continue
			}

			if cmd.Args != nil {
				for _, assignment := range cmd.Args {
					variable := parseAssignment(assignment, variables)
					variable.Exported = cmd.Variant.Value == "export"
					variable.Local = cmd.Variant.Value == "local"
					variables = append(variables, variable)
				}
			}
		case *syntax.IfClause:
			variables = append(variables, parseIfClause(cmd, parseAssignments)...)
		case *syntax.CaseClause:
			for _, caseItem := range cmd.Items {
				variables = append(variables, parseAssignments(caseItem.Stmts)...)
			}
		case *syntax.ForClause:
			variables = append(variables, parseAssignments(cmd.Do)...)
		case *syntax.WhileClause:
			variables = append(variables, parseAssignments(cmd.Do)...)
		case *syntax.Block:
			variables = append(variables, parseAssignments(cmd.Stmts)...)
		}
	}
	return
}

func parseIfClause[T ~[]E, E any](ifClause *syntax.IfClause, f func([]*syntax.Stmt) T) (accumulator T) {
	accumulator = append(accumulator, f(ifClause.Then)...)
	if ifClause.Else != nil {
		accumulator = append(accumulator, parseIfClause(ifClause.Else, f)...)
	}
	return
}

func parseAssignment(assignment *syntax.Assign, variables VariableScope) (v Variable) {
	var static, evaluated string
	var cmdsubsts []*syntax.CmdSubst
	if assignment.Value == nil {
		static = ""
		evaluated = ""
	} else {
		static, evaluated = parseWordParts(assignment.Value.Parts, variables)
		cmdsubsts = extractCmdSubst(assignment.Value.Parts)
	}

	v = Variable{
		Assignment: assignment,
		// TODO: properly evaluate value to obtain actual name
		Name: optional.MapOr(
			optional.PtrFromNillable(assignment.Name),
			"",
			func(v *syntax.Lit) string { return v.Value },
		),
		Value:          static,
		EvaluatedValue: evaluated,
		CommandSubst:   cmdsubsts,
	}
	return
}

func parseWordParts(parts []syntax.WordPart, variables VariableScope) (static string, parsed string) {
	var staticBuilder strings.Builder

	printer := syntax.NewPrinter()
	expandConfig := expand.Config{
		Env: Environ{VariableScope: variables},
		CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error {
			// We cannot evaluate command substitutions, just return them verbatim
			return printer.Print(w, cs)
		},
	}

	for _, part := range parts {
		printer.Print(&staticBuilder, part)
	}
	expanded, _ := expand.Document(&expandConfig, &syntax.Word{
		Parts: parts,
	})

	static = staticBuilder.String()
	if len(static) > 0 && static[0] == '"' {
		static = static[1:]
	}
	if len(static) > 0 && static[len(static)-1] == '"' {
		static = static[:len(static)-1]
	}

	return static, expanded

}

func extractCmdSubst(parts []syntax.WordPart) (cmdsubsts []*syntax.CmdSubst) {
	for _, part := range parts {
		switch subpart := part.(type) {
		case *syntax.CmdSubst:
			cmdsubsts = append(cmdsubsts, subpart)
		case *syntax.DblQuoted:
			cmdsubsts = append(cmdsubsts, extractCmdSubst(subpart.Parts)...)
		}
	}
	return
}

type Function struct {
	Name      string
	FuncDecl  *syntax.FuncDecl
	Stmts     []*syntax.Stmt
	Variables VariableScope
}

// StmtsIterator returns an Iterator that returns each top-level statements in
// this function.
func (f Function) StmtsIterator() iter.Seq[*syntax.Stmt] {
	return func(yield func(*syntax.Stmt) bool) {
		for _, stmt := range f.Stmts {
			if !yield(stmt) {
				return
			}
		}
	}
}

func parseFunctions(stmts []*syntax.Stmt) (functions []Function) {
	for _, stmt := range stmts {
		switch cmd := stmt.Cmd.(type) {
		case *syntax.FuncDecl:
			function := Function{
				Name:     cmd.Name.Value,
				FuncDecl: cmd,
			}

			if body, ok := cmd.Body.Cmd.(*syntax.Block); ok {
				function.Variables = parseAssignments(body.Stmts)
				function.Stmts = body.Stmts
			}

			functions = append(functions, function)
		}
	}
	return
}

func MakeFile(program string) (f io.Reader) {
	f = strings.NewReader(program)
	return
}

type Call struct {
	Command string
	Args    []string
	Expr    *syntax.CallExpr
}

func ParseCallExpr(expr *syntax.CallExpr) (call Call) {
	call.Expr = expr
	for _, word := range expr.Args {
		static, _ := parseWordParts(word.Parts, VariableScope{})

		if call.Command == "" {
			call.Command = static
		} else {
			call.Args = append(call.Args, static)
		}
	}

	return
}

func ParseHeredocs(stmts []*syntax.Stmt) (heredocs []*syntax.Word) {
	for _, stmt := range stmts {
		for _, redir := range stmt.Redirs {
			if redir.Hdoc != nil {
				heredocs = append(heredocs, redir.Hdoc)
			}
		}
		switch cmd := stmt.Cmd.(type) {
		case *syntax.FuncDecl:
			funcCmd, ok := cmd.Body.Cmd.(*syntax.Block)
			if !ok {
				continue
			}
			heredocs = append(heredocs, ParseHeredocs(funcCmd.Stmts)...)
		case *syntax.ForClause:
			heredocs = append(heredocs, ParseHeredocs(cmd.Do)...)
		case *syntax.WhileClause:
			heredocs = append(heredocs, ParseHeredocs(cmd.Do)...)
		case *syntax.IfClause:
			heredocs = append(heredocs, parseIfClause(cmd, ParseHeredocs)...)
		case *syntax.CaseClause:
		}
	}
	return
}
