/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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 io.undertow.server.handlers.proxy.mod_cluster;

import static org.xnio.Bits.allAreClear;
import static org.xnio.Bits.allAreSet;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

import io.undertow.server.ExchangeCompletionListener;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.proxy.ProxyCallback;
import io.undertow.server.handlers.proxy.ProxyConnection;

/**
 *
 * @author Emanuel Muckenhuber
 */
class Context {

    private static final AtomicInteger idGen = new AtomicInteger();

    enum Status {

        ENABLED,
        DISABLED,
        STOPPED,
        ;

    }

    private final int id;
    private final Node node;
    private final String path;
    private final Node.VHostMapping vhost;

    private static final int STOPPED = (1 << 31);
    private static final int DISABLED = (1 << 30);
    private static final int REQUEST_MASK = ((1 << 30) - 1);

    private volatile int state = STOPPED;
    private static final AtomicIntegerFieldUpdater<Context> stateUpdater = AtomicIntegerFieldUpdater.newUpdater(Context.class, "state");

    Context(final String path, final Node.VHostMapping vHost, final Node node) {
        id = idGen.incrementAndGet();
        this.path = path;
        this.node = node;
        this.vhost = vHost;
    }

    public int getId() {
        return id;
    }

    public String getJVMRoute() {
        return node.getJvmRoute();
    }

    public String getPath() {
        return path;
    }

    public List<String> getVirtualHosts() {
        return vhost.getAliases();
    }

    public int getActiveRequests() {
        return state & REQUEST_MASK;
    }

    public Status getStatus() {
        final int state = this.state;
        if ((state & STOPPED) == STOPPED) {
            return Status.STOPPED;
        } else if ((state & DISABLED) == DISABLED) {
            return Status.DISABLED;
        }
        return Status.ENABLED;
    }

    public boolean isEnabled() {
        return allAreClear(state, DISABLED | STOPPED);
    }

    public boolean isStopped() {
        return allAreSet(state, STOPPED);
    }

    public boolean isDisabled() {
        return allAreSet(state, DISABLED);
    }

    Node getNode() {
        return node;
    }

    Node.VHostMapping getVhost() {
        return vhost;
    }

    boolean checkAvailable(boolean existingSession) {
        if (node.checkAvailable(existingSession)) {
            return existingSession ? !isStopped() : isEnabled();
        }
        return false;
    }

    void enable() {
        int oldState, newState;
        for (;;) {
            oldState = this.state;
            newState = oldState & ~(STOPPED | DISABLED);
            if (stateUpdater.compareAndSet(this, oldState, newState)) {
                return;
            }
        }
    }

    void disable() {
        int oldState, newState;
        for (;;) {
            oldState = this.state;
            newState = oldState | DISABLED;
            if (stateUpdater.compareAndSet(this, oldState, newState)) {
                return;
            }
        }
    }

    void stop() {
        int oldState, newState;
        for (;;) {
            oldState = this.state;
            newState = oldState | STOPPED;
            if (stateUpdater.compareAndSet(this, oldState, newState)) {
                return;
            }
        }
    }

    /**
     * Handle a proxy request for this context.
     *
     * @param target       the proxy target
     * @param exchange     the http server exchange
     * @param callback     the proxy callback
     * @param timeout      the timeout
     * @param timeUnit     the time unit
     * @param exclusive    whether this connection is exclusive
     */
    void handleRequest(final ModClusterProxyTarget target, final HttpServerExchange exchange, final ProxyCallback<ProxyConnection> callback, long timeout, TimeUnit timeUnit, boolean exclusive) {
        if (addRequest()) {
            exchange.addExchangeCompleteListener(new ExchangeCompletionListener() {
                @Override
                public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) {
                    requestDone();
                    nextListener.proceed();
                }
            });
            node.getConnectionPool().connect(target, exchange, callback, timeout, timeUnit, exclusive);
        } else {
            callback.failed(exchange);
        }
    }

    boolean addRequest() {
        int oldState, newState;
        for (;;) {
            oldState = this.state;
            if ((oldState & STOPPED) != 0) {
                return false;
            }
            newState = oldState + 1;
            if ((newState & REQUEST_MASK) == REQUEST_MASK) {
                return false;
            }
            if (stateUpdater.compareAndSet(this, oldState, newState)) {
                return true;
            }
        }
    }

    void requestDone() {
        int oldState, newState;
        for (;;) {
            oldState = this.state;
            if ((oldState & REQUEST_MASK) == 0) {
                return;
            }
            newState = oldState - 1;
            if (stateUpdater.compareAndSet(this, oldState, newState)) {
                return;
            }
        }
    }

    @Override
    public String toString() {
        return "Context{" +
                ", path='" + path + '\'' +
                '}';
    }
}
