/*
 * 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.core.preagg;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;

import org.apache.carbondata.common.logging.LogServiceFactory;
import org.apache.carbondata.core.constants.CarbonCommonConstants;
import org.apache.carbondata.core.util.CarbonProperties;

import org.apache.log4j.Logger;

/**
 * class for applying time-series udf
 */
public class TimeSeriesUDF {

  private static final Logger LOGGER =
      LogServiceFactory.getLogService(TimeSeriesUDF.class.getName());

  public final List<String> TIMESERIES_FUNCTION = new ArrayList<>();

  // thread local for keeping calender instance
  private ThreadLocal<Calendar> calenderThreadLocal = new ThreadLocal<>();

  /**
   * singleton instance
   */
  public static final TimeSeriesUDF INSTANCE = new TimeSeriesUDF();

  private TimeSeriesUDF() {
    initialize();
  }

  /**
   * Below method will be used to apply udf on data provided
   * Method will work based on below logic.
   * Data: 2016-7-23 01:01:30,10
   * Year Level UDF will return: 2016-1-1 00:00:00,0
   * Month Level UDF will return: 2016-7-1 00:00:00,0
   * Day Level UDF will return: 2016-7-23 00:00:00,0
   * Hour Level UDF will return: 2016-7-23 01:00:00,0
   * Minute Level UDF will return: 2016-7-23 01:01:00,0
   * Second Level UDF will return: 2016-7-23 01:01:30,0
   * If function does not match with any of the above functions
   * it will throw IllegalArgumentException
   *
   * @param data     timestamp data
   * @param function time series function name
   * @return data after applying udf
   */
  public Timestamp applyUDF(Timestamp data, String function) {
    if (null == data) {
      return data;
    }
    initialize();
    Calendar calendar = calenderThreadLocal.get();
    calendar.clear();
    calendar.setTimeInMillis(data.getTime());
    TimeSeriesFunctionEnum timeSeriesFunctionEnum =
        TimeSeriesFunctionEnum.valueOf(function.toUpperCase());
    switch (timeSeriesFunctionEnum) {
      case SECOND:
        calendar.set(Calendar.MILLISECOND, 0);
        break;
      case MINUTE:
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.SECOND, 0);
        break;
      case FIVE_MINUTE:
        setData(calendar, 5);
        break;
      case TEN_MINUTE:
        setData(calendar, 10);
        break;
      case FIFTEEN_MINUTE:
        setData(calendar, 15);
        break;
      case THIRTY_MINUTE:
        setData(calendar, 30);
        break;
      case WEEK:
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek());
        break;
      case HOUR:
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        break;
      case DAY:
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        break;
      case MONTH:
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        break;
      case YEAR:
        calendar.set(Calendar.MONTH, 1);
        calendar.set(Calendar.DAY_OF_YEAR, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        break;
      default:
        throw new IllegalArgumentException("Invalid time-series function name: " + function);
    }
    data.setTime(calendar.getTimeInMillis());
    return data;
  }

  /**
   * This method sets the calender data based on the interval time of granularity
   */
  private void setData(Calendar calendar, int intervalTime) {
    calendar.set(Calendar.MILLISECOND, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MINUTE, (calendar.get(Calendar.MINUTE) / intervalTime) * intervalTime);
  }

  /**
   * Below method will be used to initialize the thread local
   */
  private void initialize() {
    if (calenderThreadLocal.get() == null) {
      calenderThreadLocal.set(new GregorianCalendar());
    }
    if (TIMESERIES_FUNCTION.isEmpty()) {
      TIMESERIES_FUNCTION.add("second");
      TIMESERIES_FUNCTION.add("minute");
      TIMESERIES_FUNCTION.add("five_minute");
      TIMESERIES_FUNCTION.add("ten_minute");
      TIMESERIES_FUNCTION.add("fifteen_minute");
      TIMESERIES_FUNCTION.add("thirty_minute");
      TIMESERIES_FUNCTION.add("hour");
      TIMESERIES_FUNCTION.add("day");
      TIMESERIES_FUNCTION.add("week");
      TIMESERIES_FUNCTION.add("month");
      TIMESERIES_FUNCTION.add("year");
    }
    int firstDayOfWeek;
    try {
      firstDayOfWeek = DaysOfWeekEnum.valueOf(CarbonProperties.getInstance()
          .getProperty(CarbonCommonConstants.CARBON_TIMESERIES_FIRST_DAY_OF_WEEK,
              CarbonCommonConstants.CARBON_TIMESERIES_FIRST_DAY_OF_WEEK_DEFAULT).toUpperCase())
          .getOrdinal();
    } catch (IllegalArgumentException ex) {
      LOGGER.warn("Invalid value set for first of the week. Considering the default value as: "
          + CarbonCommonConstants.CARBON_TIMESERIES_FIRST_DAY_OF_WEEK_DEFAULT);
      firstDayOfWeek =
          DaysOfWeekEnum.valueOf(CarbonCommonConstants.CARBON_TIMESERIES_FIRST_DAY_OF_WEEK_DEFAULT)
              .getOrdinal();
    }
    calenderThreadLocal.get().setFirstDayOfWeek(firstDayOfWeek);
  }
}
