// Refactor element.style.XXXX = YYYY to element.setCssStyles({ XXXX: YYYY }).
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-styles.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node, Expression } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";

const isDryRun = !Deno.args.includes("--run");

if (isDryRun) {
    console.log("=== DRY RUN MODE ===");
    console.log(
        "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-styles.ts --run\n"
    );
} else {
    console.log("=== RUN MODE: WILL MODIFY FILES ===");
}

const project = new Project({ tsConfigFilePath: "../tsconfig.json" });

// Manually add files under src/ to ensure those excluded by tsconfig.json are processed if needed.
project.addSourceFilesAtPaths("../src/**/*.ts");
project.addSourceFilesAtPaths("../src/**/*.svelte");

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");

function toPosixPath(filePath: string): string {
    return filePath.replace(/\\/g, "/");
}

const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;

function matchStyleAccess(node: Node): { element: Node; propertyName: string; isComputed: boolean } | undefined {
    if (Node.isPropertyAccessExpression(node)) {
        const expr = node.getExpression();
        if (Node.isPropertyAccessExpression(expr) && expr.getName() === "style") {
            return {
                element: expr.getExpression(),
                propertyName: node.getName(),
                isComputed: false,
            };
        }
    } else if (Node.isElementAccessExpression(node)) {
        const expr = node.getExpression();
        if (Node.isPropertyAccessExpression(expr) && expr.getName() === "style") {
            const arg = node.getArgumentExpression();
            if (arg) {
                return {
                    element: expr.getExpression(),
                    propertyName: arg.getText(),
                    isComputed: true,
                };
            }
        }
    }
    return undefined;
}

function getStyleAssignment(statement: Node) {
    if (!Node.isExpressionStatement(statement)) return undefined;
    const expr = statement.getExpression();
    if (!Node.isBinaryExpression(expr)) return undefined;
    if (expr.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) return undefined;

    const styleAccess = matchStyleAccess(expr.getLeft());
    if (!styleAccess) return undefined;

    return {
        elementText: styleAccess.element.getText(),
        property: styleAccess.propertyName,
        valueText: expr.getRight().getText(),
        isComputed: styleAccess.isComputed,
        statementNode: statement,
    };
}

interface StyleGroup {
    elementText: string;
    assignments: {
        property: string;
        valueText: string;
        isComputed: boolean;
        statementNode: Node;
    }[];
}

let modifiedFilesCount = 0;

for (const sourceFile of project.getSourceFiles()) {
    const filePath = sourceFile.getFilePath();
    const posixFilePath = toPosixPath(filePath);

    // Only process files inside the project src directory.
    if (!posixFilePath.startsWith(posixSrc)) {
        continue;
    }

    // Exclude unit and integration test files
    if (
        posixFilePath.endsWith(".spec.ts") ||
        posixFilePath.endsWith(".test.ts") ||
        posixFilePath.includes("/_test/") ||
        posixFilePath.includes("/testdeno/")
    ) {
        continue;
    }

    // Collect all blocks, case clauses, and the source file itself
    const containers = [
        sourceFile,
        ...sourceFile.getDescendantsOfKind(SyntaxKind.Block),
        ...sourceFile.getDescendantsOfKind(SyntaxKind.CaseClause),
        ...sourceFile.getDescendantsOfKind(SyntaxKind.DefaultClause),
    ];

    const fileGroups: StyleGroup[] = [];

    for (const container of containers) {
        const statements = container.getStatements();
        let i = 0;
        while (i < statements.length) {
            const assignment = getStyleAssignment(statements[i]);
            if (assignment) {
                const currentGroup: StyleGroup = {
                    elementText: assignment.elementText,
                    assignments: [
                        {
                            property: assignment.property,
                            valueText: assignment.valueText,
                            isComputed: assignment.isComputed,
                            statementNode: assignment.statementNode,
                        },
                    ],
                };

                // Look ahead to collect consecutive assignments to the same element
                let j = i + 1;
                while (j < statements.length) {
                    const nextAssignment = getStyleAssignment(statements[j]);
                    if (nextAssignment && nextAssignment.elementText === assignment.elementText) {
                        currentGroup.assignments.push({
                            property: nextAssignment.property,
                            valueText: nextAssignment.valueText,
                            isComputed: nextAssignment.isComputed,
                            statementNode: nextAssignment.statementNode,
                        });
                        j++;
                    } else {
                        break;
                    }
                }
                fileGroups.push(currentGroup);
                i = j;
            } else {
                i++;
            }
        }
    }

    if (fileGroups.length > 0) {
        console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);

        // Process groups in reverse order to keep Node references valid when removing
        const reversedGroups = [...fileGroups].reverse();

        for (const group of reversedGroups) {
            const props = group.assignments.map((c) => {
                if (c.isComputed) {
                    if (
                        (c.property.startsWith("'") && c.property.endsWith("'")) ||
                        (c.property.startsWith('"') && c.property.endsWith('"')) ||
                        (c.property.startsWith("`") && c.property.endsWith("`"))
                    ) {
                        return `${c.property}: ${c.valueText}`;
                    }
                    return `[${c.property}]: ${c.valueText}`;
                }
                return `${c.property}: ${c.valueText}`;
            });

            let newText = "";
            if (props.length === 1) {
                newText = `${group.elementText}.setCssStyles({ ${props[0]} });`;
            } else {
                newText = `${group.elementText}.setCssStyles({\n    ${props.join(",\n    ")}\n});`;
            }

            const firstNode = group.assignments[0].statementNode;
            const { line } = sourceFile.getLineAndColumnAtPos(firstNode.getStart());

            console.log(`  Line ${line}: Replacing consecutive style assignments on "${group.elementText}" with:`);
            console.log(
                newText
                    .split("\n")
                    .map((l) => `    ${l}`)
                    .join("\n")
            );

            if (!isDryRun) {
                firstNode.replaceWithText(newText);
                for (let k = 1; k < group.assignments.length; k++) {
                    group.assignments[k].statementNode.remove();
                }
            }
        }

        modifiedFilesCount++;
    }
}

console.log(`\nTotal files to modify: ${modifiedFilesCount}`);

if (!isDryRun) {
    project.saveSync();
    console.log("All changes successfully saved.");
} else {
    console.log("Dry run complete. No changes were written to files.");
}
