/**
 * Copyright 2010-present Facebook.
 *
 * 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.facebook.widget;

import android.content.Context;
import android.os.Handler;
import android.support.v4.content.Loader;
import com.facebook.*;
import com.facebook.internal.CacheableRequestBatch;
import com.facebook.model.GraphObject;
import com.facebook.model.GraphObjectList;

class GraphObjectPagingLoader<T extends GraphObject> extends Loader<SimpleGraphObjectCursor<T>> {
    private final Class<T> graphObjectClass;
    private boolean skipRoundtripIfCached;
    private Request originalRequest;
    private Request currentRequest;
    private Request nextRequest;
    private OnErrorListener onErrorListener;
    private SimpleGraphObjectCursor<T> cursor;
    private boolean appendResults = false;
    private boolean loading = false;

    public interface OnErrorListener {
        public void onError(FacebookException error, GraphObjectPagingLoader<?> loader);
    }

    public GraphObjectPagingLoader(Context context, Class<T> graphObjectClass) {
        super(context);

        this.graphObjectClass = graphObjectClass;
    }

    public OnErrorListener getOnErrorListener() {
        return onErrorListener;
    }

    public void setOnErrorListener(OnErrorListener listener) {
        this.onErrorListener = listener;
    }

    public SimpleGraphObjectCursor<T> getCursor() {
        return cursor;
    }

    public void clearResults() {
        nextRequest = null;
        originalRequest = null;
        currentRequest = null;

        deliverResult(null);
    }

    public boolean isLoading() {
        return loading;
    }

    public void startLoading(Request request, boolean skipRoundtripIfCached) {
        originalRequest = request;
        startLoading(request, skipRoundtripIfCached, 0);
    }

    public void refreshOriginalRequest(long afterDelay) {
        if (originalRequest == null) {
            throw new FacebookException(
                    "refreshOriginalRequest may not be called until after startLoading has been called.");
        }
        startLoading(originalRequest, false, afterDelay);
    }

    public void followNextLink() {
        if (nextRequest != null) {
            appendResults = true;
            currentRequest = nextRequest;

            currentRequest.setCallback(new Request.Callback() {
                @Override
                public void onCompleted(Response response) {
                    requestCompleted(response);
                }
            });

            loading = true;
            CacheableRequestBatch batch = putRequestIntoBatch(currentRequest, skipRoundtripIfCached);
            Request.executeBatchAsync(batch);
        }
    }

    @Override
    public void deliverResult(SimpleGraphObjectCursor<T> cursor) {
        SimpleGraphObjectCursor<T> oldCursor = this.cursor;
        this.cursor = cursor;

        if (isStarted()) {
            super.deliverResult(cursor);

            if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
                oldCursor.close();
            }
        }
    }

    @Override
    protected void onStartLoading() {
        super.onStartLoading();

        if (cursor != null) {
            deliverResult(cursor);
        }
    }

    private void startLoading(Request request, boolean skipRoundtripIfCached, long afterDelay) {
        this.skipRoundtripIfCached = skipRoundtripIfCached;
        appendResults = false;
        nextRequest = null;
        currentRequest = request;
        currentRequest.setCallback(new Request.Callback() {
            @Override
            public void onCompleted(Response response) {
                requestCompleted(response);
            }
        });

        // We are considered loading even if we have a delay.
        loading = true;

        final RequestBatch batch = putRequestIntoBatch(request, skipRoundtripIfCached);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Request.executeBatchAsync(batch);
            }
        };
        if (afterDelay == 0) {
            r.run();
        } else {
            Handler handler = new Handler();
            handler.postDelayed(r, afterDelay);
        }
    }

    private CacheableRequestBatch putRequestIntoBatch(Request request, boolean skipRoundtripIfCached) {
        // We just use the request URL as the cache key.
        CacheableRequestBatch batch = new CacheableRequestBatch(request);
        // We use the default cache key (request URL).
        batch.setForceRoundTrip(!skipRoundtripIfCached);
        return batch;
    }

    private void requestCompleted(Response response) {
        Request request = response.getRequest();
        if (request != currentRequest) {
            return;
        }

        loading = false;
        currentRequest = null;

        FacebookRequestError requestError = response.getError();
        FacebookException exception = (requestError == null) ? null : requestError.getException();
        if (response.getGraphObject() == null && exception == null) {
            exception = new FacebookException("GraphObjectPagingLoader received neither a result nor an error.");
        }

        if (exception != null) {
            nextRequest = null;

            if (onErrorListener != null) {
                onErrorListener.onError(exception, this);
            }
        } else {
            addResults(response);
        }
    }

    private void addResults(Response response) {
        SimpleGraphObjectCursor<T> cursorToModify = (cursor == null || !appendResults) ? new SimpleGraphObjectCursor<T>() :
                new SimpleGraphObjectCursor<T>(cursor);

        PagedResults result = response.getGraphObjectAs(PagedResults.class);
        boolean fromCache = response.getIsFromCache();

        GraphObjectList<T> data = result.getData().castToListOf(graphObjectClass);
        boolean haveData = data.size() > 0;

        if (haveData) {
            nextRequest = response.getRequestForPagedResults(Response.PagingDirection.NEXT);

            cursorToModify.addGraphObjects(data, fromCache);
            if (nextRequest != null) {
                cursorToModify.setMoreObjectsAvailable(true);
            } else {
                cursorToModify.setMoreObjectsAvailable(false);
            }
        }

        if (!haveData) {
            cursorToModify.setMoreObjectsAvailable(false);
            cursorToModify.setFromCache(fromCache);

            nextRequest = null;
        }

        // Once we get any set of results NOT from the cache, stop trying to get any future ones
        // from it.
        if (!fromCache) {
            skipRoundtripIfCached = false;
        }

        deliverResult(cursorToModify);
    }

    interface PagedResults extends GraphObject {
        GraphObjectList<GraphObject> getData();
    }
}
