/*
 * 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.carbondata.spark.testsuite.allqueries


import java.util

import scala.collection.JavaConverters._

import org.apache.spark.sql.{CarbonEnv, Row}
import org.apache.spark.sql.hive.CarbonRelation
import org.apache.spark.sql.test.util.QueryTest
import org.scalatest.BeforeAndAfterAll

import org.apache.carbondata.core.datamap.dev.DataMap
import org.apache.carbondata.core.datamap.{DataMapChooser, DataMapStoreManager, Segment, TableDataMap}
import org.apache.carbondata.core.datastore.block.SegmentPropertiesAndSchemaHolder
import org.apache.carbondata.core.indexstore.blockletindex.{BlockDataMap, BlockletDataMap}
import org.apache.carbondata.core.indexstore.schema.CarbonRowSchema
import org.apache.carbondata.core.indexstore.Blocklet
import org.apache.carbondata.core.metadata.datatype.DataTypes
import org.apache.carbondata.core.metadata.schema.table.column.CarbonDimension
import org.apache.carbondata.core.scan.expression.conditional.NotEqualsExpression
import org.apache.carbondata.core.scan.expression.logical.AndExpression
import org.apache.carbondata.core.scan.expression.{ColumnExpression, LiteralExpression}
import org.apache.carbondata.core.scan.filter.resolver.FilterResolverIntf

/**
 * test class for validating COLUMN_META_CACHE and CACHE_LEVEL
 */
class TestQueryWithColumnMetCacheAndCacheLevelProperty extends QueryTest with BeforeAndAfterAll {

  override def beforeAll(): Unit = {
    dropSchema
  }

  override def afterAll(): Unit = {
    dropSchema
  }

  private def dropSchema: Unit = {
    sql("drop table if exists metaCache")
    sql("drop table if exists column_min_max_cache_test")
    sql("drop table if exists minMaxSerialize")
  }

  private def createAndLoadTable(cacheLevel: String): Unit = {
    sql(s"CREATE table column_min_max_cache_test (empno int, empname String, designation String, doj Timestamp, workgroupcategory int, workgroupcategoryname String, deptno int, deptname String, projectcode int, projectjoindate Timestamp, projectenddate Timestamp, attendance int, utilization int,salary int) STORED BY 'carbondata' TBLPROPERTIES('DICTIONARY_INCLUDE'='empno','column_meta_cache'='workgroupcategoryname,designation,salary,attendance', 'CACHE_LEVEL'= '$cacheLevel')")
    sql(s"LOAD DATA LOCAL INPATH '$resourcesPath/data.csv' INTO " +
        "TABLE column_min_max_cache_test OPTIONS('DELIMITER'=',', " +
        "'BAD_RECORDS_LOGGER_ENABLE'='FALSE', 'BAD_RECORDS_ACTION'='FORCE')")
  }

  private def getDataMaps(dbName: String,
      tableName: String,
      segmentId: String,
      isSchemaModified: Boolean = false): List[DataMap[_ <: Blocklet]] = {
    val relation: CarbonRelation = CarbonEnv.getInstance(sqlContext.sparkSession).carbonMetastore
      .lookupRelation(Some(dbName), tableName)(sqlContext.sparkSession)
      .asInstanceOf[CarbonRelation]
    val carbonTable = relation.carbonTable
    assert(carbonTable.getTableInfo.isSchemaModified == isSchemaModified)
    val segment: Segment = Segment.getSegment(segmentId, carbonTable.getTablePath)
    val defaultDataMap: TableDataMap = DataMapStoreManager.getInstance()
      .getDefaultDataMap(carbonTable)
    val dataMaps: List[DataMap[_ <: Blocklet]] = defaultDataMap.getDataMapFactory
      .getDataMaps(segment).asScala.toList
    dataMaps
  }

