/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.flink.table.planner.plan.nodes.exec.stream;

import org.apache.flink.FlinkVersion;
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.ReadableConfig;
import org.apache.flink.core.memory.ManagedMemoryUseCase;
import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
import org.apache.flink.streaming.api.transformations.OneInputTransformation;
import org.apache.flink.table.api.TableException;
import org.apache.flink.table.connector.Projection;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.expressions.FieldReferenceExpression;
import org.apache.flink.table.expressions.ValueLiteralExpression;
import org.apache.flink.table.functions.python.PythonAggregateFunctionInfo;
import org.apache.flink.table.functions.python.PythonFunctionInfo;
import org.apache.flink.table.functions.python.PythonFunctionKind;
import org.apache.flink.table.planner.codegen.CodeGeneratorContext;
import org.apache.flink.table.planner.codegen.ProjectionCodeGenerator;
import org.apache.flink.table.planner.delegation.PlannerBase;
import org.apache.flink.table.planner.plan.logical.LogicalWindow;
import org.apache.flink.table.planner.plan.logical.SessionGroupWindow;
import org.apache.flink.table.planner.plan.logical.SlidingGroupWindow;
import org.apache.flink.table.planner.plan.logical.TumblingGroupWindow;
import org.apache.flink.table.planner.plan.nodes.exec.ExecEdge;
import org.apache.flink.table.planner.plan.nodes.exec.ExecNode;
import org.apache.flink.table.planner.plan.nodes.exec.ExecNodeConfig;
import org.apache.flink.table.planner.plan.nodes.exec.ExecNodeContext;
import org.apache.flink.table.planner.plan.nodes.exec.ExecNodeMetadata;
import org.apache.flink.table.planner.plan.nodes.exec.InputProperty;
import org.apache.flink.table.planner.plan.nodes.exec.utils.CommonPythonUtil;
import org.apache.flink.table.planner.plan.nodes.exec.utils.ExecNodeUtil;
import org.apache.flink.table.planner.plan.utils.AggregateInfoList;
import org.apache.flink.table.planner.plan.utils.AggregateUtil;
import org.apache.flink.table.planner.plan.utils.KeySelectorUtil;
import org.apache.flink.table.planner.plan.utils.PythonUtil;
import org.apache.flink.table.planner.plan.utils.WindowEmitStrategy;
import org.apache.flink.table.planner.utils.JavaScalaConversionUtil;
import org.apache.flink.table.planner.utils.TableConfigUtils;
import org.apache.flink.table.runtime.dataview.DataViewSpec;
import org.apache.flink.table.runtime.generated.GeneratedProjection;
import org.apache.flink.table.runtime.groupwindow.NamedWindowProperty;
import org.apache.flink.table.runtime.keyselector.RowDataKeySelector;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.CountSlidingWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.CountTumblingWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.GroupWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.SessionWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.SlidingWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.assigners.TumblingWindowAssigner;
import org.apache.flink.table.runtime.operators.window.groupwindow.triggers.ElementTriggers;
import org.apache.flink.table.runtime.operators.window.groupwindow.triggers.EventTimeTriggers;
import org.apache.flink.table.runtime.operators.window.groupwindow.triggers.ProcessingTimeTriggers;
import org.apache.flink.table.runtime.operators.window.groupwindow.triggers.Trigger;
import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
import org.apache.flink.table.runtime.util.TimeWindowUtil;
import org.apache.flink.table.types.logical.RowType;

import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonProperty;

import org.apache.calcite.rel.core.AggregateCall;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.apache.flink.table.planner.plan.utils.AggregateUtil.hasRowIntervalType;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.hasTimeIntervalType;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.isProctimeAttribute;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.isRowtimeAttribute;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.toDuration;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.toLong;
import static org.apache.flink.table.planner.plan.utils.AggregateUtil.transformToStreamAggregateInfoList;
import static org.apache.flink.util.Preconditions.checkArgument;
import static org.apache.flink.util.Preconditions.checkNotNull;

/** Stream {@link ExecNode} for group widow aggregate (Python user defined aggregate function). */
@ExecNodeMetadata(
        name = "stream-exec-python-group-window-aggregate",
        version = 1,
        producedTransformations =
                StreamExecPythonGroupWindowAggregate.PYTHON_GROUP_WINDOW_AGGREGATE_TRANSFORMATION,
        minPlanVersion = FlinkVersion.v1_16,
        minStateVersion = FlinkVersion.v1_16)
