/*
 * 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.pulsar.client.impl;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class ClientCnxIdleState {

    private final ClientCnx clientCnx;

    /** Stat. **/
    private volatile State state;

    /** Create time. This field is only used to troubleshoot (analyze by dump file). **/
    private final long createTime;

    /** The time when marks the connection is idle. **/
    private volatile long idleMarkTime;

    public ClientCnxIdleState(ClientCnx clientCnx){
        this.clientCnx = clientCnx;
        this.createTime = System.currentTimeMillis();
        this.state = State.USING;
    }

    private static final AtomicReferenceFieldUpdater<ClientCnxIdleState, State> STATE_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(ClientCnxIdleState.class, State.class, "state");

    /**
     * Indicates the usage status of the connection and whether it has been released.
     */
    public enum State {
        /** It was using at the time of last check. At current time may be idle. **/
        USING,
        /** The connection is in idle. **/
        IDLE,
        /** The connection is in idle and will be released soon. In this state, the connection can still be used. **/
        RELEASING,
        /** The connection has already been released. **/
        RELEASED;
    }

    /**
     * Get idle-stat.
     * @return connection idle-stat
     */
    public State getIdleStat() {
        return STATE_UPDATER.get(this);
    }
    /**
     * Compare and switch idle-stat.
     * @return Whether the update is successful.Because there may be other threads competing, possible return false.
     */
    boolean compareAndSetIdleStat(State originalStat, State newStat) {
        return STATE_UPDATER.compareAndSet(this, originalStat, newStat);
    }

    /**
     * @return Whether this connection is in use.
     */
    public boolean isUsing() {
        return getIdleStat() == State.USING;
    }

    /**
     * @return Whether this connection is in idle.
     */
    public boolean isIdle() {
        return getIdleStat() == State.IDLE;
    }

    /**
     * @return Whether this connection is in idle and will be released soon.
     */
    public boolean isReleasing() {
        return getIdleStat() == State.RELEASING;
    }

    /**
     * @return Whether this connection has already been released.
     */
    public boolean isReleased() {
        return getIdleStat() == State.RELEASED;
    }

    /**
     * Try to transform the state of the connection to #{@link State#IDLE}, state should only be
     * transformed to #{@link State#IDLE} from state  #{@link State#USING}. if the state
     * is successfully transformed, "idleMarkTime" will be  assigned to current time.
     */
    public void tryMarkIdleAndInitIdleTime() {
        if (compareAndSetIdleStat(State.USING, State.IDLE)) {
            idleMarkTime = System.currentTimeMillis();
        }
    }

    /**
     * Changes the idle-state of the connection to #{@link State#USING} as much as possible, This method
     * is used when connection borrow, and reset {@link #idleMarkTime} if change state to
     * #{@link State#USING} success.
     * @return Whether change idle-stat to #{@link State#USING} success. False is returned only if the
     * connection has already been released.
     */
    public boolean tryMarkUsingAndClearIdleTime() {
        while (true) {
            // Ensure not released
            if (isReleased()) {
                return false;
            }
            // Try mark release
            if (compareAndSetIdleStat(State.IDLE, State.USING)) {
                idleMarkTime = 0;
                return true;
            }
            if (compareAndSetIdleStat(State.RELEASING, State.USING)) {
                idleMarkTime = 0;
                return true;
            }
            if (isUsing()){
                return true;
            }
        }
    }

    /**
     * Changes the idle-state of the connection to #{@link State#RELEASING}, This method only changes this
     * connection from the #{@link State#IDLE} state to the #{@link State#RELEASING} state.
     * @return Whether change idle-stat to #{@link State#RELEASING} success.
     */
    public boolean tryMarkReleasing() {
        return compareAndSetIdleStat(State.IDLE, State.RELEASING);
    }

    /**
     * Changes the idle-state of the connection to #{@link State#RELEASED}, This method only changes this
     * connection from the #{@link State#RELEASING} state to the #{@link State#RELEASED}
     * state, and close {@param clientCnx} if change state to #{@link State#RELEASED} success.
     * @return Whether change idle-stat to #{@link State#RELEASED} and close connection success.
     */
    public boolean tryMarkReleasedAndCloseConnection() {
        if (!compareAndSetIdleStat(State.RELEASING, State.RELEASED)) {
            return false;
        }
        clientCnx.close();
        return true;
    }

    /**
     * Check whether the connection is idle, and if so, set the idle-state to #{@link State#IDLE}.
     * If the state is already idle and the {@param maxIdleSeconds} is reached, set the state to
     * #{@link State#RELEASING}.
     */
    public void doIdleDetect(long maxIdleSeconds) {
        if (isReleasing()) {
            return;
        }
        if (isIdle()) {
            // check if the connection is still idle, if not, mark it as using
            if (!clientCnx.idleCheck() && compareAndSetIdleStat(State.IDLE, State.USING)) {
                idleMarkTime = 0;
                return;
            }
            if (maxIdleSeconds * 1000 + idleMarkTime < System.currentTimeMillis()) {
                tryMarkReleasing();
            }
            return;
        }
        if (clientCnx.idleCheck()) {
            tryMarkIdleAndInitIdleTime();
        }
    }
}
