package click.kamil.springstatemachineexporter.analysis.index;

import org.eclipse.jdt.core.dom.*;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;

/**
 * Detects trivial JavaBean-style getters and setters whose bodies only read or write a single field.
 */
public class TrivialAccessorDetector {

    @FunctionalInterface
    public interface FieldLookup extends Function<String, Optional<String>> {
    }

    public Optional<AccessorSummary> detect(String ownerFqn, MethodDeclaration method, FieldLookup fieldLookup) {
        if (ownerFqn == null || method == null || method.getBody() == null || fieldLookup == null) {
            return Optional.empty();
        }

        String methodName = method.getName().getIdentifier();
        if (method.isConstructor()) {
            return Optional.empty();
        }

        Optional<String> getterField = propertyNameForGetter(methodName, method);
        if (getterField.isPresent()) {
            Optional<AccessorSummary> fieldGetter = detectGetter(ownerFqn, method, methodName, getterField.get(), fieldLookup);
            if (fieldGetter.isPresent()) {
                return fieldGetter;
            }
            return detectConstantGetter(ownerFqn, method, methodName);
        }

        Optional<String> setterField = propertyNameForSetter(methodName);
        if (setterField.isPresent() && !method.parameters().isEmpty()) {
            return detectSetter(ownerFqn, method, methodName, setterField.get(), fieldLookup);
        }

        return Optional.empty();
    }

    public Optional<AccessorSummary> detectRecordComponent(String ownerFqn, String componentName) {
        if (ownerFqn == null || componentName == null || componentName.isEmpty()) {
            return Optional.empty();
        }
        return Optional.of(new AccessorSummary(
                ownerFqn,
                componentName,
                AccessorKind.RECORD_COMPONENT,
                componentName,
                null,
                ownerFqn,
                false
        ));
    }

