package android.content;

import android.accounts.Account;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.database.SQLException;
import android.net.TrafficStats;
import android.os.Bundle;
import android.os.Debug;
import android.os.Parcelable;
import android.os.Process;
import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Config;
import android.util.EventLog;
import android.util.Log;
import android.util.TimingLogger;

import java.io.IOException;

/**
 * @hide
 */
public abstract class TempProviderSyncAdapter extends SyncAdapter {
    private static final String TAG = "Sync";

    private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20;
    private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10;
    private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5;
    private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20;

    private volatile SyncableContentProvider mProvider;
    private volatile SyncThread mSyncThread = null;
    private volatile boolean mProviderSyncStarted;
    private volatile boolean mAdapterSyncStarted;
    
    public TempProviderSyncAdapter(SyncableContentProvider provider) {
        super();
        mProvider = provider;
    }

    /**
     * Used by getServerDiffs() to track the sync progress for a given
     * sync adapter. Implementations of SyncAdapter generally specialize
     * this class in order to track specific data about that SyncAdapter's
     * sync. If an implementation of SyncAdapter doesn't need to store
     * any data for a sync it may use TrivialSyncData.
     */
    public static abstract class SyncData implements Parcelable {

    }

    public final void setContext(Context context) {
        mContext = context;
    }

    /**
     * Retrieve the Context this adapter is running in.  Only available
     * once onSyncStarting() is called (not available from constructor).
     */
    final public Context getContext() {
        return mContext;
    }

    /**
     * Called right before a sync is started.
     *
     * @param context allows you to publish status and interact with the
     * @param account the account to sync
     * @param manualSync true if this sync was requested manually by the user
     * @param result information to track what happened during this sync attempt
     */
    public abstract void onSyncStarting(SyncContext context, Account account, boolean manualSync,
            SyncResult result);

    /**
     * Called right after a sync is completed
     *
     * @param context allows you to publish status and interact with the
     *                user during interactive syncs.
     * @param success true if the sync suceeded, false if an error occured
     */
    public abstract void onSyncEnding(SyncContext context, boolean success);

    /**
     * Implement this to return true if the data in your content provider
     * is read only.
     */
    public abstract boolean isReadOnly();

    public abstract boolean getIsSyncable(Account account)
            throws IOException, AuthenticatorException, OperationCanceledException;

    /**
     * Get diffs from the server since the last completed sync and put them
     * into a temporary provider.
     *
     * @param context allows you to publish status and interact with the
     *                user during interactive syncs.
     * @param syncData used to track the progress this client has made in syncing data
     *   from the server
     * @param tempProvider this is where the diffs should be stored
     * @param extras any extra data describing the sync that is desired
     * @param syncInfo sync adapter-specific data that is used during a single sync operation
     * @param syncResult information to track what happened during this sync attempt
     */
    public abstract void getServerDiffs(SyncContext context,
            SyncData syncData, SyncableContentProvider tempProvider,
            Bundle extras, Object syncInfo, SyncResult syncResult);

    /**
     * Send client diffs to the server, optionally receiving more diffs from the server
     *
     * @param context allows you to publish status and interact with the
     *                user during interactive syncs.
     * @param clientDiffs the diffs from the client
     * @param serverDiffs the SyncableContentProvider that should be populated with
*   the entries that were returned in response to an insert/update/delete request
*   to the server
     * @param syncResult information to track what happened during this sync attempt
     * @param dontActuallySendDeletes
     */
    public abstract void sendClientDiffs(SyncContext context,
            SyncableContentProvider clientDiffs,
            SyncableContentProvider serverDiffs, SyncResult syncResult,
            boolean dontActuallySendDeletes);

    /**
     * Reads the sync data from the ContentProvider
     * @param contentProvider the ContentProvider to read from
     * @return the SyncData for the provider. This may be null.
     */
    public SyncData readSyncData(SyncableContentProvider contentProvider) {
        return null;
    }

    /**
     * Create and return a new, empty SyncData object
     */
    public SyncData newSyncData() {
        return null;
    }

    /**
     * Stores the sync data in the Sync Stats database, keying it by
     * the account that was set in the last call to onSyncStarting()
     */
    public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {}

    /**
     * Indicate to the SyncAdapter that the last sync that was started has
     * been cancelled.
     */
    public abstract void onSyncCanceled();

