package p.studio.compiler.pbs.lowering;

import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.identifiers.CallableShapeId;
import p.studio.compiler.source.identifiers.TypeSurfaceId;
import p.studio.compiler.source.tables.CallableShapeTable;
import p.studio.compiler.source.tables.TypeSurfaceTable;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.ArrayList;

final class PbsCallableShapeSurfaceService {

    String callableShapeSurfaceOf(final PbsAst.FunctionDecl functionDecl) {
        final var typeSurfaceTable = new TypeSurfaceTable();
        final var callableShapeTable = new CallableShapeTable();
        final var callableShapeId = callableShapeId(functionDecl, typeSurfaceTable, callableShapeTable);
        return callableShapeSurface(callableShapeId, typeSurfaceTable, callableShapeTable);
    }

    CallableShapeId callableShapeId(
            final PbsAst.FunctionDecl functionDecl,
            final TypeSurfaceTable typeSurfaceTable,
            final CallableShapeTable callableShapeTable) {
        final var parameterSurfaceIds = new ArrayList<TypeSurfaceId>(functionDecl.parameters().size());
        for (final var parameter : functionDecl.parameters()) {
            parameterSurfaceIds.add(typeSurfaceTable.register(typeSurfaceKey(parameter.typeRef())));
        }
        final var outputSurfaceId = typeSurfaceTable.register(outputShapeSurface(
                functionDecl.returnKind(),
                functionDecl.returnType(),
                functionDecl.resultErrorType()));
        return callableShapeTable.register(ReadOnlyList.wrap(parameterSurfaceIds), outputSurfaceId);
    }

    String callableShapeSurface(
            final CallableShapeId callableShapeId,
            final TypeSurfaceTable typeSurfaceTable,
            final CallableShapeTable callableShapeTable) {
        final var shape = callableShapeTable.get(callableShapeId);
        final var builder = new StringBuilder();
        builder.append('(');
        for (var i = 0; i < shape.parameterTypeSurfaces().size(); i++) {
            if (i > 0) {
                builder.append(',');
            }
            builder.append(typeSurfaceTable.get(shape.parameterTypeSurfaces().get(i)).surface());
        }
        builder.append(")->");
        builder.append(typeSurfaceTable.get(shape.outputTypeSurface()).surface());
        return builder.toString();
    }

    private String outputShapeSurface(
            final PbsAst.ReturnKind returnKind,
            final PbsAst.TypeRef returnType,
            final PbsAst.TypeRef resultErrorType) {
        return switch (returnKind) {
            case INFERRED_UNIT, EXPLICIT_UNIT -> "unit";
            case PLAIN -> typeSurfaceKey(returnType);
            case RESULT -> "result<" + typeSurfaceKey(resultErrorType) + ">" + typeSurfaceKey(returnType);
        };
    }

    private String typeSurfaceKey(final PbsAst.TypeRef typeRef) {
        final var unwrapped = unwrapGroup(typeRef);
        if (unwrapped == null) {
            return "unit";
        }
        return switch (unwrapped.kind()) {
            case SIMPLE -> "simple:" + unwrapped.name();
            case SELF -> "self";
            case OPTIONAL -> "optional(" + typeSurfaceKey(unwrapped.inner()) + ")";
            case UNIT -> "unit";
            case GROUP -> typeSurfaceKey(unwrapped.inner());
            case NAMED_TUPLE -> "tuple(" + unwrapped.fields().stream()
                    .map(field -> typeSurfaceKey(field.typeRef()))
                    .reduce((left, right) -> left + "," + right)
                    .orElse("") + ")";
            case ERROR -> "error";
        };
    }

    private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
        if (typeRef == null) {
            return null;
        }
        if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
            return typeRef;
        }
        return unwrapGroup(typeRef.inner());
    }
}
