// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.offlinepages.evaluation;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.offlinepages.ClientId;
import org.chromium.chrome.browser.offlinepages.OfflinePageItem;
import org.chromium.chrome.browser.offlinepages.SavePageRequest;
import org.chromium.chrome.browser.profiles.Profile;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * Class used for offline page evaluation testing tools.
 */
@JNINamespace("offline_pages::android")
public class OfflinePageEvaluationBridge {
    /**
     * Observer class for notifications on changes to save page requests or offline page model which
     * are used for testing.
     */
    public abstract static class OfflinePageEvaluationObserver {
        /**
         * Event fired when the offline page model is loaded.
         */
        public void offlinePageModelLoaded() {}

        /**
         * Event fired when a new request is added.
         * @param request The newly added save page request.
         */
        public void savePageRequestAdded(SavePageRequest request) {}

        /**
         * Event fired when a request is completed.
         * @param request The completed request.
         * @param status The status of the completion, see
         * org.chromium.components.offlinepages.BackgroundSavePageResult.
         */
        public void savePageRequestCompleted(SavePageRequest request, int status) {}

        /**
         * Event fired when a new request is changed.
         * @param request The changed request.
         */
        public void savePageRequestChanged(SavePageRequest request) {}
    }

    /**
     * Get the instance of the evaluation bridge.
     * @param profile The profile used to get bridge.
     * @param useEvaluationScheduler True if using the evaluation scheduler instead of the
     *                               GCMNetworkManager one.
     */
    public OfflinePageEvaluationBridge(Profile profile, boolean useEvaluationScheduler) {
        ThreadUtils.assertOnUiThread();
        mNativeOfflinePageEvaluationBridge =
                OfflinePageEvaluationBridgeJni.get().createBridgeForProfile(
                        OfflinePageEvaluationBridge.this, profile, useEvaluationScheduler);
    }

    private static final String TAG = "OPEvalBridge";

