import {
    ArrayBindingOrAssignmentElement,
    ArrayBindingOrAssignmentPattern,
    AssignmentPattern,
    BindingOrAssignmentElementTarget,
    BindingOrAssignmentPattern,
    Block,
    cast,
    ClassDeclaration,
    ConciseBody,
    Debug,
    Expression,
    FunctionDeclaration,
    getModifiers,
    getStartsOnNewLine,
    isArrayBindingPattern,
    isArrayLiteralExpression,
    isBindingElement,
    isBindingPattern,
    isBlock,
    isDefaultModifier,
    isExportModifier,
    isExpression,
    isIdentifier,
    isObjectBindingPattern,
    isObjectLiteralElementLike,
    isObjectLiteralExpression,
    map,
    NodeConverters,
    NodeFactory,
    notImplemented,
    ObjectBindingOrAssignmentElement,
    ObjectBindingOrAssignmentPattern,
    setOriginalNode,
    setStartsOnNewLine,
    setTextRange,
    SyntaxKind,
} from "../_namespaces/ts.js";

/** @internal */
export function createNodeConverters(factory: NodeFactory): NodeConverters {
    return {
        convertToFunctionBlock,
        convertToFunctionExpression,
        convertToClassExpression,
        convertToArrayAssignmentElement,
        convertToObjectAssignmentElement,
        convertToAssignmentPattern,
        convertToObjectAssignmentPattern,
        convertToArrayAssignmentPattern,
        convertToAssignmentElementTarget,
    };

    function convertToFunctionBlock(node: ConciseBody, multiLine?: boolean): Block {
        if (isBlock(node)) return node;
        const returnStatement = factory.createReturnStatement(node);
        setTextRange(returnStatement, node);
        const body = factory.createBlock([returnStatement], multiLine);
        setTextRange(body, node);
        return body;
    }

    function convertToFunctionExpression(node: FunctionDeclaration) {
        if (!node.body) return Debug.fail(`Cannot convert a FunctionDeclaration without a body`);
        const updated = factory.createFunctionExpression(
            getModifiers(node)?.filter(modifier => !isExportModifier(modifier) && !isDefaultModifier(modifier)),
            node.asteriskToken,
            node.name,
            node.typeParameters,
            node.parameters,
            node.type,
            node.body,
        );
        setOriginalNode(updated, node);
        setTextRange(updated, node);
        if (getStartsOnNewLine(node)) {
            setStartsOnNewLine(updated, /*newLine*/ true);
        }
        return updated;
    }

    function convertToClassExpression(node: ClassDeclaration) {
        const updated = factory.createClassExpression(
            node.modifiers?.filter(modifier => !isExportModifier(modifier) && !isDefaultModifier(modifier)),
            node.name,
            node.typeParameters,
            node.heritageClauses,
            node.members,
        );
        setOriginalNode(updated, node);
        setTextRange(updated, node);
        if (getStartsOnNewLine(node)) {
            setStartsOnNewLine(updated, /*newLine*/ true);
        }
        return updated;
    }

    function convertToArrayAssignmentElement(element: ArrayBindingOrAssignmentElement) {
        if (isBindingElement(element)) {
            if (element.dotDotDotToken) {
                Debug.assertNode(element.name, isIdentifier);
                return setOriginalNode(setTextRange(factory.createSpreadElement(element.name), element), element);
            }
            const expression = convertToAssignmentElementTarget(element.name);
            return element.initializer
                ? setOriginalNode(
                    setTextRange(
                        factory.createAssignment(expression, element.initializer),
                        element,
                    ),
                    element,
                )
                : expression;
        }
        return cast(element, isExpression);
    }

    function convertToObjectAssignmentElement(element: ObjectBindingOrAssignmentElement) {
        if (isBindingElement(element)) {
            if (element.dotDotDotToken) {
                Debug.assertNode(element.name, isIdentifier);
                return setOriginalNode(setTextRange(factory.createSpreadAssignment(element.name), element), element);
            }
            if (element.propertyName) {
                const expression = convertToAssignmentElementTarget(element.name);
                return setOriginalNode(setTextRange(factory.createPropertyAssignment(element.propertyName, element.initializer ? factory.createAssignment(expression, element.initializer) : expression), element), element);
            }
            Debug.assertNode(element.name, isIdentifier);
            return setOriginalNode(setTextRange(factory.createShorthandPropertyAssignment(element.name, element.initializer), element), element);
        }

        return cast(element, isObjectLiteralElementLike);
    }

    function convertToAssignmentPattern(node: BindingOrAssignmentPattern): AssignmentPattern {
        switch (node.kind) {
            case SyntaxKind.ArrayBindingPattern:
            case SyntaxKind.ArrayLiteralExpression:
                return convertToArrayAssignmentPattern(node);

            case SyntaxKind.ObjectBindingPattern:
            case SyntaxKind.ObjectLiteralExpression:
                return convertToObjectAssignmentPattern(node);
        }
    }

    function convertToObjectAssignmentPattern(node: ObjectBindingOrAssignmentPattern) {
        if (isObjectBindingPattern(node)) {
            return setOriginalNode(
                setTextRange(
                    factory.createObjectLiteralExpression(map(node.elements, convertToObjectAssignmentElement)),
                    node,
                ),
                node,
            );
        }
        return cast(node, isObjectLiteralExpression);
    }

    function convertToArrayAssignmentPattern(node: ArrayBindingOrAssignmentPattern) {
        if (isArrayBindingPattern(node)) {
            return setOriginalNode(
                setTextRange(
                    factory.createArrayLiteralExpression(map(node.elements, convertToArrayAssignmentElement)),
                    node,
                ),
                node,
            );
        }
        return cast(node, isArrayLiteralExpression);
    }

    function convertToAssignmentElementTarget(node: BindingOrAssignmentElementTarget): Expression {
        if (isBindingPattern(node)) {
            return convertToAssignmentPattern(node);
        }

        return cast(node, isExpression);
    }
}

/** @internal */
export const nullNodeConverters: NodeConverters = {
    convertToFunctionBlock: notImplemented,
    convertToFunctionExpression: notImplemented,
    convertToClassExpression: notImplemented,
    convertToArrayAssignmentElement: notImplemented,
    convertToObjectAssignmentElement: notImplemented,
    convertToAssignmentPattern: notImplemented,
    convertToObjectAssignmentPattern: notImplemented,
    convertToArrayAssignmentPattern: notImplemented,
    convertToAssignmentElementTarget: notImplemented,
};
