import { EType, Expr, Param, Stmt } from "./ast.ts";

export type VisitRes = "stop" | void;

export interface AstVisitor<Args extends unknown[] = []> {
    visitStmts?(stmts: Stmt[], ...args: Args): VisitRes;
    visitStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitErrorStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitImportStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitBreakStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitReturnStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitFnStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitLetStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitAssignStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitExprStmt?(stmt: Stmt, ...args: Args): VisitRes;
    visitExpr?(expr: Expr, ...args: Args): VisitRes;
    visitErrorExpr?(expr: Expr, ...args: Args): VisitRes;
    visitIntExpr?(expr: Expr, ...args: Args): VisitRes;
    visitStringExpr?(expr: Expr, ...args: Args): VisitRes;
    visitIdentExpr?(expr: Expr, ...args: Args): VisitRes;
    visitGroupExpr?(expr: Expr, ...args: Args): VisitRes;
    visitFieldExpr?(expr: Expr, ...args: Args): VisitRes;
    visitIndexExpr?(expr: Expr, ...args: Args): VisitRes;
    visitCallExpr?(expr: Expr, ...args: Args): VisitRes;
    visitPathExpr?(expr: Expr, ...args: Args): VisitRes;
    visitETypeArgsExpr?(expr: Expr, ...args: Args): VisitRes;
    visitUnaryExpr?(expr: Expr, ...args: Args): VisitRes;
    visitBinaryExpr?(expr: Expr, ...args: Args): VisitRes;
    visitIfExpr?(expr: Expr, ...args: Args): VisitRes;
    visitBoolExpr?(expr: Expr, ...args: Args): VisitRes;
    visitNullExpr?(expr: Expr, ...args: Args): VisitRes;
    visitLoopExpr?(expr: Expr, ...args: Args): VisitRes;
    visitWhileExpr?(expr: Expr, ...args: Args): VisitRes;
    visitForInExpr?(expr: Expr, ...args: Args): VisitRes;
    visitForExpr?(expr: Expr, ...args: Args): VisitRes;
    visitBlockExpr?(expr: Expr, ...args: Args): VisitRes;
    visitSymExpr?(expr: Expr, ...args: Args): VisitRes;
    visitParam?(param: Param, ...args: Args): VisitRes;
    visitEType?(etype: EType, ...args: Args): VisitRes;
    visitErrorEType?(etype: EType, ...args: Args): VisitRes;
    visitNullEType?(etype: EType, ...args: Args): VisitRes;
    visitIntEType?(etype: EType, ...args: Args): VisitRes;
    visitBoolEType?(etype: EType, ...args: Args): VisitRes;
    visitStringEType?(etype: EType, ...args: Args): VisitRes;
    visitIdentEType?(etype: EType, ...args: Args): VisitRes;
    visitSymEType?(etype: EType, ...args: Args): VisitRes;
    visitArrayEType?(etype: EType, ...args: Args): VisitRes;
    visitStructEType?(etype: EType, ...args: Args): VisitRes;
    visitAnno?(etype: EType, ...args: Args): VisitRes;
}

export function visitStmts<Args extends unknown[] = []>(
    stmts: Stmt[],
    v: AstVisitor<Args>,
    ...args: Args
) {
    if (v.visitStmts?.(stmts, ...args) === "stop") return;
    stmts.map((stmt) => visitStmt(stmt, v, ...args));
}

export function visitStmt<Args extends unknown[] = []>(
    stmt: Stmt,
    v: AstVisitor<Args>,
    ...args: Args
) {
    if (v.visitStmt?.(stmt, ...args) == "stop") return;
    switch (stmt.kind.type) {
        case "error":
            if (v.visitErrorStmt?.(stmt, ...args) == "stop") return;
            break;
        case "import":
            if (v.visitImportStmt?.(stmt, ...args) == "stop") return;
            visitExpr(stmt.kind.path, v, ...args);
            break;
        case "break":
            if (v.visitBreakStmt?.(stmt, ...args) == "stop") return;
            if (stmt.kind.expr) visitExpr(stmt.kind.expr, v, ...args);
            break;
        case "return":
            if (v.visitReturnStmt?.(stmt, ...args) == "stop") return;
            if (stmt.kind.expr) visitExpr(stmt.kind.expr, v, ...args);
            break;
        case "fn":
            if (v.visitFnStmt?.(stmt, ...args) == "stop") return;
            stmt.kind.params.map((param) => visitParam(param, v, ...args));
            if (stmt.kind.returnType) {
                visitEType(stmt.kind.returnType, v, ...args);
            }
            visitExpr(stmt.kind.body, v, ...args);
            break;
        case "let":
            if (v.visitLetStmt?.(stmt, ...args) == "stop") return;
            visitParam(stmt.kind.param, v, ...args);
            visitExpr(stmt.kind.value, v, ...args);
            break;
        case "assign":
            if (v.visitAssignStmt?.(stmt, ...args) == "stop") return;
            visitExpr(stmt.kind.subject, v, ...args);
            visitExpr(stmt.kind.value, v, ...args);
            break;
        case "expr":
            if (v.visitExprStmt?.(stmt, ...args) == "stop") return;
            visitExpr(stmt.kind.expr, v, ...args);
            break;
        default:
            throw new Error(
                `statement '${
                    (stmt.kind as { type: string }).type
                }' not implemented`,
            );
    }
}