    private static TaskRunner sSequencedTaskRunner =
            PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK);

    private long mNativeOfflinePageEvaluationBridge;
    private boolean mIsOfflinePageModelLoaded;
    private ObserverList<OfflinePageEvaluationObserver> mObservers =
            new ObserverList<OfflinePageEvaluationObserver>();

    private OutputStreamWriter mLogOutput;

    /** Destroys the native portion of the bridge. */
    public void destroy() {
        if (mNativeOfflinePageEvaluationBridge != 0) {
            OfflinePageEvaluationBridgeJni.get().destroy(
                    mNativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge.this);
            mNativeOfflinePageEvaluationBridge = 0;
            mIsOfflinePageModelLoaded = false;
        }
        mObservers.clear();
    }

    /**
     * Add an observer of the evaluation events.
     */
    public void addObserver(OfflinePageEvaluationObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * Remove an observer of evaluation events.
     */
    public void removeObserver(OfflinePageEvaluationObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Gets all pages in offline page model.
     * @param callback The callback would be invoked after the action completes and return with a
     *                 list of pages.
     */
    public void getAllPages(final Callback<List<OfflinePageItem>> callback) {
        List<OfflinePageItem> result = new ArrayList<>();
        OfflinePageEvaluationBridgeJni.get().getAllPages(mNativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge.this, result, callback);
    }

    /**
     * Saves a url as offline page async.
     * @param url The url of the web page.
     * @param namespace The namespace to which the page belongs.
     * @param userRequest True if it's user-requested page.
     */
    public void savePageLater(final String url, final String namespace, boolean userRequested) {
        ClientId clientId = ClientId.createGuidClientIdForNamespace(namespace);
        OfflinePageEvaluationBridgeJni.get().savePageLater(mNativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge.this, url, namespace, clientId.getId(), userRequested);
    }

    /**
     * Forces request coordinator to process the requests in the queue.
     * @param callback The callback would be invoked after the operation completes.
     * @return True if processing starts successfully and callback is expected to be called, false
     * otherwise.
     */
    public boolean pushRequestProcessing(final Callback<Boolean> callback) {
        return OfflinePageEvaluationBridgeJni.get().pushRequestProcessing(
                mNativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge.this, callback);
    }

    /**
     * Gets all requests in the queue.
     * @param callback The callback would be invoked with a list of requests which are in the queue.
     */
    public void getRequestsInQueue(Callback<SavePageRequest[]> callback) {
        OfflinePageEvaluationBridgeJni.get().getRequestsInQueue(
                mNativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge.this, callback);
    }

    /**
     * Removes requests from the queue by request ids.
     * @param requestIds The list of request ids to be deleted.
     * @param callback The callback would be invoked with number of successfully deleted ids.
     */
    public void removeRequestsFromQueue(List<Long> requestIds, Callback<Integer> callback) {
        long[] ids = new long[requestIds.size()];
        for (int i = 0; i < requestIds.size(); i++) {
            ids[i] = requestIds.get(i);
        }
        OfflinePageEvaluationBridgeJni.get().removeRequestsFromQueue(
                mNativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge.this, ids,
                callback);
    }

    public void setLogOutputFile(File outputFile) throws IOException {
        // This open file operation shouldn't happen on UI thread.
        assert !ThreadUtils.runningOnUiThread();
        mLogOutput = new FileWriter(outputFile);
    }

    /**
     * @return True if the offline page model has fully loaded.
     */
    public boolean isOfflinePageModelLoaded() {
        return mIsOfflinePageModelLoaded;
    }

    @CalledByNative
    public void log(String sourceTag, String message) {
        Date date = new Date(System.currentTimeMillis());
        SimpleDateFormat formatter =
                new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.getDefault());
        String logString = formatter.format(date) + ": " + sourceTag + " | " + message
                + System.getProperty("line.separator");
        Log.d(TAG, logString);
        sSequencedTaskRunner.postTask(() -> {
            try {
                mLogOutput.write(logString);
                mLogOutput.flush();
            } catch (IOException e) {
                Log.e(TAG, e.getMessage(), e);
            }
        });
    }

    public void closeLog() {
        try {
            mLogOutput.close();
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    @CalledByNative
    void savePageRequestAdded(SavePageRequest request) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestAdded(request);
        }
    }

    @CalledByNative
    void savePageRequestCompleted(SavePageRequest request, int status) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestCompleted(request, status);
        }
    }

    @CalledByNative
    void savePageRequestChanged(SavePageRequest request) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestChanged(request);
        }
    }

    @CalledByNative
    void offlinePageModelLoaded() {
        mIsOfflinePageModelLoaded = true;
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.offlinePageModelLoaded();
        }
    }

    @CalledByNative
    private static void createOfflinePageAndAddToList(List<OfflinePageItem> offlinePagesList,
            String url, long offlineId, String clientNamespace, String clientId, String title,
            String filePath, long fileSize, long creationTime, int accessCount,
            long lastAccessTimeMs, String requestOrigin) {
        offlinePagesList.add(createOfflinePageItem(url, offlineId, clientNamespace, clientId, title,
                filePath, fileSize, creationTime, accessCount, lastAccessTimeMs, requestOrigin));
    }

    // This is added as a utility method in the bridge because SavePageRequest_jni.h is supposed
    // only to be included in one bridge (OfflinePageBridge). So as a testing bridge, this will be
    // used to create SavePageRequest on the native side.
    @CalledByNative
    private static SavePageRequest createSavePageRequest(
            int state, long requestId, String url, String clientIdNamespace, String clientIdId) {
        return SavePageRequest.create(state, requestId, url, clientIdNamespace, clientIdId);
    }

    private static OfflinePageItem createOfflinePageItem(String url, long offlineId,
            String clientNamespace, String clientId, String title, String filePath, long fileSize,
            long creationTime, int accessCount, long lastAccessTimeMs, String requestOrigin) {
        return new OfflinePageItem(url, offlineId, clientNamespace, clientId, title, filePath,
                fileSize, creationTime, accessCount, lastAccessTimeMs, requestOrigin);
    }

    @NativeMethods
    interface Natives {
        long createBridgeForProfile(OfflinePageEvaluationBridge caller, Profile profile,
                boolean useEvaluationScheduler);
        void destroy(long nativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge caller);
        void getAllPages(long nativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge caller,
                List<OfflinePageItem> offlinePages, final Callback<List<OfflinePageItem>> callback);
        void savePageLater(long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller, String url, String clientNamespace,
                String clientId, boolean userRequested);
        boolean pushRequestProcessing(long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller, Callback<Boolean> callback);
        void getRequestsInQueue(long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller, final Callback<SavePageRequest[]> callback);
        void removeRequestsFromQueue(long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller, long[] requestIds,
                final Callback<Integer> callback);
    }
}