  private def validateMinMaxColumnsCacheLength(dataMaps: List[DataMap[_ <: Blocklet]],
      expectedLength: Int, storeBlockletCount: Boolean = false): Boolean = {
    val index = dataMaps(0).asInstanceOf[BlockDataMap].getSegmentPropertiesIndex
    val summarySchema = SegmentPropertiesAndSchemaHolder.getInstance()
      .getSegmentPropertiesWrapper(index).getTaskSummarySchema(storeBlockletCount, false)
    val minSchemas = summarySchema(0).asInstanceOf[CarbonRowSchema.StructCarbonRowSchema]
      .getChildSchemas
    minSchemas.length == expectedLength
  }

  test("verify if number of columns cached are as per the COLUMN_META_CACHE property dataMap instance is as per CACHE_LEVEL property") {
    sql("drop table if exists metaCache")
    sql("create table metaCache(name string, c1 string, c2 string) stored by 'carbondata'")
    sql("insert into metaCache select 'a','aa','aaa'")
    checkAnswer(sql("select * from metaCache"), Row("a", "aa", "aaa"))
    var dataMaps = getDataMaps("default", "metaCache", "0")
    // validate dataMap is non empty, its an instance of BlockDataMap and minMaxSchema length is 3
    assert(dataMaps.nonEmpty)
    assert(dataMaps(0).isInstanceOf[BlockDataMap])
    assert(validateMinMaxColumnsCacheLength(dataMaps, 3, true))
    var segmentPropertyIndex = dataMaps(0).asInstanceOf[BlockDataMap].getSegmentPropertiesIndex

    // alter table to add column_meta_cache and cache_level
    sql(
      "alter table metaCache set tblproperties('column_meta_cache'='c2,c1', 'CACHE_LEVEL'='BLOCKLET')")
    var wrapper = SegmentPropertiesAndSchemaHolder.getInstance()
      .getSegmentPropertiesWrapper(segmentPropertyIndex)
    // after alter operation cache should be cleaned and cache should be evicted
    assert(null == wrapper)
    checkAnswer(sql("select * from metaCache"), Row("a", "aa", "aaa"))
    // validate dataMap is non empty, its an instance of BlockletDataMap and minMaxSchema length
    // is 1
    dataMaps = getDataMaps("default", "metaCache", "0")
    assert(dataMaps.nonEmpty)
    assert(dataMaps(0).isInstanceOf[BlockletDataMap])
    assert(validateMinMaxColumnsCacheLength(dataMaps, 2))

    // alter table to add same value as previous with order change for column_meta_cache and cache_level
    segmentPropertyIndex = dataMaps(0).asInstanceOf[BlockDataMap].getSegmentPropertiesIndex
    sql(
      "alter table metaCache set tblproperties('column_meta_cache'='c1,c2', 'CACHE_LEVEL'='BLOCKLET')")
    wrapper = SegmentPropertiesAndSchemaHolder.getInstance()
      .getSegmentPropertiesWrapper(segmentPropertyIndex)
    // after alter operation cache should not be cleaned as value are unchanged
    assert(null != wrapper)

    // alter table to cache no column in column_meta_cache
    segmentPropertyIndex = dataMaps(0).asInstanceOf[BlockDataMap].getSegmentPropertiesIndex
    sql(
      "alter table metaCache set tblproperties('column_meta_cache'='')")
    wrapper = SegmentPropertiesAndSchemaHolder.getInstance()
      .getSegmentPropertiesWrapper(segmentPropertyIndex)

    // after alter operation cache should be cleaned and cache should be evicted
    assert(null == wrapper)
    checkAnswer(sql("select * from metaCache"), Row("a", "aa", "aaa"))
    // validate dataMap is non empty, its an instance of BlockletDataMap and minMaxSchema length
    // is 0
    dataMaps = getDataMaps("default", "metaCache", "0")
    assert(dataMaps.nonEmpty)
    assert(dataMaps(0).isInstanceOf[BlockletDataMap])
    assert(validateMinMaxColumnsCacheLength(dataMaps, 0))

    // alter table to cache no column in column_meta_cache
    segmentPropertyIndex = dataMaps(0).asInstanceOf[BlockDataMap].getSegmentPropertiesIndex
    sql(
      "alter table metaCache unset tblproperties('column_meta_cache', 'cache_level')")
    wrapper = SegmentPropertiesAndSchemaHolder.getInstance()
      .getSegmentPropertiesWrapper(segmentPropertyIndex)
    // after alter operation cache should be cleaned and cache should be evicted
    assert(null == wrapper)
    checkAnswer(sql("select * from metaCache"), Row("a", "aa", "aaa"))
    // validate dataMap is non empty, its an instance of BlockletDataMap and minMaxSchema length
    // is 3
    dataMaps = getDataMaps("default", "metaCache", "0")
    assert(dataMaps.nonEmpty)
    assert(dataMaps(0).isInstanceOf[BlockDataMap])
    assert(validateMinMaxColumnsCacheLength(dataMaps, 3))
  }