    /**
     * Initializes the temporary content providers used during
     * {@link TempProviderSyncAdapter#sendClientDiffs}.
     * May copy relevant data from the underlying db into this provider so
     * joins, etc., can work.
     *
     * @param cp The ContentProvider to initialize.
     */
    protected void initTempProvider(SyncableContentProvider cp) {}

    protected Object createSyncInfo() {
        return null;
    }

    /**
     * Called when the accounts list possibly changed, to give the
     * SyncAdapter a chance to do any necessary bookkeeping, e.g.
     * to make sure that any required SubscribedFeeds subscriptions
     * exist.
     * @param accounts the list of accounts
     */
    public abstract void onAccountsChanged(Account[] accounts);

    private Context mContext;

    private class SyncThread extends Thread {
        private final Account mAccount;
        private final String mAuthority;
        private final Bundle mExtras;
        private final SyncContext mSyncContext;
        private volatile boolean mIsCanceled = false;
        private long mInitialTxBytes;
        private long mInitialRxBytes;
        private final SyncResult mResult;

        SyncThread(SyncContext syncContext, Account account, String authority, Bundle extras) {
            super("SyncThread");
            mAccount = account;
            mAuthority = authority;
            mExtras = extras;
            mSyncContext = syncContext;
            mResult = new SyncResult();
        }

        void cancelSync() {
            mIsCanceled = true;
            if (mAdapterSyncStarted) onSyncCanceled();
            if (mProviderSyncStarted) mProvider.onSyncCanceled();
            // We may lose the last few sync events when canceling.  Oh well.
            int uid = Process.myUid();
            logSyncDetails(TrafficStats.getUidTxBytes(uid) - mInitialTxBytes,
                    TrafficStats.getUidRxBytes(uid) - mInitialRxBytes, mResult);
        }
        
        @Override
        public void run() {
            Process.setThreadPriority(Process.myTid(),
                    Process.THREAD_PRIORITY_BACKGROUND);
            int uid = Process.myUid();
            mInitialTxBytes = TrafficStats.getUidTxBytes(uid);
            mInitialRxBytes = TrafficStats.getUidRxBytes(uid);
            try {
                sync(mSyncContext, mAccount, mAuthority, mExtras);
            } catch (SQLException e) {
                Log.e(TAG, "Sync failed", e);
                mResult.databaseError = true;
            } finally {
                mSyncThread = null;
                if (!mIsCanceled) {
                    logSyncDetails(TrafficStats.getUidTxBytes(uid) - mInitialTxBytes,
                    TrafficStats.getUidRxBytes(uid) - mInitialRxBytes, mResult);
                    mSyncContext.onFinished(mResult);
                }
            }
        }

        private void sync(SyncContext syncContext, Account account, String authority,
                Bundle extras) {
            mIsCanceled = false;

            mProviderSyncStarted = false;
            mAdapterSyncStarted = false;
            String message = null;

            // always attempt to initialize if the isSyncable state isn't set yet
            int isSyncable = ContentResolver.getIsSyncable(account, authority);
            if (isSyncable < 0) {
                try {
                    isSyncable = (getIsSyncable(account)) ? 1 : 0;
                    ContentResolver.setIsSyncable(account, authority, isSyncable);
                } catch (IOException e) {
                    ++mResult.stats.numIoExceptions;
                } catch (AuthenticatorException e) {
                    ++mResult.stats.numParseExceptions;
                } catch (OperationCanceledException e) {
                    // do nothing
                }
            }

            // if this is an initialization request then our work is done here
            if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
                return;
            }

            // if we aren't syncable then get out
            if (isSyncable <= 0) {
                return;
            }

            boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);

