/*
 * 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 com.groupon.mesos.scheduler;

import static com.google.common.base.Preconditions.checkNotNull;

import static org.apache.mesos.Protos.Status.DRIVER_ABORTED;
import static org.apache.mesos.Protos.Status.DRIVER_NOT_STARTED;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.groupon.mesos.util.Log;
import com.groupon.mesos.util.NetworkUtil;
import com.groupon.mesos.util.UPID;

import org.apache.mesos.Protos;
import org.apache.mesos.Protos.FrameworkID;
import org.apache.mesos.Protos.FrameworkInfo;
import org.apache.mesos.Protos.MasterInfo;
import org.apache.mesos.Protos.OfferID;
import org.apache.mesos.Protos.SlaveID;
import org.apache.mesos.Protos.Status;

/**
 * Glues all the pieces of the scheduler together, keeps track of state etc. This is a too big and random collection of things.
 */
class SchedulerDriverContext
{
    private static final Log LOG = Log.getLog(SchedulerDriverContext.class);

    private final AtomicReference<Status> stateMachine = new AtomicReference<>(DRIVER_NOT_STARTED);
    private final AtomicReference<FrameworkInfo> frameworkInfo = new AtomicReference<>();
    private final AtomicReference<MasterInfo> masterInfo = new AtomicReference<>();
    private final AtomicReference<UPID> masterUpid = new AtomicReference<>();

    private final BlockingQueue<SettableFuture<Status>> stateMachineFutures = new LinkedBlockingQueue<>();

    private final AtomicBoolean connected = new AtomicBoolean();
    private final AtomicBoolean failover = new AtomicBoolean(false);
    private final UPID driverUpid;

    private final Table<OfferID, SlaveID, UPID> offerCache = HashBasedTable.create();
    private final Map<SlaveID, UPID> slaveCache = new ConcurrentHashMap<>(16, 0.75f, 3);

    SchedulerDriverContext(final FrameworkInfo frameworkInfo)
                    throws IOException
    {
        this.frameworkInfo.set(checkNotNull(frameworkInfo, "frameworkInfo is null"));

        this.driverUpid = UPID.fromParts(UUID.randomUUID().toString(),
            HostAndPort.fromParts(frameworkInfo.getHostname(), NetworkUtil.findUnusedPort()));

        // If the framework info sent in has an id, we are in failover mode from the start.
        failover.set(hasFrameworkId(frameworkInfo));
    }

    UPID getDriverUPID()
    {
        return driverUpid;
    }

    //
    // connected status
    //

    boolean connected()
    {
        return connected.getAndSet(true);
    }

    /**
     * Disconnect and return the previous state.
     */
    boolean disconnected()
    {
        return connected.getAndSet(false);
    }

    boolean isConnected()
    {
        return connected.get();
    }

    //
    // failover status
    //

    void setFailover(final boolean failover)
    {
        this.failover.set(failover);
    }

    boolean isFailover()
    {
        return failover.get();
    }

    //
    // Master information
    //

    synchronized MasterInfo getMaster()
    {
        return masterInfo.get();
    }

    synchronized MasterInfo connectedMaster()
    {
        if (isStateMachine(DRIVER_ABORTED)) {
            LOG.debug("driver is aborted!");
            return null;
        }

        if (!isConnected()) {
            LOG.debug("Not connected!");
            return null;
        }

        return masterInfo.get();
    }

    synchronized void setMaster(final MasterInfo newMasterInfo)
    {
        masterInfo.set(newMasterInfo);
        masterUpid.set(newMasterInfo == null ? null : UPID.create(newMasterInfo.getPid()));
    }

    synchronized UPID getMasterUPID()
    {
        return masterUpid.get();
    }

    void setFrameworkId(final FrameworkID frameworkId)
    {
        checkNotNull(frameworkId, "frameworkId is null");

        frameworkInfo.set(FrameworkInfo.newBuilder(frameworkInfo.get())
            .setId(frameworkId)
            .build());
    }

    //
    // Framework Id
    //

    FrameworkID getFrameworkId()
    {
        return frameworkInfo.get().getId();
    }

    boolean hasFrameworkId()
    {
        return hasFrameworkId(frameworkInfo.get());
    }

    FrameworkInfo getFrameworkInfo()
    {
        return frameworkInfo.get();
    }

    //
    // State machine management
    //

    synchronized void setStateMachine(final Status status)
    {
        final Status oldStatus = stateMachine.getAndSet(status);

        if (status != oldStatus) {
            // Fire all the futures waiting for a status change.
            final List<SettableFuture<Status>> settableFutures = new ArrayList<>(stateMachineFutures.size());
            stateMachineFutures.drainTo(settableFutures);

            for (final SettableFuture<Status> future : settableFutures) {
                future.set(status);
            }
        }
    }

    synchronized Status getStateMachine()
    {
        return stateMachine.get();
    }

    synchronized ListenableFuture<Status> waitForStateChange(final Status expectedStatus)
    {
        final SettableFuture<Status> future = SettableFuture.create();
        if (!isStateMachine(expectedStatus)) {
            // Current status is not the expected status. Return
            // it immediately.
            future.set(stateMachine.get());
        }
        else {
            // Current status is expected status: Queue up for a status change.
            stateMachineFutures.add(future);
        }
        return future;
    }

    synchronized boolean isStateMachine(final Protos.Status ... statusWanted)
    {
        final Protos.Status currentState = stateMachine.get();
        for (final Protos.Status status : statusWanted) {
            if (currentState == status) {
                return true;
            }
        }
        return false;
    }

    //
    // Offer cache management
    //

    void addOffer(final OfferID offerId, final SlaveID slaveId, final UPID pid)
    {
        synchronized (offerCache) {
            offerCache.put(offerId, slaveId, pid);
        }
    }

    void removeAllOffers(final OfferID offerId)
    {
        synchronized (offerCache) {
            offerCache.row(offerId).clear();
        }
    }

    boolean hasOffers(final OfferID offerId)
    {
        synchronized (offerCache) {
            return offerCache.containsRow(offerId);
        }
    }

    boolean hasOffer(final OfferID offerId, final SlaveID slaveId)
    {
        synchronized (offerCache) {
            return offerCache.contains(offerId, slaveId);
        }
    }

    UPID getOffer(final OfferID offerId, final SlaveID slaveId)
    {
        synchronized (offerCache) {
            return offerCache.get(offerId, slaveId);
        }
    }

    //
    // Slave cache management
    //

    void addSlave(final SlaveID slaveId, final UPID upid)
    {
        slaveCache.put(slaveId, upid);
    }

    void removeSlave(final SlaveID slaveId)
    {
        slaveCache.remove(slaveId);
    }

    boolean containsSlave(final SlaveID slaveId)
    {
        return slaveCache.containsKey(slaveId);
    }

    UPID getSlaveUPID(final SlaveID slaveId)
    {
        return slaveCache.get(slaveId);
    }

    /**
     * Static helper for use in the c'tor
     */
    private static boolean hasFrameworkId(final FrameworkInfo frameworkInfo)
    {
        return frameworkInfo.hasId() && !"".equals(frameworkInfo.getId().getValue());
    }
}