  test("test UPDATE scenario after column_meta_cache") {
    sql("drop table if exists metaCache")
    sql("create table metaCache(name string, c1 string, c2 string) stored by 'carbondata' TBLPROPERTIES('COLUMN_META_CACHE'='')")
    sql("insert into metaCache select 'a','aa','aaa'")
    sql("insert into metaCache select 'b','bb','bbb'")
    sql("update metaCache set(c1)=('new_c1') where c1='aa'").show()
    checkAnswer(sql("select c1 from metaCache"), Seq(Row("new_c1"), Row("bb")))
  }

  test("test queries with column_meta_cache and cache_level='BLOCK'") {
    dropSchema
    // set cache_level
    createAndLoadTable("BLOCK")
    // check count(*)
    checkAnswer(sql("select count(*) from column_min_max_cache_test"), Row(10))
    // check query on cached dimension columns
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where workgroupcategoryname='developer' OR designation='PL'"),
      Row(6))
    // check query on cached dimension column and non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where empname='pramod' and " +
      "workgroupcategoryname='developer'"),
      Row(1))
    // query on cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where workgroupcategoryname='developer'"),
      Row(5))
    // check query on non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where empname='pramod' and " +
      "deptname='network'"),
      Row(0))
    // check query on cached dimension and measure column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where attendance='77' and " +
      "salary='11248' and workgroupcategoryname='manager'"),
      Row(1))
    // check query on cached dimension and measure column with one non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where attendance='77' and " +
      "salary='11248' OR deptname='network'"),
      Row(4))
  }

  test("test queries with column_meta_cache and cache_level='BLOCKLET'") {
    dropSchema
    // set cache_level
    createAndLoadTable("BLOCKLET")
    // check count(*)
    checkAnswer(sql("select count(*) from column_min_max_cache_test"), Row(10))
    // check query on cached dimension columns
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where workgroupcategoryname='developer' OR designation='PL'"),
      Row(6))
    // check query on cached dimension column and non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where empname='pramod' and " +
      "workgroupcategoryname='developer'"),
      Row(1))
    // query on cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where workgroupcategoryname='developer'"),
      Row(5))
    // check query on non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where empname='pramod' and " +
      "deptname='network'"),
      Row(0))
    // check query on cached dimension and measure column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where attendance='77' and " +
      "salary='11248' and workgroupcategoryname='manager'"),
      Row(1))
    // check query on cached dimension and measure column with one non cached column
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where attendance='77' and " +
      "salary='11248' OR deptname='network'"),
      Row(4))
  }

  test("test update on column cached") {
    dropSchema
    // set cache_level
    createAndLoadTable("BLOCKLET")
    sql("update column_min_max_cache_test set (designation)=('SEG') where empname='ayushi'").show()
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where empname='ayushi' and " +
      "designation='SEG'"),
      Row(1))
  }

  test("test update on column not cached") {
    dropSchema
    // set cache_level
    createAndLoadTable("BLOCKLET")
    sql(
      "update column_min_max_cache_test set (workgroupcategoryname)=('solution engrr') where " +
      "workgroupcategoryname='developer'")
      .show()
    checkAnswer(sql(
      "select count(*) from column_min_max_cache_test where workgroupcategoryname='solution " +
      "engrr'"),
      Row(5))
  }

  test("verify column caching with alter add column") {
    sql("drop table if exists alter_add_column_min_max")
    sql("create table alter_add_column_min_max (imei string,AMSize string,channelsId string,ActiveCountry string, Activecity string,gamePointId double,deviceInformationId double,productionDate Timestamp,deliveryDate timestamp,deliverycharge double) STORED BY 'org.apache.carbondata.format' TBLPROPERTIES('table_blocksize'='1','COLUMN_META_CACHE'='AMSize','CACHE_LEVEL'='BLOCKLET')")
    sql("insert into alter_add_column_min_max select '1AA1','8RAM size','4','Chinese','guangzhou',2738,1,'2014-07-01 12:07:28','2014-07-01 12:07:28',25")
    sql("alter table alter_add_column_min_max add columns(age int, name string)")
    sql("ALTER TABLE alter_add_column_min_max SET TBLPROPERTIES('COLUMN_META_CACHE'='age,name')")
    sql("insert into alter_add_column_min_max select '1AA1','8RAM size','4','Chinese','guangzhou',2738,1,'2014-07-01 12:07:28','2014-07-01 12:07:28',25,29,'Rahul'")
    checkAnswer(sql("select count(*) from alter_add_column_min_max where AMSize='8RAM size'"), Row(2))
    sql("drop table if exists alter_add_column_min_max")
  }

  test("verify min/max getting serialized to executor when cache_level = blocklet") {
    sql("drop table if exists minMaxSerialize")
    sql("create table minMaxSerialize(name string, c1 string, c2 string) stored by 'carbondata' TBLPROPERTIES('CACHE_LEVEL'='BLOCKLET', 'COLUMN_META_CACHE'='c1,c2')")
    sql("insert into minMaxSerialize select 'a','aa','aaa'")
    checkAnswer(sql("select * from minMaxSerialize where name='a'"), Row("a", "aa", "aaa"))
    checkAnswer(sql("select * from minMaxSerialize where name='b'"), Seq.empty)
    val relation: CarbonRelation = CarbonEnv.getInstance(sqlContext.sparkSession).carbonMetastore
      .lookupRelation(Some("default"), "minMaxSerialize")(sqlContext.sparkSession)
      .asInstanceOf[CarbonRelation]
    val carbonTable = relation.carbonTable
    // form a filter expression and generate filter resolver tree
    val columnExpression = new ColumnExpression("name", DataTypes.STRING)
    columnExpression.setDimension(true)
    val dimension: CarbonDimension = carbonTable.getDimensionByName(carbonTable.getTableName, "name")
    columnExpression.setDimension(dimension)
    columnExpression.setCarbonColumn(dimension)
    val literalValueExpression = new LiteralExpression("a", DataTypes.STRING)
    val literalNullExpression = new LiteralExpression(null, DataTypes.STRING)
    val notEqualsExpression = new NotEqualsExpression(columnExpression, literalNullExpression)
    val equalsExpression = new NotEqualsExpression(columnExpression, literalValueExpression)
    val andExpression = new AndExpression(notEqualsExpression, equalsExpression)
    val resolveFilter: FilterResolverIntf = carbonTable.resolveFilter(andExpression)
    val exprWrapper = DataMapChooser.getDefaultDataMap(carbonTable, resolveFilter)
    val segment = new Segment("0")
    // get the pruned blocklets
    val prunedBlocklets = exprWrapper.prune(List(segment).asJava, null)
    prunedBlocklets.asScala.foreach { blocklet =>
      // all the blocklets should have useMinMaxForPrune flag set to true
      assert(blocklet.getDetailInfo.isUseMinMaxForPruning)
    }
  }

}
