package linter

import (
	"alpinelinux.org/go/atools/apkbuild"
	"mvdan.cc/sh/v3/syntax"
)

var (
	AL64 LintingViolation = LintingViolation{
		Code:        "AL64",
		Description: "'%s' is an unapproved bashism",
		Certainty:   Certain,
		Severity:    Important,
		Skip:        "UNAPPROVED_BASHISM",
	}
)

func init() {
	Register(&AL64, CheckUnapprovedBashism)
}

func VisitIfClause(ifClause *syntax.IfClause, f func(stmt *syntax.Stmt) bool) {
	VisitStatements(ifClause.Cond, f)
	VisitStatements(ifClause.Then, f)
	if ifClause.Else != nil {
		VisitIfClause(ifClause.Else, f)
	}
}

// VisitStatements recusively iterates over statements and calls the provided
// function f for each statement. If the function returns `true`, the visitor
// will not recurse deeper into the current node.
func VisitStatements(stmts []*syntax.Stmt, f func(stmt *syntax.Stmt) bool) {
	for _, stmt := range stmts {
		if f(stmt) {
			continue
		}
		switch cmd := stmt.Cmd.(type) {
		case *syntax.ArithmCmd:
		case *syntax.BinaryCmd:
			VisitStatements([]*syntax.Stmt{cmd.X, cmd.Y}, f)
		case *syntax.Block:
			VisitStatements(cmd.Stmts, f)
		case *syntax.CallExpr:
		case *syntax.CaseClause:
			for _, item := range cmd.Items {
				VisitStatements(item.Stmts, f)
			}
		case *syntax.CoprocClause:
			VisitStatements([]*syntax.Stmt{cmd.Stmt}, f)
		case *syntax.DeclClause:
		case *syntax.ForClause:
			VisitStatements(cmd.Do, f)
		case *syntax.FuncDecl:
			VisitStatements([]*syntax.Stmt{cmd.Body}, f)
		case *syntax.IfClause:
			VisitIfClause(cmd, f)
		case *syntax.LetClause:
		case *syntax.TestClause:
		case *syntax.TimeClause:
			VisitStatements([]*syntax.Stmt{cmd.Stmt}, f)
		case *syntax.WhileClause:
			VisitStatements(cmd.Cond, f)
			VisitStatements(cmd.Do, f)
		}
	}
}

func CheckUnapprovedBashism(apkbld *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	violations = []LintingViolation{}
	VisitStatements(apkbld.File.Stmts, func(stmt *syntax.Stmt) bool {
		switch cmd := stmt.Cmd.(type) {
		case *syntax.TestClause:
			violations = append(violations, AL64.Instantiate(
				apkbld.File.Name,
				stmt.Cmd.Pos().Line(),
				stmt.Cmd.Pos().Col(),
				"[[",
			))
		case *syntax.CallExpr:
			call := apkbuild.ParseCallExpr(cmd)
			switch call.Command {
			case "[":
				// TODO: actual parse the arguments, this will catch '==' anywhere.
				for _, arg := range call.Args {
					if arg == "==" {
						violations = append(violations, AL64.Instantiate(
							apkbld.File.Name,
							stmt.Cmd.Pos().Line(),
							stmt.Cmd.Pos().Col(),
							"==",
						))
					}
				}
			case "read":
				for _, arg := range call.Args {
					if arg == "-p" {
						violations = append(violations, AL64.Instantiate(
							apkbld.File.Name,
							stmt.Cmd.Pos().Line(),
							stmt.Cmd.Pos().Col(),
							"read -p",
						))
					}
					if arg == "--" {
						break
					}
				}
			case "type":
				for _, arg := range call.Args {
					if arg == "-p" {
						violations = append(violations, AL64.Instantiate(
							apkbld.File.Name,
							stmt.Cmd.Pos().Line(),
							stmt.Cmd.Pos().Col(),
							"type -p",
						))
					}
					if arg == "--" {
						break
					}
				}
			case "source":
				violations = append(violations, AL64.Instantiate(
					apkbld.File.Name,
					stmt.Cmd.Pos().Line(),
					stmt.Cmd.Pos().Col(),
					"source",
				))
			}

		}
		for _, redir := range stmt.Redirs {
			if redir.Op == syntax.RdrAll || redir.Op == syntax.AppAll {
				violations = append(violations, AL64.Instantiate(
					apkbld.File.Name,
					redir.OpPos.Line(),
					redir.OpPos.Col(),
					"&> / &>>",
				))
			}
		}
		return false
	})
	return violations
}
