/*
 * 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.iotdb.db.mpp.execution;

import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.mpp.common.QueryId;

import javax.annotation.concurrent.GuardedBy;

import java.time.Instant;
import java.time.format.DateTimeFormatter;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly;
import static java.lang.String.format;
import static java.time.ZoneOffset.UTC;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

public class QueryIdGenerator {

  private static final DateTimeFormatter TIMESTAMP_FORMAT =
      DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(UTC);
  private static final long BASE_SYSTEM_TIME_MILLIS = System.currentTimeMillis();
  private static final long BASE_NANO_TIME = System.nanoTime();

  private final String dataNodeId;

  @GuardedBy("this")
  private long lastTimeInDays;

  @GuardedBy("this")
  private long lastTimeInSeconds;

  @GuardedBy("this")
  private String lastTimestamp;

  @GuardedBy("this")
  private int counter;

  public QueryIdGenerator() {
    int dataNodeId = IoTDBDescriptor.getInstance().getConfig().getDataNodeId();
    checkArgument(dataNodeId != -1, "DataNodeId should be init first!");
    this.dataNodeId = String.valueOf(dataNodeId);
  }

  public String getCoordinatorId() {
    return dataNodeId;
  }

  /**
   * Generate next queryId using the following format: {@code yyyyMMdd_HHmmss_index_dataNodeId}
   *
   * <p>{@code index} rolls at the start of every day or when it is close to reaching {@code
   * 99,999}. {@code dataNodeId} is a unique id generated by config node when this data node is
   * created.
   */
  public synchronized QueryId createNextQueryId() {
    long now = nowInMillis();

    // restart counter if it is at the limit
    if (counter > 99_999) {
      // Wait for the timestamp to change to reset the counter to avoid duplicates
      // in the very unlikely case we generated all 100,000 in the previous second
      while (MILLISECONDS.toSeconds(now) == lastTimeInSeconds) {
        long delta = SECONDS.toMillis(lastTimeInSeconds + 1) - now;
        sleepUninterruptibly(delta, MILLISECONDS);
        now = nowInMillis();
      }
      counter = 0;
    }

    // if the second changed since the last ID was generated, generate a new timestamp
    if (MILLISECONDS.toSeconds(now) != lastTimeInSeconds) {
      // generate new timestamp
      lastTimeInSeconds = MILLISECONDS.toSeconds(now);
      lastTimestamp = formatEpochMilli(now);

      // if the day has rolled over, restart the counter
      if (MILLISECONDS.toDays(now) != lastTimeInDays) {
        lastTimeInDays = MILLISECONDS.toDays(now);
        counter = 0;
      }

      // restart the counter if it is close to the limit, to avoid sleep on rollover
      if (counter >= 90_000) {
        counter = 0;
      }
    }

    long index = counter;
    counter++;

    return new QueryId(format("%s_%05d_%s", lastTimestamp, index, dataNodeId));
  }

  private static String formatEpochMilli(long milli) {
    return TIMESTAMP_FORMAT.format(Instant.ofEpochMilli(milli));
  }

  protected long nowInMillis() {
    // avoid problems with the clock moving backwards
    return BASE_SYSTEM_TIME_MILLIS + NANOSECONDS.toMillis(System.nanoTime() - BASE_NANO_TIME);
  }
}