    private Optional<AccessorSummary> detectConstantGetter(
            String ownerFqn,
            MethodDeclaration method,
            String methodName) {
        List<?> statements = method.getBody().statements();
        if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
            return Optional.empty();
        }
        Expression expr = rs.getExpression();
        if (expr == null || !isConstantReturnExpression(expr)) {
            return Optional.empty();
        }
        return Optional.of(new AccessorSummary(
                ownerFqn,
                methodName,
                AccessorKind.CONSTANT_GETTER,
                null,
                null,
                ownerFqn,
                false
        ));
    }

    private boolean isConstantReturnExpression(Expression expr) {
        Expression current = unwrap(expr);
        if (current instanceof StringLiteral
                || current instanceof NumberLiteral
                || current instanceof BooleanLiteral
                || current instanceof CharacterLiteral) {
            return true;
        }
        if (current instanceof QualifiedName) {
            return !containsMethodCall(current);
        }
        if (current instanceof SimpleName) {
            return false;
        }
        if (current instanceof FieldAccess fa) {
            Expression receiver = fa.getExpression();
            return (receiver instanceof SimpleName || receiver instanceof QualifiedName)
                    && !containsMethodCall(current);
        }
        return false;
    }

    private Optional<AccessorSummary> detectGetter(
            String ownerFqn,
            MethodDeclaration method,
            String methodName,
            String fieldName,
            FieldLookup fieldLookup) {
        if (!isTrivialGetterBody(method.getBody(), fieldName)) {
            return Optional.empty();
        }
        Optional<String> declaringFqn = fieldLookup.apply(fieldName);
        if (declaringFqn.isEmpty()) {
            return Optional.empty();
        }

        AccessorKind kind = methodName.startsWith("is") ? AccessorKind.BOOLEAN_GETTER : AccessorKind.GETTER;
        return Optional.of(new AccessorSummary(
                ownerFqn,
                methodName,
                kind,
                fieldName,
                null,
                declaringFqn.get(),
                false
        ));
    }

    private Optional<AccessorSummary> detectSetter(
            String ownerFqn,
            MethodDeclaration method,
            String methodName,
            String fieldName,
            FieldLookup fieldLookup) {
        SingleVariableDeclaration param = (SingleVariableDeclaration) method.parameters().get(0);
        String paramName = param.getName().getIdentifier();

        boolean fluent = isTrivialFluentSetterBody(method.getBody(), fieldName, paramName);
        boolean voidSetter = isVoidReturn(method) && isTrivialVoidSetterBody(method.getBody(), fieldName, paramName);
        if (!fluent && !voidSetter) {
            return Optional.empty();
        }

        Optional<String> declaringFqn = fieldLookup.apply(fieldName);
        if (declaringFqn.isEmpty()) {
            return Optional.empty();
        }

        AccessorKind kind = fluent ? AccessorKind.FLUENT_SETTER : AccessorKind.SETTER;
        return Optional.of(new AccessorSummary(
                ownerFqn,
                methodName,
                kind,
                fieldName,
                paramName,
                declaringFqn.get(),
                fluent
        ));
    }

    private Optional<String> propertyNameForGetter(String methodName, MethodDeclaration method) {
        if (methodName.startsWith("get") && methodName.length() > 3) {
            return Optional.of(decapitalize(methodName.substring(3)));
        }
        if (methodName.startsWith("is") && methodName.length() > 2 && isBooleanReturn(method)) {
            return Optional.of(decapitalize(methodName.substring(2)));
        }
        return Optional.empty();
    }

    private Optional<String> propertyNameForSetter(String methodName) {
        if (methodName.startsWith("set") && methodName.length() > 3) {
            return Optional.of(decapitalize(methodName.substring(3)));
        }
        return Optional.empty();
    }

    private boolean isBooleanReturn(MethodDeclaration method) {
        return "boolean".equals(method.getReturnType2().toString())
                || "Boolean".equals(method.getReturnType2().toString());
    }

    private boolean isVoidReturn(MethodDeclaration method) {
        return method.getReturnType2() instanceof PrimitiveType pt && pt.getPrimitiveTypeCode() == PrimitiveType.VOID;
    }

    public static String decapitalize(String name) {
        if (name == null || name.isEmpty()) {
            return name;
        }
        if (name.length() > 1 && Character.isUpperCase(name.charAt(1))) {
            return name;
        }
        return Character.toLowerCase(name.charAt(0)) + name.substring(1);
    }

    private boolean isTrivialGetterBody(Block body, String fieldName) {
        List<?> statements = body.statements();
        if (statements.size() != 1 || !(statements.get(0) instanceof ReturnStatement rs)) {
            return false;
        }
        Expression expr = rs.getExpression();
        if (expr == null) {
            return false;
        }
        return isDirectFieldRead(unwrap(expr), fieldName);
    }

    private boolean isTrivialVoidSetterBody(Block body, String fieldName, String paramName) {
        List<?> statements = body.statements();
        if (statements.size() != 1) {
            return false;
        }
        return isAssignmentStatement(statements.get(0), fieldName, paramName);
    }

    private boolean isTrivialFluentSetterBody(Block body, String fieldName, String paramName) {
        List<?> statements = body.statements();
        if (statements.size() != 2) {
            return false;
        }
        if (!isAssignmentStatement(statements.get(0), fieldName, paramName)) {
            return false;
        }
        if (!(statements.get(1) instanceof ReturnStatement rs)) {
            return false;
        }
        Expression ret = unwrap(rs.getExpression());
        return ret instanceof ThisExpression;
    }

    private boolean isAssignmentStatement(Object statement, String fieldName, String paramName) {
        if (!(statement instanceof ExpressionStatement es)) {
            return false;
        }
        if (!(es.getExpression() instanceof Assignment assignment)) {
            return false;
        }
        if (assignment.getOperator() != Assignment.Operator.ASSIGN) {
            return false;
        }
        if (!isDirectFieldWrite(assignment.getLeftHandSide(), fieldName)) {
            return false;
        }
        Expression rhs = unwrap(assignment.getRightHandSide());
        return rhs instanceof SimpleName sn && sn.getIdentifier().equals(paramName);
    }

    private boolean isDirectFieldRead(Expression expr, String fieldName) {
        if (containsMethodCall(expr)) {
            return false;
        }
        if (expr instanceof SimpleName sn) {
            return sn.getIdentifier().equals(fieldName);
        }
        if (expr instanceof FieldAccess fa) {
            Expression receiver = fa.getExpression();
            if (receiver == null || receiver instanceof ThisExpression) {
                return fa.getName().getIdentifier().equals(fieldName);
            }
        }
        return false;
    }

    private boolean isDirectFieldWrite(Expression expr, String fieldName) {
        if (expr instanceof SimpleName sn) {
            return sn.getIdentifier().equals(fieldName);
        }
        if (expr instanceof FieldAccess fa) {
            Expression receiver = fa.getExpression();
            if (receiver == null || receiver instanceof ThisExpression) {
                return fa.getName().getIdentifier().equals(fieldName);
            }
        }
        return false;
    }

    private boolean containsMethodCall(Expression expr) {
        if (expr == null) {
            return false;
        }
        final boolean[] found = {false};
        expr.accept(new ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                found[0] = true;
                return false;
            }
        });
        return found[0];
    }

    private Expression unwrap(Expression expr) {
        Expression current = expr;
        while (current instanceof ParenthesizedExpression pe) {
            current = pe.getExpression();
        }
        while (current instanceof CastExpression ce) {
            current = ce.getExpression();
        }
        return current;
    }
}