public class StreamExecPythonGroupWindowAggregate extends StreamExecAggregateBase {

    private static final Logger LOGGER =
            LoggerFactory.getLogger(StreamExecPythonGroupWindowAggregate.class);

    public static final String PYTHON_GROUP_WINDOW_AGGREGATE_TRANSFORMATION =
            "python-group-window-aggregate";

    public static final String FIELD_NAME_WINDOW = "window";
    public static final String FIELD_NAME_NAMED_WINDOW_PROPERTIES = "namedWindowProperties";

    private static final String ARROW_STREAM_PYTHON_GROUP_WINDOW_AGGREGATE_FUNCTION_OPERATOR_NAME =
            "org.apache.flink.table.runtime.operators.python.aggregate.arrow.stream."
                    + "StreamArrowPythonGroupWindowAggregateFunctionOperator";

    private static final String
            GENERAL_STREAM_PYTHON_GROUP_WINDOW_AGGREGATE_FUNCTION_OPERATOR_NAME =
                    "org.apache.flink.table.runtime.operators.python.aggregate."
                            + "PythonStreamGroupWindowAggregateOperator";

    private static final String GENERAL_STREAM_PYTHON_CREATE_TUMBLING_GROUP_WINDOW_METHOD =
            "createTumblingGroupWindowAggregateOperator";
    private static final String GENERAL_STREAM_PYTHON_CREATE_SLIDING_GROUP_WINDOW_METHOD =
            "createSlidingGroupWindowAggregateOperator";
    private static final String GENERAL_STREAM_PYTHON_CREATE_SESSION_GROUP_WINDOW_METHOD =
            "createSessionGroupWindowAggregateOperator";

    @JsonProperty(FIELD_NAME_GROUPING)
    private final int[] grouping;

    @JsonProperty(FIELD_NAME_AGG_CALLS)
    private final AggregateCall[] aggCalls;

    @JsonProperty(FIELD_NAME_WINDOW)
    private final LogicalWindow window;

    @JsonProperty(FIELD_NAME_NAMED_WINDOW_PROPERTIES)
    private final NamedWindowProperty[] namedWindowProperties;

    @JsonProperty(FIELD_NAME_NEED_RETRACTION)
    private final boolean needRetraction;

    @JsonProperty(FIELD_NAME_GENERATE_UPDATE_BEFORE)
    private final boolean generateUpdateBefore;

    public StreamExecPythonGroupWindowAggregate(
            ReadableConfig tableConfig,
            int[] grouping,
            AggregateCall[] aggCalls,
            LogicalWindow window,
            NamedWindowProperty[] namedWindowProperties,
            boolean generateUpdateBefore,
            boolean needRetraction,
            InputProperty inputProperty,
            RowType outputType,
            String description) {
        this(
                ExecNodeContext.newNodeId(),
                ExecNodeContext.newContext(StreamExecPythonGroupWindowAggregate.class),
                ExecNodeContext.newPersistedConfig(
                        StreamExecPythonGroupWindowAggregate.class, tableConfig),
                grouping,
                aggCalls,
                window,
                namedWindowProperties,
                generateUpdateBefore,
                needRetraction,
                Collections.singletonList(inputProperty),
                outputType,
                description);
    }

