package p.studio.compiler.pbs.semantics;

import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.ExprResult;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.ExprUse;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Kind;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView;
import p.studio.compiler.source.diagnostics.Diagnostics;

final class PbsFlowBranchExpressionAnalyzer {
    @FunctionalInterface
    interface ExpressionAnalyzerDelegate {
        ExprResult analyze(PbsAst.Expression expression, PbsFlowExpressionContext context);
    }

    @FunctionalInterface
    interface BlockAnalysisDelegate {
        TypeView analyze(PbsAst.Block block, PbsFlowExpressionContext context, boolean valueContext);
    }

    private final PbsFlowTypeOps typeOps;
    private final ExpressionAnalyzerDelegate expressionAnalyzerDelegate;
    private final BlockAnalysisDelegate blockAnalysisDelegate;

    PbsFlowBranchExpressionAnalyzer(
            final PbsFlowTypeOps typeOps,
            final ExpressionAnalyzerDelegate expressionAnalyzerDelegate,
            final BlockAnalysisDelegate blockAnalysisDelegate) {
        this.typeOps = typeOps;
        this.expressionAnalyzerDelegate = expressionAnalyzerDelegate;
        this.blockAnalysisDelegate = blockAnalysisDelegate;
    }

    ExprResult analyzeIfExpression(
            final PbsAst.IfExpr ifExpr,
            final PbsFlowExpressionContext context) {
        final var diagnostics = context.diagnostics();
        final var expectedType = context.expectedType();
        final var condition = analyzeValueExpression(ifExpr.condition(), context, TypeView.bool()).type();
        if (!typeOps.isBool(condition)) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name(),
                    "If expression condition must have bool type",
                    ifExpr.condition().span());
        }

        final var thenType = blockAnalysisDelegate.analyze(ifExpr.thenBlock(), context, true);
        final var elseType = analyzeValueExpression(ifExpr.elseExpression(), context, expectedType).type();

        if (!typeOps.compatible(thenType, elseType)) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_IF_BRANCH_TYPE_MISMATCH.name(),
                    "If expression branches must have compatible types",
                    ifExpr.span());
            return ExprResult.type(TypeView.unknown());
        }
        return ExprResult.type(thenType);
    }

    ExprResult analyzeElseExpression(
            final PbsAst.ElseExpr elseExpr,
            final PbsFlowExpressionContext context) {
        final var diagnostics = context.diagnostics();
        final var optional = analyzeValueExpression(elseExpr.optionalExpression(), context, null).type();
        if (optional.kind() != Kind.OPTIONAL) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_ELSE_NON_OPTIONAL_LEFT.name(),
                    "Left operand of 'else' must have optional type",
                    elseExpr.optionalExpression().span());
            analyzeValueExpression(elseExpr.fallbackExpression(), context, null);
            return ExprResult.type(TypeView.unknown());
        }

        final var payload = optional.inner();
        final var fallback = analyzeValueExpression(elseExpr.fallbackExpression(), context, payload).type();
        if (!typeOps.compatible(fallback, payload)) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_ELSE_FALLBACK_TYPE_MISMATCH.name(),
                    "Fallback expression in 'else' must match optional payload type",
                    elseExpr.fallbackExpression().span());
        }
        return ExprResult.type(payload);
    }

    ExprResult analyzePropagateExpression(
            final PbsAst.PropagateExpr propagateExpr,
            final PbsFlowExpressionContext context) {
        final var diagnostics = context.diagnostics();
        final var resultErrorName = context.resultErrorName();
        final var source = analyzeValueExpression(propagateExpr.expression(), context, null).type();
        if (source.kind() != Kind.RESULT) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_NON_RESULT.name(),
                    "Propagation operator '!' requires result type",
                    propagateExpr.expression().span());
            return ExprResult.type(TypeView.unknown());
        }
        final var sourceError = source.errorType();
        if (resultErrorName == null || sourceError == null || !resultErrorName.equals(sourceError.name())) {
            Diagnostics.error(diagnostics,
                    PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH.name(),
                    "Propagation operator '!' requires matching result error type in enclosing callable",
                    propagateExpr.span());
        }
        return ExprResult.type(source.inner());
    }

    private ExprResult analyzeValueExpression(
            final PbsAst.Expression expression,
            final PbsFlowExpressionContext context,
            final TypeView expectedType) {
        return expressionAnalyzerDelegate.analyze(
                expression,
                context.withExpectedType(expectedType)
                        .withUse(ExprUse.VALUE)
                        .withValueContext(true));
    }
}
