/*
 * 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.`match`

import org.apache.flink.api.common.typeinfo.{BasicTypeInfo, TypeInformation}
import org.apache.flink.api.java.typeutils.RowTypeInfo
import org.apache.flink.cep.pattern.Pattern
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.datastream.DataStream
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.table.api._
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.api.internal.TableEnvironmentImpl
import org.apache.flink.table.data.RowData
import org.apache.flink.table.expressions.Expression
import org.apache.flink.table.planner.calcite.FlinkPlannerImpl
import org.apache.flink.table.planner.delegation.PlannerBase
import org.apache.flink.table.planner.plan.nodes.exec.common.CommonExecMatch
import org.apache.flink.table.planner.plan.nodes.physical.stream.{StreamPhysicalDataStreamScan, StreamPhysicalMatch}
import org.apache.flink.table.planner.plan.utils.MatchUtil
import org.apache.flink.table.planner.utils.TableTestUtil
import org.apache.flink.table.types.logical.{IntType, RowType}
import org.apache.flink.types.Row

import org.apache.calcite.rel.RelNode
import org.apache.calcite.tools.RelBuilder
import org.assertj.core.api.Assertions.fail
import org.mockito.Mockito.{mock, when}

abstract class PatternTranslatorTestBase {

  // setup test utils
  private val testTableTypeInfo = new RowTypeInfo(BasicTypeInfo.INT_TYPE_INFO)
  private val testTableRowType = RowType.of(new IntType)
  private val tableName = "testTable"
  private val context = prepareContext(testTableTypeInfo)
  private val calcitePlanner: FlinkPlannerImpl = context._2.createFlinkPlanner
  private val parser = context._2.plannerContext.createCalciteParser()

  private def prepareContext(
      typeInfo: TypeInformation[Row]): (RelBuilder, PlannerBase, StreamExecutionEnvironment) = {
    // create DataStreamTable
    val dataStreamMock = mock(classOf[DataStream[Row]])
    when(dataStreamMock.getType).thenReturn(typeInfo)
    when(dataStreamMock.getId).thenReturn(0)

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val tEnv = StreamTableEnvironment.create(env, TableTestUtil.STREAM_SETTING)
    TableTestUtil.createTemporaryView(
      tEnv,
      tableName,
      dataStreamMock,
      Some(Array[Expression]('f0, 'proctime.proctime)))

    // prepare RelBuilder
    val planner = tEnv.asInstanceOf[TableEnvironmentImpl].getPlanner.asInstanceOf[PlannerBase]
    val relBuilder: RelBuilder = planner.createRelBuilder
    relBuilder.scan(tableName)

    (relBuilder, planner, env)
  }

  def verifyPattern(matchRecognize: String, expected: Pattern[RowData, _ <: RowData]): Unit = {
    // create RelNode from SQL expression
    val parsed = parser.parse(s"""
                                 |SELECT *
                                 |FROM $tableName
                                 |$matchRecognize
                                 |""".stripMargin)
    val validated = calcitePlanner.validate(parsed)
    val converted = calcitePlanner.rel(validated).rel

    val plannerBase = context._2
    val optimized: RelNode = plannerBase.optimize(Seq(converted)).head

    // throw exception if plan contains more than a match
    // the plan should be: StreamExecMatch -> StreamPhysicalExchange -> StreamPhysicalDataStreamScan
    if (!optimized.getInput(0).getInput(0).isInstanceOf[StreamPhysicalDataStreamScan]) {
      fail("Expression is converted into more than a Match operation. Use a different test method.")
    }

    val dataMatch = optimized.asInstanceOf[StreamPhysicalMatch]
    val p = CommonExecMatch
      .translatePattern(
        MatchUtil.createMatchSpec(dataMatch.getLogicalMatch),
        new Configuration,
        Thread.currentThread().getContextClassLoader,
        context._1,
        testTableRowType)
      .f0

    compare(expected, p)
  }

  private def compare(
      expected: Pattern[RowData, _ <: RowData],
      actual: Pattern[RowData, _ <: RowData]): Unit = {
    var currentLeft = expected
    var currentRight = actual
    do {
      val sameName = currentLeft.getName == currentRight.getName
      val sameQuantifier = currentLeft.getQuantifier == currentRight.getQuantifier
      val sameTimes = currentLeft.getTimes == currentRight.getTimes
      val sameSkipStrategy = currentLeft.getAfterMatchSkipStrategy ==
        currentRight.getAfterMatchSkipStrategy

      val sameTimeWindow = if (currentLeft.getWindowSize.isPresent && currentRight != null) {
        currentLeft.getWindowSize.get.toMillis == currentRight.getWindowSize.get.toMillis
      } else {
        !currentLeft.getWindowSize.isPresent && !currentRight.getWindowSize.isPresent
      }

      currentLeft = currentLeft.getPrevious
      currentRight = currentRight.getPrevious

      if (!sameName || !sameQuantifier || !sameTimes || !sameSkipStrategy || !sameTimeWindow) {
        throw new AssertionError(
          s"Compiled different pattern. expected: $expected, actual: $actual")
      }

    } while (currentLeft != null)

    if (currentRight != null) {
      throw new AssertionError(s"Compiled different pattern. expected: $expected, actual: $actual")
    }
  }
}
