/*
 * Copyright (C) 2010 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 com.android.server.content;

import android.accounts.Account;
import android.content.pm.PackageManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;

/**
 * Value type that represents a sync operation.
 * TODO: This is the class to flesh out with all the scheduling data - metered/unmetered,
 * transfer-size, etc.
 * {@hide}
 */
public class SyncOperation implements Comparable {
    public static final String TAG = "SyncManager";

    public static final int REASON_BACKGROUND_DATA_SETTINGS_CHANGED = -1;
    public static final int REASON_ACCOUNTS_UPDATED = -2;
    public static final int REASON_SERVICE_CHANGED = -3;
    public static final int REASON_PERIODIC = -4;
    /** Sync started because it has just been set to isSyncable. */
    public static final int REASON_IS_SYNCABLE = -5;
    /** Sync started because it has just been set to sync automatically. */
    public static final int REASON_SYNC_AUTO = -6;
    /** Sync started because master sync automatically has been set to true. */
    public static final int REASON_MASTER_SYNC_AUTO = -7;
    public static final int REASON_USER_START = -8;

    private static String[] REASON_NAMES = new String[] {
            "DataSettingsChanged",
            "AccountsUpdated",
            "ServiceChanged",
            "Periodic",
            "IsSyncable",
            "AutoSync",
            "MasterSyncAuto",
            "UserStart",
    };

    public static final int SYNC_TARGET_UNKNOWN = 0;
    public static final int SYNC_TARGET_ADAPTER = 1;
    public static final int SYNC_TARGET_SERVICE = 2;

    /** Identifying info for the target for this operation. */
    public final SyncStorageEngine.EndPoint target;
    /** Why this sync was kicked off. {@link #REASON_NAMES} */
    public final int reason;
    /** Where this sync was initiated. */
    public final int syncSource;
    public final boolean allowParallelSyncs;
    public final String key;
    /** Internal boolean to avoid reading a bundle everytime we want to compare operations. */
    private final boolean expedited;
    public Bundle extras;
    /** Bare-bones version of this operation that is persisted across reboots. */
    public SyncStorageEngine.PendingOperation pendingOperation;
    /** Elapsed real time in millis at which to run this sync. */
    public long latestRunTime;
    /** Set by the SyncManager in order to delay retries. */
    public long backoff;
    /** Specified by the adapter to delay subsequent sync operations. */
    public long delayUntil;
    /**
     * Elapsed real time in millis when this sync will be run.
     * Depends on max(backoff, latestRunTime, and delayUntil).
     */
    public long effectiveRunTime;
    /** Amount of time before {@link #effectiveRunTime} from which this sync can run. */
    public long flexTime;

    /** Descriptive string key for this operation */
    public String wakeLockName;

    /** Whether this sync op was recently skipped due to the app being idle */
    public boolean appIdle;

    public SyncOperation(Account account, int userId, int reason, int source, String provider,
            Bundle extras, long runTimeFromNow, long flexTime, long backoff,
            long delayUntil, boolean allowParallelSyncs) {
        this(new SyncStorageEngine.EndPoint(account, provider, userId),
                reason, source, extras, runTimeFromNow, flexTime, backoff, delayUntil,
                allowParallelSyncs);
    }

    public SyncOperation(ComponentName service, int userId, int reason, int source,
            Bundle extras, long runTimeFromNow, long flexTime, long backoff,
            long delayUntil) {
        this(new SyncStorageEngine.EndPoint(service, userId), reason, source, extras,
                runTimeFromNow, flexTime, backoff, delayUntil, true /* allowParallelSyncs */);
    }

