/*
 * 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.spark.sql.execution.command.datamap

import scala.collection.JavaConverters._

import org.apache.spark.sql._
import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.execution.command._

import org.apache.carbondata.common.exceptions.sql.{MalformedCarbonCommandException, MalformedDataMapCommandException}
import org.apache.carbondata.common.logging.LogServiceFactory
import org.apache.carbondata.core.datamap.{DataMapProvider, DataMapStoreManager}
import org.apache.carbondata.core.datamap.status.DataMapStatusManager
import org.apache.carbondata.core.metadata.ColumnarFormatVersion
import org.apache.carbondata.core.metadata.schema.datamap.{DataMapClassProvider, DataMapProperty}
import org.apache.carbondata.core.metadata.schema.table.{CarbonTable, DataMapSchema}
import org.apache.carbondata.core.util.{CarbonProperties, CarbonUtil}
import org.apache.carbondata.datamap.{DataMapManager, IndexDataMapProvider}
import org.apache.carbondata.events._

/**
 * Below command class will be used to create datamap on table
 * and updating the parent table about the datamap information
 */
case class CarbonCreateDataMapCommand(
    dataMapName: String,
    tableIdentifier: Option[TableIdentifier],
    dmProviderName: String,
    dmProperties: Map[String, String],
    queryString: Option[String],
    ifNotExistsSet: Boolean = false,
    var deferredRebuild: Boolean = false)
  extends AtomicRunnableCommand {

  private val LOGGER = LogServiceFactory.getLogService(this.getClass.getName)
  private var dataMapProvider: DataMapProvider = _
  private var mainTable: CarbonTable = _
  private var dataMapSchema: DataMapSchema = _

  override def processMetadata(sparkSession: SparkSession): Seq[Row] = {
    // since streaming segment does not support building index and pre-aggregate yet,
    // so streaming table does not support create datamap
    mainTable = tableIdentifier match {
      case Some(table) =>
        CarbonEnv.getCarbonTable(table.database, table.table)(sparkSession)
      case _ => null
    }

    if (mainTable != null) {
      setAuditTable(mainTable)
    }
    setAuditInfo(Map("provider" -> dmProviderName, "dmName" -> dataMapName) ++ dmProperties)

    if (mainTable != null && !mainTable.getTableInfo.isTransactionalTable) {
      throw new MalformedCarbonCommandException("Unsupported operation on non transactional table")
    }

    if (mainTable != null && mainTable.getDataMapSchema(dataMapName) != null) {
      if (!ifNotExistsSet) {
        throw new MalformedDataMapCommandException(s"DataMap name '$dataMapName' already exist")
      } else {
        return Seq.empty
      }
    }

    if (mainTable != null && CarbonUtil.getFormatVersion(mainTable) != ColumnarFormatVersion.V3) {
      throw new MalformedCarbonCommandException(s"Unsupported operation on table with " +
                                                s"V1 or V2 format data")
    }

    dataMapSchema = new DataMapSchema(dataMapName, dmProviderName)

    val property = dmProperties.map(x => (x._1.trim, x._2.trim)).asJava
    val javaMap = new java.util.HashMap[String, String](property)
    javaMap.put(DataMapProperty.DEFERRED_REBUILD, deferredRebuild.toString)
    dataMapSchema.setProperties(javaMap)

    if (dataMapSchema.isIndexDataMap && mainTable == null) {
      throw new MalformedDataMapCommandException(
        "For this datamap, main table is required. Use `CREATE DATAMAP ... ON TABLE ...` ")
    }
    dataMapProvider = DataMapManager.get.getDataMapProvider(mainTable, dataMapSchema, sparkSession)
    if (deferredRebuild && !dataMapProvider.supportRebuild()) {
      throw new MalformedDataMapCommandException(
        s"DEFERRED REBUILD is not supported on this datamap $dataMapName" +
        s" with provider ${dataMapSchema.getProviderName}")
    }

    if (null != mainTable) {
      if (mainTable.isChildTable || mainTable.isChildDataMap) {
        throw new MalformedDataMapCommandException(
          "Cannot create DataMap on child table " + mainTable.getTableUniqueName)
      }
    }
    if (!dataMapSchema.isIndexDataMap && !dataMapSchema.getProviderName
      .equalsIgnoreCase(DataMapClassProvider.PREAGGREGATE.getShortName) && !dataMapSchema
      .getProviderName.equalsIgnoreCase(DataMapClassProvider.TIMESERIES.getShortName)) {
      if (DataMapStoreManager.getInstance().getAllDataMapSchemas.asScala
        .exists(_.getDataMapName.equalsIgnoreCase(dataMapSchema.getDataMapName))) {
        if (!ifNotExistsSet) {
          throw new MalformedDataMapCommandException(
            "DataMap with name " + dataMapSchema.getDataMapName + " already exists in storage")
        }
        else {
          return Seq.empty
        }
      }
    }

    val systemFolderLocation: String = CarbonProperties.getInstance().getSystemFolderLocation
    val operationContext: OperationContext = new OperationContext()

    // If it is index datamap, check whether the column has datamap created already
    dataMapProvider match {
      case provider: IndexDataMapProvider =>
        val isBloomFilter = DataMapClassProvider.BLOOMFILTER.getShortName
          .equalsIgnoreCase(dmProviderName)
        val datamaps = DataMapStoreManager.getInstance.getAllDataMap(mainTable).asScala
        val thisDmProviderName =
          dataMapProvider.asInstanceOf[IndexDataMapProvider].getDataMapSchema.getProviderName
        val existingIndexColumn4ThisProvider = datamaps.filter { datamap =>
          thisDmProviderName.equalsIgnoreCase(datamap.getDataMapSchema.getProviderName)
        }.flatMap { datamap =>
          datamap.getDataMapSchema.getIndexColumns
        }.distinct

        provider.getIndexedColumns.asScala.foreach { column =>
          if (existingIndexColumn4ThisProvider.contains(column.getColName)) {
            throw new MalformedDataMapCommandException(String.format(
              "column '%s' already has %s index datamap created",
              column.getColName, thisDmProviderName))
          } else if (isBloomFilter) {
            // if datamap provider is bloomfilter,the index column datatype cannot be complex type
            if (column.isComplex) {
              throw new MalformedDataMapCommandException(
                s"BloomFilter datamap does not support complex datatype column: ${
                  column.getColName
                }")
            }
          }
        }

        val createDataMapPreExecutionEvent: CreateDataMapPreExecutionEvent =
          new CreateDataMapPreExecutionEvent(sparkSession,
            systemFolderLocation, tableIdentifier.get)
        OperationListenerBus.getInstance().fireEvent(createDataMapPreExecutionEvent,
          operationContext)
        dataMapProvider.initMeta(queryString.orNull)
        DataMapStatusManager.disableDataMap(dataMapName)
      case _ =>
        val createDataMapPreExecutionEvent: CreateDataMapPreExecutionEvent =
          CreateDataMapPreExecutionEvent(sparkSession,
            systemFolderLocation, tableIdentifier.orNull)
        OperationListenerBus.getInstance().fireEvent(createDataMapPreExecutionEvent,
          operationContext)
        dataMapProvider.initMeta(queryString.orNull)
    }
    val createDataMapPostExecutionEvent: CreateDataMapPostExecutionEvent =
      CreateDataMapPostExecutionEvent(sparkSession,
        systemFolderLocation, tableIdentifier, dmProviderName)
    OperationListenerBus.getInstance().fireEvent(createDataMapPostExecutionEvent,
      operationContext)
    Seq.empty
  }

  override def processData(sparkSession: SparkSession): Seq[Row] = {
    if (dataMapProvider != null) {
      dataMapProvider.initData()
      if (mainTable != null && !deferredRebuild) {
        dataMapProvider.rebuild()
        if (dataMapSchema.isIndexDataMap) {
          val operationContext: OperationContext = new OperationContext()
          val systemFolderLocation: String = CarbonProperties.getInstance().getSystemFolderLocation
          val updateDataMapPreExecutionEvent: UpdateDataMapPreExecutionEvent =
            new UpdateDataMapPreExecutionEvent(sparkSession,
              systemFolderLocation, tableIdentifier.get)
          OperationListenerBus.getInstance().fireEvent(updateDataMapPreExecutionEvent,
            operationContext)
          DataMapStatusManager.enableDataMap(dataMapName)
          val updateDataMapPostExecutionEvent: UpdateDataMapPostExecutionEvent =
            new UpdateDataMapPostExecutionEvent(sparkSession,
              systemFolderLocation, tableIdentifier.get)
          OperationListenerBus.getInstance().fireEvent(updateDataMapPostExecutionEvent,
            operationContext)
        }
      }
      if (null != dataMapSchema.getRelationIdentifier && !dataMapSchema.isIndexDataMap &&
          !dataMapSchema.isLazy) {
        DataMapStatusManager.enableDataMap(dataMapName)
      }
    }
    Seq.empty
  }

  override def undoMetadata(sparkSession: SparkSession, exception: Exception): Seq[Row] = {
    if (dataMapProvider != null) {
      val table =
        if (mainTable != null) {
          Some(TableIdentifier(mainTable.getTableName, Some(mainTable.getDatabaseName)))
        } else {
          None
        }
        CarbonDropDataMapCommand(
          dataMapName,
          true,
          table,
          forceDrop = false).run(sparkSession)
    }
    Seq.empty
  }

  override protected def opName: String = "CREATE DATAMAP"
}

