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

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCache.StartMode;
import org.apache.fluo.accumulo.util.LongUtil;
import org.apache.fluo.accumulo.util.ZookeeperPath;
import org.apache.fluo.api.config.FluoConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides cache of all Fluo transactors. Used by clients to determine if transactor is running.
 */
public final class TransactorCache implements AutoCloseable {

  public enum TcStatus {
    OPEN, CLOSED
  }

  private final Environment env;
  private final PathChildrenCache cache;
  private final Cache<Long, AtomicLong> timeoutCache;
  private TcStatus status;

  private static final Logger log = LoggerFactory.getLogger(TransactorCache.class);

  public TransactorCache(Environment env) {
    final FluoConfiguration conf = env.getConfiguration();

    timeoutCache =
        CacheBuilder.newBuilder().maximumSize(FluoConfigurationImpl.getTransactorMaxCacheSize(conf))
            .expireAfterAccess(
                FluoConfigurationImpl.getTransactorCacheTimeout(conf, TimeUnit.MILLISECONDS),
                TimeUnit.MILLISECONDS)
            .concurrencyLevel(10).build();

    this.env = env;
    cache = new PathChildrenCache(env.getSharedResources().getCurator(),
        ZookeeperPath.TRANSACTOR_NODES, true);
    try {
      cache.start(StartMode.BUILD_INITIAL_CACHE);
      status = TcStatus.OPEN;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private void logTimedoutTransactor(Long transactorId, long lockTs, Long startTime) {
    log.warn("Transactor ID {} was unresponsive for {} secs, marking as dead for lockTs <= {}",
        LongUtil.toMaxRadixString(transactorId), (System.currentTimeMillis() - startTime) / 1000.0,
        lockTs);
  }

  public void addTimedoutTransactor(final Long transactorId, final long lockTs,
      final Long startTime) {
    try {
      AtomicLong cachedLockTs = timeoutCache.get(transactorId, () -> {
        logTimedoutTransactor(transactorId, lockTs, startTime);
        return new AtomicLong(lockTs);
      });

      long currVal = cachedLockTs.get();

      while (lockTs > currVal) {
        if (cachedLockTs.compareAndSet(currVal, lockTs)) {
          logTimedoutTransactor(transactorId, lockTs, startTime);
        }

        // its possible another thread updates and the above compare and set failed, so the
        // following will get us out of loop in this case... it will also get
        // us out of loop in case where compared and set succeeds
        currVal = cachedLockTs.get();
      }

    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    }
  }

  public boolean checkTimedout(Long transactorId, long lockTs) {
    AtomicLong timedoutLockTs = timeoutCache.getIfPresent(transactorId);
    return timedoutLockTs != null && lockTs <= timedoutLockTs.get();
  }

  public boolean checkExists(Long transactorId) {
    return cache.getCurrentData(TransactorNode.getNodePath(env, transactorId)) != null;
  }

  public TcStatus getStatus() {
    return status;
  }

  @Override
  public void close() {
    status = TcStatus.CLOSED;
    try {
      cache.close();
    } catch (IOException e) {
      log.error("Failed to close cache");
      throw new IllegalStateException(e);
    }
  }
}