    private SyncOperation(SyncStorageEngine.EndPoint info, int reason, int source, Bundle extras,
            long runTimeFromNow, long flexTime, long backoff, long delayUntil,
            boolean allowParallelSyncs) {
        this.target = info;
        this.reason = reason;
        this.syncSource = source;
        this.extras = new Bundle(extras);
        cleanBundle(this.extras);
        this.delayUntil = delayUntil;
        this.backoff = backoff;
        this.allowParallelSyncs = allowParallelSyncs;
        final long now = SystemClock.elapsedRealtime();
        // Set expedited based on runTimeFromNow. The SyncManager specifies whether the op is
        // expedited (Not done solely based on bundle).
        if (runTimeFromNow < 0) {
            this.expedited = true;
            // Sanity check: Will always be true.
            if (!this.extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) {
                this.extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
            }
            this.latestRunTime = now;
            this.flexTime = 0;
        } else {
            this.expedited = false;
            this.extras.remove(ContentResolver.SYNC_EXTRAS_EXPEDITED);
            this.latestRunTime = now + runTimeFromNow;
            this.flexTime = flexTime;
        }
        updateEffectiveRunTime();
        this.key = toKey(info, this.extras);
    }

    /** Used to reschedule a sync at a new point in time. */
    public SyncOperation(SyncOperation other, long newRunTimeFromNow) {
        this(other.target, other.reason, other.syncSource, new Bundle(other.extras),
                newRunTimeFromNow,
                0L /* In back-off so no flex */,
                other.backoff,
                other.delayUntil,
                other.allowParallelSyncs);
    }

    public boolean matchesAuthority(SyncOperation other) {
        return this.target.matchesSpec(other.target);
    }

