/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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 cn.yyxx.support.volley.source;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;

/**
 * Callback to notify the caller when the network request returns. Valid responses can be used by
 * all duplicate requests.
 */
class WaitingRequestManager implements Request.NetworkRequestCompleteListener {

    /**
     * Staging area for requests that already have a duplicate request in flight.
     *
     * <ul>
     *   <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
     *       key.
     *   <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
     *       is <em>not</em> contained in that list. Is null if no requests are staged.
     * </ul>
     */
    private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();

    private final ResponseDelivery mResponseDelivery;

    /**
     * RequestQueue that is passed in by the AsyncRequestQueue. This is null when this instance is
     * initialized by the {@link CacheDispatcher}
     */
    @Nullable
    private final RequestQueue mRequestQueue;

    /**
     * CacheDispacter that is passed in by the CacheDispatcher. This is null when this instance is
     * initialized by the {@link AsyncRequestQueue}
     */
    @Nullable
    private final CacheDispatcher mCacheDispatcher;

    /**
     * BlockingQueue that is passed in by the CacheDispatcher. This is null when this instance is
     * initialized by the {@link AsyncRequestQueue}
     */
    @Nullable
    private final BlockingQueue<Request<?>> mNetworkQueue;

    WaitingRequestManager(@NonNull RequestQueue requestQueue) {
        mRequestQueue = requestQueue;
        mResponseDelivery = mRequestQueue.getResponseDelivery();
        mCacheDispatcher = null;
        mNetworkQueue = null;
    }

    WaitingRequestManager(
            @NonNull CacheDispatcher cacheDispatcher,
            @NonNull BlockingQueue<Request<?>> networkQueue,
            ResponseDelivery responseDelivery) {
        mRequestQueue = null;
        mResponseDelivery = responseDelivery;
        mCacheDispatcher = cacheDispatcher;
        mNetworkQueue = networkQueue;
    }

    /**
     * Request received a valid response that can be used by other waiting requests.
     */
    @Override
    public void onResponseReceived(Request<?> request, Response<?> response) {
        if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
            onNoUsableResponseReceived(request);
            return;
        }
        String cacheKey = request.getCacheKey();
        List<Request<?>> waitingRequests;
        synchronized (this) {
            waitingRequests = mWaitingRequests.remove(cacheKey);
        }
        if (waitingRequests != null) {
            if (VolleyLog.DEBUG) {
                VolleyLog.v(
                        "Releasing %d waiting requests for cacheKey=%s.",
                        waitingRequests.size(), cacheKey);
            }
            // Process all queued up requests.
            for (Request<?> waiting : waitingRequests) {
                mResponseDelivery.postResponse(waiting, response);
            }
        }
    }

    /**
     * No valid response received from network, release waiting requests.
     */
    @Override
    public synchronized void onNoUsableResponseReceived(Request<?> request) {
        String cacheKey = request.getCacheKey();
        List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
        if (waitingRequests != null && !waitingRequests.isEmpty()) {
            if (VolleyLog.DEBUG) {
                VolleyLog.v(
                        "%d waiting requests for cacheKey=%s; resend to network",
                        waitingRequests.size(), cacheKey);
            }
            Request<?> nextInLine = waitingRequests.remove(0);
            mWaitingRequests.put(cacheKey, waitingRequests);
            nextInLine.setNetworkRequestCompleteListener(this);
            // RequestQueue will be non-null if this instance was created in AsyncRequestQueue.
            if (mRequestQueue != null) {
                // Will send the network request from the RequestQueue.
                mRequestQueue.sendRequestOverNetwork(nextInLine);
            } else if (mCacheDispatcher != null && mNetworkQueue != null) {
                // If we're not using the AsyncRequestQueue, then submit it to the network queue.
                try {
                    mNetworkQueue.put(nextInLine);
                } catch (InterruptedException iex) {
                    VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
                    // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
                    Thread.currentThread().interrupt();
                    // Quit the current CacheDispatcher thread.
                    mCacheDispatcher.quit();
                }
            }
        }
    }

    /**
     * For cacheable requests, if a request for the same cache key is already in flight, add it to a
     * queue to wait for that in-flight request to finish.
     *
     * @return whether the request was queued. If false, we should continue issuing the request over
     * the network. If true, we should put the request on hold to be processed when the
     * in-flight request finishes.
     */
    synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
        String cacheKey = request.getCacheKey();
        // Insert request into stage if there's already a request with the same cache key
        // in flight.
        if (mWaitingRequests.containsKey(cacheKey)) {
            // There is already a request in flight. Queue up.
            List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
            if (stagedRequests == null) {
                stagedRequests = new ArrayList<>();
            }
            request.addMarker("waiting-for-response");
            stagedRequests.add(request);
            mWaitingRequests.put(cacheKey, stagedRequests);
            if (VolleyLog.DEBUG) {
                VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
            }
            return true;
        } else {
            // Insert 'null' queue for this cacheKey, indicating there is now a request in
            // flight.
            mWaitingRequests.put(cacheKey, null);
            request.setNetworkRequestCompleteListener(this);
            if (VolleyLog.DEBUG) {
                VolleyLog.d("new request, sending to network %s", cacheKey);
            }
            return false;
        }
    }
}