export function visitExpr<Args extends unknown[] = []>(
    expr: Expr,
    v: AstVisitor<Args>,
    ...args: Args
) {
    if (v.visitExpr?.(expr, ...args) == "stop") return;
    switch (expr.kind.type) {
        case "error":
            if (v.visitErrorExpr?.(expr, ...args) == "stop") return;
            break;
        case "string":
            if (v.visitStringExpr?.(expr, ...args) == "stop") return;
            break;
        case "int":
            if (v.visitIntExpr?.(expr, ...args) == "stop") return;
            break;
        case "ident":
            if (v.visitIdentExpr?.(expr, ...args) == "stop") return;
            break;
        case "group":
            if (v.visitGroupExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.expr, v, ...args);
            break;
        case "field":
            if (v.visitFieldExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            break;
        case "index":
            if (v.visitIndexExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            visitExpr(expr.kind.value, v, ...args);
            break;
        case "call":
            if (v.visitCallExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            expr.kind.args.map((arg) => visitExpr(arg, v, ...args));
            break;
        case "path":
            if (v.visitPathExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            break;
        case "etype_args":
            if (v.visitETypeArgsExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            expr.kind.etypeArgs.map((arg) => visitEType(arg, v, ...args));
            break;
        case "unary":
            if (v.visitUnaryExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.subject, v, ...args);
            break;
        case "binary":
            if (v.visitBinaryExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.left, v, ...args);
            visitExpr(expr.kind.right, v, ...args);
            break;
        case "if":
            if (v.visitIfExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.cond, v, ...args);
            visitExpr(expr.kind.truthy, v, ...args);
            if (expr.kind.falsy) visitExpr(expr.kind.falsy, v, ...args);
            break;
        case "bool":
            if (v.visitBoolExpr?.(expr, ...args) == "stop") return;
            break;
        case "null":
            if (v.visitNullExpr?.(expr, ...args) == "stop") return;
            break;
        case "loop":
            if (v.visitLoopExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.body, v, ...args);
            break;
        case "while":
            if (v.visitWhileExpr?.(expr, ...args) == "stop") return;
            visitExpr(expr.kind.cond, v, ...args);
            visitExpr(expr.kind.body, v, ...args);
            break;
        case "for_in":
            if (v.visitForInExpr?.(expr, ...args) == "stop") return;
            visitParam(expr.kind.param, v, ...args);
            visitExpr(expr.kind.value, v, ...args);
            visitExpr(expr.kind.body, v, ...args);
            break;
        case "for":
            if (v.visitForExpr?.(expr, ...args) == "stop") return;
            if (expr.kind.decl) visitStmt(expr.kind.decl, v, ...args);
            if (expr.kind.cond) visitExpr(expr.kind.cond, v, ...args);
            if (expr.kind.incr) visitStmt(expr.kind.incr, v, ...args);
            visitExpr(expr.kind.body, v, ...args);
            break;
        case "block":
            if (v.visitBlockExpr?.(expr, ...args) == "stop") return;
            expr.kind.stmts.map((stmt) => visitStmt(stmt, v, ...args));
            if (expr.kind.expr) visitExpr(expr.kind.expr, v, ...args);
            break;
        case "sym":
            if (v.visitSymExpr?.(expr, ...args) == "stop") return;
            break;
        default:
            throw new Error(
                `expression '${
                    (expr.kind as { type: string }).type
                }' not implemented`,
            );
    }
}

export function visitParam<Args extends unknown[] = []>(
    param: Param,
    v: AstVisitor<Args>,
    ...args: Args
) {
    if (v.visitParam?.(param, ...args) == "stop") return;
    if (param.etype) visitEType(param.etype, v, ...args);
}

export function visitEType<Args extends unknown[] = []>(
    etype: EType,
    v: AstVisitor<Args>,
    ...args: Args
) {
    if (v.visitEType?.(etype, ...args) == "stop") return;
    switch (etype.kind.type) {
        case "error":
            if (v.visitErrorEType?.(etype, ...args) == "stop") return;
            break;
        case "string":
            if (v.visitStringEType?.(etype, ...args) == "stop") return;
            break;
        case "null":
            if (v.visitNullEType?.(etype, ...args) == "stop") return;
            break;
        case "int":
            if (v.visitIntEType?.(etype, ...args) == "stop") return;
            break;
        case "bool":
            if (v.visitBoolEType?.(etype, ...args) == "stop") return;
            break;
        case "ident":
            if (v.visitIdentEType?.(etype, ...args) == "stop") return;
            break;
        case "sym":
            if (v.visitSymEType?.(etype, ...args) == "stop") return;
            break;
        case "array":
            if (v.visitArrayEType?.(etype, ...args) == "stop") return;
            if (etype.kind.inner) visitEType(etype.kind.inner, v, ...args);
            break;
        case "struct":
            if (v.visitStructEType?.(etype, ...args) == "stop") return;
            etype.kind.fields.map((field) => visitParam(field, v, ...args));
            break;
        default:
            throw new Error(
                `etype '${
                    (etype.kind as { type: string }).type
                }' not implemented`,
            );
    }
}

export function stmtToString(stmt: Stmt): string {
    const body = (() => {
        switch (stmt.kind.type) {
            case "assign":
                return `{ subject: ${exprToString(stmt.kind.subject)}, value: ${
                    exprToString(stmt.kind.value)
                } }`;
        }
        return "(<not implemented>)";
    })();
    const { line } = stmt.pos;
    return `${stmt.kind.type}:${line}${body}`;
}

export function exprToString(expr: Expr): string {
    const body = (() => {
        switch (expr.kind.type) {
            case "binary":
                return `(${
                    exprToString(expr.kind.left)
                } ${expr.kind.binaryType} ${exprToString(expr.kind.right)})`;
            case "sym":
                return `(${expr.kind.ident})`;
        }
        return "(<not implemented>)";
    })();
    const { line } = expr.pos;
    return `${expr.kind.type}:${line}${body}`;
}