    /**
     * Make sure the bundle attached to this SyncOperation doesn't have unnecessary
     * flags set.
     * @param bundle to clean.
     */
    private void cleanBundle(Bundle bundle) {
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_UPLOAD);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_MANUAL);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_EXPEDITED);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS);
        removeFalseExtra(bundle, ContentResolver.SYNC_EXTRAS_DISALLOW_METERED);
    }

    private void removeFalseExtra(Bundle bundle, String extraName) {
        if (!bundle.getBoolean(extraName, false)) {
            bundle.remove(extraName);
        }
    }

    /**
     * Determine whether if this sync operation is running, the provided operation would conflict
     * with it.
     * Parallel syncs allow multiple accounts to be synced at the same time. 
     */
    public boolean isConflict(SyncOperation toRun) {
        final SyncStorageEngine.EndPoint other = toRun.target;
        if (target.target_provider) {
            return target.account.type.equals(other.account.type)
                    && target.provider.equals(other.provider)
                    && target.userId == other.userId
                    && (!allowParallelSyncs
                            || target.account.name.equals(other.account.name));
        } else {
            // Ops that target a service default to allow parallel syncs, which is handled by the
            // service returning SYNC_IN_PROGRESS if they don't.
            return target.service.equals(other.service) && !allowParallelSyncs;
        }
    }

    @Override
    public String toString() {
        return dump(null, true);
    }

    public String dump(PackageManager pm, boolean useOneLine) {
        StringBuilder sb = new StringBuilder();
        if (target.target_provider) {
            sb.append(target.account.name)
                .append(" u")
                .append(target.userId).append(" (")
                .append(target.account.type)
                .append(")")
                .append(", ")
                .append(target.provider)
                .append(", ");
        } else if (target.target_service) {
            sb.append(target.service.getPackageName())
                .append(" u")
                .append(target.userId).append(" (")
                .append(target.service.getClassName()).append(")")
                .append(", ");
        }
        sb.append(SyncStorageEngine.SOURCES[syncSource])
            .append(", currentRunTime ")
            .append(effectiveRunTime);
        if (expedited) {
            sb.append(", EXPEDITED");
        }
        sb.append(", reason: ");
        sb.append(reasonToString(pm, reason));
        if (!useOneLine && !extras.keySet().isEmpty()) {
            sb.append("\n    ");
            extrasToStringBuilder(extras, sb);
        }
        return sb.toString();
    }

    public static String reasonToString(PackageManager pm, int reason) {
        if (reason >= 0) {
            if (pm != null) {
                final String[] packages = pm.getPackagesForUid(reason);
                if (packages != null && packages.length == 1) {
                    return packages[0];
                }
                final String name = pm.getNameForUid(reason);
                if (name != null) {
                    return name;
                }
                return String.valueOf(reason);
            } else {
                return String.valueOf(reason);
            }
        } else {
            final int index = -reason - 1;
            if (index >= REASON_NAMES.length) {
                return String.valueOf(reason);
            } else {
                return REASON_NAMES[index];
            }
        }
    }

    public boolean isInitialization() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
    }

    public boolean isExpedited() {
        return expedited;
    }

    public boolean ignoreBackoff() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
    }

    public boolean isNotAllowedOnMetered() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISALLOW_METERED, false);
    }

    public boolean isManual() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
    }

    public boolean isIgnoreSettings() {
        return extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false);
    }

    /** Changed in V3. */
    public static String toKey(SyncStorageEngine.EndPoint info, Bundle extras) {
        StringBuilder sb = new StringBuilder();
        if (info.target_provider) {
            sb.append("provider: ").append(info.provider);
            sb.append(" account {name=" + info.account.name
                    + ", user="
                    + info.userId
                    + ", type="
                    + info.account.type
                    + "}");
        } else if (info.target_service) {
            sb.append("service {package=" )
                .append(info.service.getPackageName())
                .append(" user=")
                .append(info.userId)
                .append(", class=")
                .append(info.service.getClassName())
                .append("}");
        } else {
            Log.v(TAG, "Converting SyncOperaton to key, invalid target: " + info.toString());
            return "";
        }
        sb.append(" extras: ");
        extrasToStringBuilder(extras, sb);
        return sb.toString();
    }

    private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
        sb.append("[");
        for (String key : bundle.keySet()) {
            sb.append(key).append("=").append(bundle.get(key)).append(" ");
        }
        sb.append("]");
    }

    public String wakeLockName() {
        if (wakeLockName != null) {
            return wakeLockName;
        }
        if (target.target_provider) {
            return (wakeLockName = target.provider
                    + "/" + target.account.type
                    + "/" + target.account.name);
        } else if (target.target_service) {
            return (wakeLockName = target.service.getPackageName()
                    + "/" + target.service.getClassName());
        } else {
            Log.wtf(TAG, "Invalid target getting wakelock name for operation - " + key);
            return null;
        }
    }

    /**
     * Update the effective run time of this Operation based on latestRunTime (specified at
     * creation time of sync), delayUntil (specified by SyncAdapter), or backoff (specified by
     * SyncManager on soft failures).
     */
    public void updateEffectiveRunTime() {
        // Regardless of whether we're in backoff or honouring a delayUntil, we still incorporate
        // the flex time provided by the developer.
        effectiveRunTime = ignoreBackoff() ?
                latestRunTime :
                    Math.max(Math.max(latestRunTime, delayUntil), backoff);
    }

    /**
     * SyncOperations are sorted based on their earliest effective run time.
     * This comparator is used to sort the SyncOps at a given time when
     * deciding which to run, so earliest run time is the best criteria.
     */
    @Override
    public int compareTo(Object o) {
        SyncOperation other = (SyncOperation) o;
        if (expedited != other.expedited) {
            return expedited ? -1 : 1;
        }
        long thisIntervalStart = Math.max(effectiveRunTime - flexTime, 0);
        long otherIntervalStart = Math.max(
            other.effectiveRunTime - other.flexTime, 0);
        if (thisIntervalStart < otherIntervalStart) {
            return -1;
        } else if (otherIntervalStart < thisIntervalStart) {
            return 1;
        } else {
            return 0;
        }
    }

    // TODO: Test this to make sure that casting to object doesn't lose the type info for EventLog.
    public Object[] toEventLog(int event) {
        Object[] logArray = new Object[4];
        logArray[1] = event;
        logArray[2] = syncSource;
        if (target.target_provider) {
            logArray[0] = target.provider;
            logArray[3] = target.account.name.hashCode();
        } else if (target.target_service) {
            logArray[0] = target.service.getPackageName();
            logArray[3] = target.service.hashCode();
        } else {
            Log.wtf(TAG, "sync op with invalid target: " + key);
        }
        return logArray;
    }
}