            try {
                mProvider.onSyncStart(syncContext, account);
                mProviderSyncStarted = true;
                onSyncStarting(syncContext, account, manualSync, mResult);
                if (mResult.hasError()) {
                    message = "SyncAdapter failed while trying to start sync";
                    return;
                }
                mAdapterSyncStarted = true;
                if (mIsCanceled) {
                    return;
                }
                final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing");
                final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue);
                try {
                    if (syncTracingEnabled) {
                        System.gc();
                        System.gc();
                        Debug.startMethodTracing("synctrace." + System.currentTimeMillis());
                    }
                    runSyncLoop(syncContext, account, extras);
                } finally {
                    if (syncTracingEnabled) Debug.stopMethodTracing();
                }
                onSyncEnding(syncContext, !mResult.hasError());
                mAdapterSyncStarted = false;
                mProvider.onSyncStop(syncContext, true);
                mProviderSyncStarted = false;
            } finally {
                if (mAdapterSyncStarted) {
                    mAdapterSyncStarted = false;
                    onSyncEnding(syncContext, false);
                }
                if (mProviderSyncStarted) {
                    mProviderSyncStarted = false;
                    mProvider.onSyncStop(syncContext, false);
                }
                if (!mIsCanceled) {
                    if (message != null) syncContext.setStatusText(message);
                }
            }
        }

        private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) {
            TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync");
            syncTimer.addSplit("start");
            int loopCount = 0;
            boolean tooManyGetServerDiffsAttempts = false;

            final boolean overrideTooManyDeletions =
                    extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS,
                            false);
            final boolean discardLocalDeletions =
                    extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false);
            boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD,
                    false /* default this flag to false */);
            SyncableContentProvider serverDiffs = null;
            TempProviderSyncResult result = new TempProviderSyncResult();
            try {
                if (!uploadOnly) {
                    /**
                     * This loop repeatedly calls SyncAdapter.getServerDiffs()
                     * (to get changes from the feed) followed by
                     * ContentProvider.merge() (to incorporate these changes
                     * into the provider), stopping when the SyncData returned
                     * from getServerDiffs() indicates that all the data was
                     * fetched.
                     */
                    while (!mIsCanceled) {
                        // Don't let a bad sync go forever
                        if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) {
                            Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs "
                                    + getClass().getName());
                            // TODO: change the structure here to schedule a new sync
                            // with a backoff time, keeping track to be sure
                            // we don't keep doing this forever (due to some bug or
                            // mismatch between the client and the server)
                            tooManyGetServerDiffsAttempts = true;
                            break;
                        }

                        // Get an empty content provider to put the diffs into
                        if (serverDiffs != null) serverDiffs.close();
                        serverDiffs = mProvider.getTemporaryInstance();

                        // Get records from the server which will be put into the serverDiffs
                        initTempProvider(serverDiffs);
                        Object syncInfo = createSyncInfo();
                        SyncData syncData = readSyncData(serverDiffs);
                        // syncData will only be null if there was a demarshalling error
                        // while reading the sync data.
                        if (syncData == null) {
                            mProvider.wipeAccount(account);
                            syncData = newSyncData();
                        }
                        mResult.clear();
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData "
                                    + syncData.toString());
                        }
                        getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo,
                                mResult);

                        if (mIsCanceled) return;
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: result: " + mResult);
                        }
                        if (mResult.hasError()) return;
                        if (mResult.partialSyncUnavailable) {
                            if (Config.LOGD) {
                                Log.d(TAG, "partialSyncUnavailable is set, setting "
                                        + "ignoreSyncData and retrying");
                            }
                            mProvider.wipeAccount(account);
                            continue;
                        }

                        // write the updated syncData back into the temp provider
                        writeSyncData(syncData, serverDiffs);

                        // apply the downloaded changes to the provider
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: running merge");
                        }
                        mProvider.merge(syncContext, serverDiffs,
                                null /* don't return client diffs */, mResult);
                        if (mIsCanceled) return;
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: result: " + mResult);
                        }

                        // if the server has no more changes then break out of the loop
                        if (!mResult.moreRecordsToGet) {
                            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                                Log.v(TAG, "runSyncLoop: fetched all data, moving on");
                            }
                            break;
                        }
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: more data to fetch, looping");
                        }
                    }
                }

                /**
                 * This loop repeatedly calls ContentProvider.merge() followed
                 * by SyncAdapter.merge() until either indicate that there is
                 * no more work to do by returning null.
                 * <p>
                 * The initial ContentProvider.merge() returns a temporary
                 * ContentProvider that contains any local changes that need
                 * to be committed to the server.
                 * <p>
                 * The SyncAdapter.merge() calls upload the changes to the server
                 * and populates temporary provider (the serverDiffs) with the
                 * result.
                 * <p>
                 * Subsequent calls to ContentProvider.merge() incoporate the
                 * result of previous SyncAdapter.merge() calls into the
                 * real ContentProvider and again return a temporary
                 * ContentProvider that contains any local changes that need
                 * to be committed to the server.
                 */
                loopCount = 0;
                boolean readOnly = isReadOnly();
                long previousNumModifications = 0;
                if (serverDiffs != null) {
                    serverDiffs.close();
                    serverDiffs = null;
                }

                // If we are discarding local deletions then we need to redownload all the items
                // again (since some of them might have been deleted). We do this by deleting the
                // sync data for the current account by writing in a null one.
                if (discardLocalDeletions) {
                    serverDiffs = mProvider.getTemporaryInstance();
                    initTempProvider(serverDiffs);
                    writeSyncData(null, serverDiffs);
                }

                while (!mIsCanceled) {
                    if (Config.LOGV) {
                        Log.v(TAG, "runSyncLoop: Merging diffs from server to client");
                    }
                    if (result.tempContentProvider != null) {
                        result.tempContentProvider.close();
                        result.tempContentProvider = null;
                    }
                    mResult.clear();
                    mProvider.merge(syncContext, serverDiffs, readOnly ? null : result,
                            mResult);
                    if (mIsCanceled) return;
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "runSyncLoop: result: " + mResult);
                    }

                    SyncableContentProvider clientDiffs =
                            readOnly ? null : result.tempContentProvider;
                    if (clientDiffs == null) {
                        // Nothing to commit back to the server
                        if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs");
                        break;
                    }

                    long numModifications = mResult.stats.numUpdates
                            + mResult.stats.numDeletes
                            + mResult.stats.numInserts;

                    // as long as we are making progress keep resetting the loop count
                    if (numModifications < previousNumModifications) {
                        loopCount = 0;
                    }
                    previousNumModifications = numModifications;

                    // Don't let a bad sync go forever
                    if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) {
                        Log.e(TAG, "runSyncLoop: Hit max loop count while syncing "
                                + getClass().getName());
                        mResult.tooManyRetries = true;
                        break;
                    }

                    if (!overrideTooManyDeletions && !discardLocalDeletions
                            && hasTooManyDeletions(mResult.stats)) {
                        if (Config.LOGD) {
                            Log.d(TAG, "runSyncLoop: Too many deletions were found in provider "
                                    + getClass().getName() + ", not doing any more updates");
                        }
                        long numDeletes = mResult.stats.numDeletes;
                        mResult.stats.clear();
                        mResult.tooManyDeletions = true;
                        mResult.stats.numDeletes = numDeletes;
                        break;
                    }

                    if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server");
                    if (serverDiffs != null) serverDiffs.close();
                    serverDiffs = clientDiffs.getTemporaryInstance();
                    initTempProvider(serverDiffs);
                    mResult.clear();
                    sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult,
                            discardLocalDeletions);
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "runSyncLoop: result: " + mResult);
                    }

                    if (!mResult.madeSomeProgress()) {
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
                            Log.v(TAG, "runSyncLoop: No data from client diffs merge");
                        }
                        break;
                    }
                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
                        Log.v(TAG, "runSyncLoop: made some progress, looping");
                    }
                }

                // add in any status codes that we saved from earlier
                mResult.tooManyRetries |= tooManyGetServerDiffsAttempts;
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "runSyncLoop: final result: " + mResult);
                }
            } finally {
                // do this in the finally block to guarantee that is is set and not overwritten
                if (discardLocalDeletions) {
                    mResult.fullSyncRequested = true;
                }
                if (serverDiffs != null) serverDiffs.close();
                if (result.tempContentProvider != null) result.tempContentProvider.close();
                syncTimer.addSplit("stop");
                syncTimer.dumpToLog();
            }
        }
    }

    /**
     * Logs details on the sync.
     * Normally this will be overridden by a subclass that will provide
     * provider-specific details.
     * 
     * @param bytesSent number of bytes the sync sent over the network
     * @param bytesReceived number of bytes the sync received over the network
     * @param result The SyncResult object holding info on the sync
     */
    protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) {
        EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, "");
    }

    public void startSync(SyncContext syncContext, Account account, String authority,
            Bundle extras) {
        if (mSyncThread != null) {
            syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS);
            return;
        }

        mSyncThread = new SyncThread(syncContext, account, authority, extras);
        mSyncThread.start();
    }

    public void cancelSync() {
        if (mSyncThread != null) {
            mSyncThread.cancelSync();
        }
    }

    protected boolean hasTooManyDeletions(SyncStats stats) {
        long numEntries = stats.numEntries;
        long numDeletedEntries = stats.numDeletes;

        long percentDeleted = (numDeletedEntries == 0)
                ? 0
                : (100 * numDeletedEntries /
                        (numEntries + numDeletedEntries));
        boolean tooManyDeletions =
                (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS)
                && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS);
        return tooManyDeletions;
    }
}