    @JsonCreator
    public StreamExecPythonGroupWindowAggregate(
            @JsonProperty(FIELD_NAME_ID) int id,
            @JsonProperty(FIELD_NAME_TYPE) ExecNodeContext context,
            @JsonProperty(FIELD_NAME_CONFIGURATION) ReadableConfig persistedConfig,
            @JsonProperty(FIELD_NAME_GROUPING) int[] grouping,
            @JsonProperty(FIELD_NAME_AGG_CALLS) AggregateCall[] aggCalls,
            @JsonProperty(FIELD_NAME_WINDOW) LogicalWindow window,
            @JsonProperty(FIELD_NAME_NAMED_WINDOW_PROPERTIES)
                    NamedWindowProperty[] namedWindowProperties,
            @JsonProperty(FIELD_NAME_GENERATE_UPDATE_BEFORE) boolean generateUpdateBefore,
            @JsonProperty(FIELD_NAME_NEED_RETRACTION) boolean needRetraction,
            @JsonProperty(FIELD_NAME_INPUT_PROPERTIES) List<InputProperty> inputProperties,
            @JsonProperty(FIELD_NAME_OUTPUT_TYPE) RowType outputType,
            @JsonProperty(FIELD_NAME_DESCRIPTION) String description) {
        super(id, context, persistedConfig, inputProperties, outputType, description);
        checkArgument(inputProperties.size() == 1);
        this.grouping = checkNotNull(grouping);
        this.aggCalls = checkNotNull(aggCalls);
        this.window = checkNotNull(window);
        this.namedWindowProperties = checkNotNull(namedWindowProperties);
        this.generateUpdateBefore = generateUpdateBefore;
        this.needRetraction = needRetraction;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected Transformation<RowData> translateToPlanInternal(
            PlannerBase planner, ExecNodeConfig config) {
        final boolean isCountWindow;
        if (window instanceof TumblingGroupWindow) {
            isCountWindow = hasRowIntervalType(((TumblingGroupWindow) window).size());
        } else if (window instanceof SlidingGroupWindow) {
            isCountWindow = hasRowIntervalType(((SlidingGroupWindow) window).size());
        } else {
            isCountWindow = false;
        }

        if (isCountWindow && grouping.length > 0 && config.getStateRetentionTime() < 0) {
            LOGGER.warn(
                    "No state retention interval configured for a query which accumulates state."
                            + " Please provide a query configuration with valid retention interval to"
                            + " prevent excessive state size. You may specify a retention time of 0 to"
                            + " not clean up the state.");
        }

        final ExecEdge inputEdge = getInputEdges().get(0);
        final Transformation<RowData> inputTransform =
                (Transformation<RowData>) inputEdge.translateToPlan(planner);
        final RowType inputRowType = (RowType) inputEdge.getOutputType();
        final RowType outputRowType = InternalTypeInfo.of(getOutputType()).toRowType();

        final int inputTimeFieldIndex;
        if (isRowtimeAttribute(window.timeAttribute())) {
            inputTimeFieldIndex = window.timeAttribute().getFieldIndex();
            if (inputTimeFieldIndex < 0) {
                throw new TableException(
                        "Group window must defined on a time attribute, "
                                + "but the time attribute can't be found.\n"
                                + "This should never happen. Please file an issue.");
            }
        } else {
            inputTimeFieldIndex = -1;
        }

        final ZoneId shiftTimeZone =
                TimeWindowUtil.getShiftTimeZone(
                        window.timeAttribute().getOutputDataType().getLogicalType(),
                        TableConfigUtils.getLocalTimeZone(config));
        Tuple2<GroupWindowAssigner<?>, Trigger<?>> windowAssignerAndTrigger =
                generateWindowAssignerAndTrigger();
        GroupWindowAssigner<?> windowAssigner = windowAssignerAndTrigger.f0;
        Trigger<?> trigger = windowAssignerAndTrigger.f1;
        final Configuration pythonConfig =
                CommonPythonUtil.extractPythonConfiguration(
                        planner.getTableConfig(), planner.getFlinkContext().getClassLoader());
        final ExecNodeConfig pythonNodeConfig =
                ExecNodeConfig.ofNodeConfig(pythonConfig, config.isCompiled());
        boolean isGeneralPythonUDAF =
                Arrays.stream(aggCalls)
                        .anyMatch(x -> PythonUtil.isPythonAggregate(x, PythonFunctionKind.GENERAL));
        OneInputTransformation<RowData, RowData> transform;
        WindowEmitStrategy emitStrategy = WindowEmitStrategy.apply(config, window);
        if (isGeneralPythonUDAF) {
            final boolean[] aggCallNeedRetractions = new boolean[aggCalls.length];
            Arrays.fill(aggCallNeedRetractions, needRetraction);
            final AggregateInfoList aggInfoList =
                    transformToStreamAggregateInfoList(
                            planner.getTypeFactory(),
                            inputRowType,
                            JavaScalaConversionUtil.toScala(Arrays.asList(aggCalls)),
                            aggCallNeedRetractions,
                            needRetraction,
                            true,
                            true);
            transform =
                    createGeneralPythonStreamWindowGroupOneInputTransformation(
                            inputTransform,
                            inputRowType,
                            outputRowType,
                            inputTimeFieldIndex,
                            windowAssigner,
                            aggInfoList,
                            emitStrategy.getAllowLateness(),
                            pythonConfig,
                            pythonNodeConfig,
                            planner.getFlinkContext().getClassLoader(),
                            shiftTimeZone);
        } else {
            transform =
                    createPandasPythonStreamWindowGroupOneInputTransformation(
                            inputTransform,
                            inputRowType,
                            outputRowType,
                            inputTimeFieldIndex,
                            windowAssigner,
                            trigger,
                            emitStrategy.getAllowLateness(),
                            pythonConfig,
                            pythonNodeConfig,
                            planner.getFlinkContext().getClassLoader(),
                            shiftTimeZone);
        }

        if (CommonPythonUtil.isPythonWorkerUsingManagedMemory(
                pythonConfig, planner.getFlinkContext().getClassLoader())) {
            transform.declareManagedMemoryUseCaseAtSlotScope(ManagedMemoryUseCase.PYTHON);
        }
        // set KeyType and Selector for state
        final RowDataKeySelector selector =
                KeySelectorUtil.getRowDataSelector(
                        planner.getFlinkContext().getClassLoader(),
                        grouping,
                        InternalTypeInfo.of(inputRowType));
        transform.setStateKeySelector(selector);
        transform.setStateKeyType(selector.getProducedType());
        return transform;
    }

    private Tuple2<GroupWindowAssigner<?>, Trigger<?>> generateWindowAssignerAndTrigger() {
        GroupWindowAssigner<?> windowAssiger;
        Trigger<?> trigger;
        if (window instanceof TumblingGroupWindow) {
            TumblingGroupWindow tumblingWindow = (TumblingGroupWindow) window;
            FieldReferenceExpression timeField = tumblingWindow.timeField();
            ValueLiteralExpression size = tumblingWindow.size();
            if (isProctimeAttribute(timeField) && hasTimeIntervalType(size)) {
                windowAssiger = TumblingWindowAssigner.of(toDuration(size)).withProcessingTime();
                trigger = ProcessingTimeTriggers.afterEndOfWindow();
            } else if (isRowtimeAttribute(timeField) && hasTimeIntervalType(size)) {
                windowAssiger = TumblingWindowAssigner.of(toDuration(size)).withEventTime();
                trigger = EventTimeTriggers.afterEndOfWindow();
            } else if (isProctimeAttribute(timeField) && hasRowIntervalType(size)) {
                windowAssiger = CountTumblingWindowAssigner.of(toLong(size));
                trigger = ElementTriggers.count(toLong(size));
            } else {
                // TODO: EventTimeTumblingGroupWindow should sort the stream on event time
                // before applying the  windowing logic. Otherwise, this would be the same as a
                // ProcessingTimeTumblingGroupWindow
                throw new UnsupportedOperationException(
                        "Event-time grouping windows on row intervals are currently not supported.");
            }
        } else if (window instanceof SlidingGroupWindow) {
            SlidingGroupWindow slidingWindow = (SlidingGroupWindow) window;
            FieldReferenceExpression timeField = slidingWindow.timeField();
            ValueLiteralExpression size = slidingWindow.size();
            ValueLiteralExpression slide = slidingWindow.slide();
            if (isProctimeAttribute(timeField) && hasTimeIntervalType(size)) {
                windowAssiger =
                        SlidingWindowAssigner.of(toDuration(size), toDuration(slide))
                                .withProcessingTime();
                trigger = ProcessingTimeTriggers.afterEndOfWindow();
            } else if (isRowtimeAttribute(timeField) && hasTimeIntervalType(size)) {
                windowAssiger = SlidingWindowAssigner.of(toDuration(size), toDuration(slide));
                trigger = EventTimeTriggers.afterEndOfWindow();
            } else if (isProctimeAttribute(timeField) && hasRowIntervalType(size)) {
                windowAssiger = CountSlidingWindowAssigner.of(toLong(size), toLong(slide));
                trigger = ElementTriggers.count(toLong(size));
            } else {
                // TODO: EventTimeTumblingGroupWindow should sort the stream on event time
                // before applying the  windowing logic. Otherwise, this would be the same as a
                // ProcessingTimeTumblingGroupWindow
                throw new UnsupportedOperationException(
                        "Event-time grouping windows on row intervals are currently not supported.");
            }
        } else if (window instanceof SessionGroupWindow) {
            SessionGroupWindow sessionWindow = (SessionGroupWindow) window;
            FieldReferenceExpression timeField = sessionWindow.timeField();
            ValueLiteralExpression gap = sessionWindow.gap();
            if (isProctimeAttribute(timeField)) {
                windowAssiger = SessionWindowAssigner.withGap(toDuration(gap));
                trigger = ProcessingTimeTriggers.afterEndOfWindow();
            } else if (isRowtimeAttribute(timeField)) {
                windowAssiger = SessionWindowAssigner.withGap(toDuration(gap));
                trigger = EventTimeTriggers.afterEndOfWindow();
            } else {
                throw new UnsupportedOperationException("This should not happen.");
            }
        } else {
            throw new TableException("Unsupported window: " + window.toString());
        }
        return Tuple2.of(windowAssiger, trigger);
    }

    private OneInputTransformation<RowData, RowData>
            createPandasPythonStreamWindowGroupOneInputTransformation(
                    Transformation<RowData> inputTransform,
                    RowType inputRowType,
                    RowType outputRowType,
                    int inputTimeFieldIndex,
                    GroupWindowAssigner<?> windowAssigner,
                    Trigger<?> trigger,
                    long allowance,
                    Configuration pythonConfig,
                    ExecNodeConfig config,
                    ClassLoader classLoader,
                    ZoneId shiftTimeZone) {

        Tuple2<int[], PythonFunctionInfo[]> aggInfos =
                CommonPythonUtil.extractPythonAggregateFunctionInfosFromAggregateCall(aggCalls);
        int[] pythonUdafInputOffsets = aggInfos.f0;
        PythonFunctionInfo[] pythonFunctionInfos = aggInfos.f1;
        OneInputStreamOperator<RowData, RowData> pythonOperator =
                getPandasPythonStreamGroupWindowAggregateFunctionOperator(
                        config,
                        classLoader,
                        pythonConfig,
                        inputRowType,
                        outputRowType,
                        windowAssigner,
                        trigger,
                        allowance,
                        inputTimeFieldIndex,
                        pythonUdafInputOffsets,
                        pythonFunctionInfos,
                        shiftTimeZone);
        return ExecNodeUtil.createOneInputTransformation(
                inputTransform,
                createTransformationMeta(PYTHON_GROUP_WINDOW_AGGREGATE_TRANSFORMATION, config),
                pythonOperator,
                InternalTypeInfo.of(outputRowType),
                inputTransform.getParallelism(),
                false);
    }

    private OneInputTransformation<RowData, RowData>
            createGeneralPythonStreamWindowGroupOneInputTransformation(
                    Transformation<RowData> inputTransform,
                    RowType inputRowType,
                    RowType outputRowType,
                    int inputTimeFieldIndex,
                    GroupWindowAssigner<?> windowAssigner,
                    AggregateInfoList aggInfoList,
                    long allowance,
                    Configuration pythonConfig,
                    ExecNodeConfig pythonNodeConfig,
                    ClassLoader classLoader,
                    ZoneId shiftTimeZone) {
        final int inputCountIndex = aggInfoList.getIndexOfCountStar();
        final boolean countStarInserted = aggInfoList.countStarInserted();
        final Tuple2<PythonAggregateFunctionInfo[], DataViewSpec[][]> aggInfosAndDataViewSpecs =
                CommonPythonUtil.extractPythonAggregateFunctionInfos(aggInfoList, aggCalls);
        PythonAggregateFunctionInfo[] pythonFunctionInfos = aggInfosAndDataViewSpecs.f0;
        DataViewSpec[][] dataViewSpecs = aggInfosAndDataViewSpecs.f1;
        OneInputStreamOperator<RowData, RowData> pythonOperator =
                getGeneralPythonStreamGroupWindowAggregateFunctionOperator(
                        pythonConfig,
                        classLoader,
                        inputRowType,
                        outputRowType,
                        windowAssigner,
                        pythonFunctionInfos,
                        dataViewSpecs,
                        inputTimeFieldIndex,
                        inputCountIndex,
                        generateUpdateBefore,
                        countStarInserted,
                        allowance,
                        shiftTimeZone);

        return ExecNodeUtil.createOneInputTransformation(
                inputTransform,
                createTransformationMeta(
                        PYTHON_GROUP_WINDOW_AGGREGATE_TRANSFORMATION, pythonNodeConfig),
                pythonOperator,
                InternalTypeInfo.of(outputRowType),
                inputTransform.getParallelism(),
                false);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private OneInputStreamOperator<RowData, RowData>
            getPandasPythonStreamGroupWindowAggregateFunctionOperator(
                    ExecNodeConfig config,
                    ClassLoader classLoader,
                    Configuration pythonConfig,
                    RowType inputRowType,
                    RowType outputRowType,
                    GroupWindowAssigner<?> windowAssigner,
                    Trigger<?> trigger,
                    long allowance,
                    int inputTimeFieldIndex,
                    int[] udafInputOffsets,
                    PythonFunctionInfo[] pythonFunctionInfos,
                    ZoneId shiftTimeZone) {
        Class clazz =
                CommonPythonUtil.loadClass(
                        ARROW_STREAM_PYTHON_GROUP_WINDOW_AGGREGATE_FUNCTION_OPERATOR_NAME,
                        classLoader);
        RowType userDefinedFunctionInputType =
                (RowType) Projection.of(udafInputOffsets).project(inputRowType);
        RowType userDefinedFunctionOutputType =
                (RowType)
                        Projection.range(
                                        grouping.length,
                                        outputRowType.getFieldCount()
                                                - namedWindowProperties.length)
                                .project(outputRowType);

        try {
            Constructor<OneInputStreamOperator<RowData, RowData>> ctor =
                    clazz.getConstructor(
                            Configuration.class,
                            PythonFunctionInfo[].class,
                            RowType.class,
                            RowType.class,
                            RowType.class,
                            int.class,
                            GroupWindowAssigner.class,
                            Trigger.class,
                            long.class,
                            NamedWindowProperty[].class,
                            ZoneId.class,
                            GeneratedProjection.class);
            return ctor.newInstance(
                    pythonConfig,
                    pythonFunctionInfos,
                    inputRowType,
                    userDefinedFunctionInputType,
                    userDefinedFunctionOutputType,
                    inputTimeFieldIndex,
                    windowAssigner,
                    trigger,
                    allowance,
                    namedWindowProperties,
                    shiftTimeZone,
                    ProjectionCodeGenerator.generateProjection(
                            new CodeGeneratorContext(config, classLoader),
                            "UdafInputProjection",
                            inputRowType,
                            userDefinedFunctionInputType,
                            udafInputOffsets));
        } catch (NoSuchMethodException
                | IllegalAccessException
                | InstantiationException
                | InvocationTargetException e) {
            throw new TableException(
                    "Python StreamArrowPythonGroupWindowAggregateFunctionOperator constructed failed.",
                    e);
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private OneInputStreamOperator<RowData, RowData>
            getGeneralPythonStreamGroupWindowAggregateFunctionOperator(
                    Configuration config,
                    ClassLoader classLoader,
                    RowType inputType,
                    RowType outputType,
                    GroupWindowAssigner<?> windowAssigner,
                    PythonAggregateFunctionInfo[] aggregateFunctions,
                    DataViewSpec[][] dataViewSpecs,
                    int inputTimeFieldIndex,
                    int indexOfCountStar,
                    boolean generateUpdateBefore,
                    boolean countStarInserted,
                    long allowance,
                    ZoneId shiftTimeZone) {
        Class clazz =
                CommonPythonUtil.loadClass(
                        GENERAL_STREAM_PYTHON_GROUP_WINDOW_AGGREGATE_FUNCTION_OPERATOR_NAME,
                        classLoader);

        boolean isRowTime = AggregateUtil.isRowtimeAttribute(window.timeAttribute());

        try {
            if (window instanceof TumblingGroupWindow) {
                ValueLiteralExpression size = ((TumblingGroupWindow) window).size();

                Method create =
                        clazz.getMethod(
                                GENERAL_STREAM_PYTHON_CREATE_TUMBLING_GROUP_WINDOW_METHOD,
                                Configuration.class,
                                RowType.class,
                                RowType.class,
                                PythonAggregateFunctionInfo[].class,
                                DataViewSpec[][].class,
                                int[].class,
                                int.class,
                                boolean.class,
                                boolean.class,
                                int.class,
                                GroupWindowAssigner.class,
                                boolean.class,
                                boolean.class,
                                long.class,
                                long.class,
                                NamedWindowProperty[].class,
                                ZoneId.class);
                return (OneInputStreamOperator<RowData, RowData>)
                        create.invoke(
                                null,
                                config,
                                inputType,
                                outputType,
                                aggregateFunctions,
                                dataViewSpecs,
                                grouping,
                                indexOfCountStar,
                                generateUpdateBefore,
                                countStarInserted,
                                inputTimeFieldIndex,
                                windowAssigner,
                                isRowTime,
                                AggregateUtil.hasTimeIntervalType(size),
                                AggregateUtil.toDuration(size).toMillis(),
                                allowance,
                                namedWindowProperties,
                                shiftTimeZone);
            } else if (window instanceof SlidingGroupWindow) {
                ValueLiteralExpression size = ((SlidingGroupWindow) window).size();
                ValueLiteralExpression slide = ((SlidingGroupWindow) window).slide();

                Method create =
                        clazz.getMethod(
                                GENERAL_STREAM_PYTHON_CREATE_SLIDING_GROUP_WINDOW_METHOD,
                                Configuration.class,
                                RowType.class,
                                RowType.class,
                                PythonAggregateFunctionInfo[].class,
                                DataViewSpec[][].class,
                                int[].class,
                                int.class,
                                boolean.class,
                                boolean.class,
                                int.class,
                                GroupWindowAssigner.class,
                                boolean.class,
                                boolean.class,
                                long.class,
                                long.class,
                                long.class,
                                NamedWindowProperty[].class,
                                ZoneId.class);
                return (OneInputStreamOperator<RowData, RowData>)
                        create.invoke(
                                null,
                                config,
                                inputType,
                                outputType,
                                aggregateFunctions,
                                dataViewSpecs,
                                grouping,
                                indexOfCountStar,
                                generateUpdateBefore,
                                countStarInserted,
                                inputTimeFieldIndex,
                                windowAssigner,
                                isRowTime,
                                AggregateUtil.hasTimeIntervalType(size),
                                AggregateUtil.toDuration(size).toMillis(),
                                AggregateUtil.toDuration(slide).toMillis(),
                                allowance,
                                namedWindowProperties,
                                shiftTimeZone);
            } else if (window instanceof SessionGroupWindow) {
                ValueLiteralExpression gap = ((SessionGroupWindow) window).gap();

                Method create =
                        clazz.getMethod(
                                GENERAL_STREAM_PYTHON_CREATE_SESSION_GROUP_WINDOW_METHOD,
                                Configuration.class,
                                RowType.class,
                                RowType.class,
                                PythonAggregateFunctionInfo[].class,
                                DataViewSpec[][].class,
                                int[].class,
                                int.class,
                                boolean.class,
                                boolean.class,
                                int.class,
                                GroupWindowAssigner.class,
                                boolean.class,
                                long.class,
                                long.class,
                                NamedWindowProperty[].class,
                                ZoneId.class);
                return (OneInputStreamOperator<RowData, RowData>)
                        create.invoke(
                                null,
                                config,
                                inputType,
                                outputType,
                                aggregateFunctions,
                                dataViewSpecs,
                                grouping,
                                indexOfCountStar,
                                generateUpdateBefore,
                                countStarInserted,
                                inputTimeFieldIndex,
                                windowAssigner,
                                isRowTime,
                                AggregateUtil.toDuration(gap).toMillis(),
                                allowance,
                                namedWindowProperties,
                                shiftTimeZone);
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new TableException(
                    "Python PythonStreamGroupWindowAggregateOperator constructed failed.", e);
        }
        throw new RuntimeException(String.format("Unsupported LogicWindow Type %s", window));
    }
}
