/**
 * 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;

import android.os.Bundle;
import com.facebook.internal.Validate;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * A base class for implementations of a {@link Session Session} token cache.
 * </p>
 * <p>
 * The Session constructor optionally takes a TokenCachingStrategy, from which it will
 * attempt to load a cached token during construction. Also, whenever the
 * Session updates its token, it will also save the token and associated state
 * to the TokenCachingStrategy.
 * </p>
 * <p>
 * This is the only mechanism supported for an Android service to use Session.
 * The service can create a custom TokenCachingStrategy that returns the Session provided
 * by an Activity through which the user logged in to Facebook.
 * </p>
 */
public abstract class TokenCachingStrategy {
    /**
     * The key used by Session to store the token value in the Bundle during
     * load and save.
     */
    public static final String TOKEN_KEY = "com.facebook.TokenCachingStrategy.Token";

    /**
     * The key used by Session to store the expiration date value in the Bundle
     * during load and save.
     */
    public static final String EXPIRATION_DATE_KEY = "com.facebook.TokenCachingStrategy.ExpirationDate";

    /**
     * The key used by Session to store the last refresh date value in the
     * Bundle during load and save.
     */
    public static final String LAST_REFRESH_DATE_KEY = "com.facebook.TokenCachingStrategy.LastRefreshDate";

    /**
     * The key used by Session to store the user's id value in the Bundle during
     * load and save.
     */
    public static final String USER_FBID_KEY = "com.facebook.TokenCachingStrategy.UserFBID";

    /**
     * The key used by Session to store an enum indicating the source of the token
     * in the Bundle during load and save.
     */
    public static final String TOKEN_SOURCE_KEY = "com.facebook.TokenCachingStrategy.AccessTokenSource";

    /**
     * The key used by Session to store the list of permissions granted by the
     * token in the Bundle during load and save.
     */
    public static final String PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.Permissions";

    /**
     * The key used by Session to store the list of permissions declined by the user in the token in the Bundle
     * during load and save.
     */
    public static final String DECLINED_PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.DeclinedPermissions";

    private static final long INVALID_BUNDLE_MILLISECONDS = Long.MIN_VALUE;
    private static final String IS_SSO_KEY = "com.facebook.TokenCachingStrategy.IsSSO";

    /**
     * Called during Session construction to get the token state. Typically this
     * is loaded from a persistent store that was previously initialized via
     * save.  The caller may choose to keep a reference to the returned Bundle
     * indefinitely.  Therefore the TokenCachingStrategy should not store the returned Bundle
     * and should return a new Bundle on every call to this method.
     *
     * @return A Bundle that represents the token state that was loaded.
     */
    public abstract Bundle load();

    /**
     * Called when a Session updates its token. This is passed a Bundle of
     * values that should be stored durably for the purpose of being returned
     * from a later call to load.  Some implementations may choose to store
     * bundle beyond the scope of this call, so the caller should keep no
     * references to the bundle to ensure that it is not modified later.
     * 
     * @param bundle
     *            A Bundle that represents the token state to be saved.
     */
    public abstract void save(Bundle bundle);

    /**
     * Called when a Session learns its token is no longer valid or during a
     * call to {@link Session#closeAndClearTokenInformation
     * closeAndClearTokenInformation} to clear the durable state associated with
     * the token.
     */
    public abstract void clear();

    /**
     * Returns a boolean indicating whether a Bundle contains properties that
     * could be a valid saved token.
     * 
     * @param bundle
     *            A Bundle to check for token information.
     * @return a boolean indicating whether a Bundle contains properties that
     *         could be a valid saved token.
     */
    public static boolean hasTokenInformation(Bundle bundle) {
        if (bundle == null) {
            return false;
        }

        String token = bundle.getString(TOKEN_KEY);
        if ((token == null) || (token.length() == 0)) {
            return false;
        }

        long expiresMilliseconds = bundle.getLong(EXPIRATION_DATE_KEY, 0L);
        if (expiresMilliseconds == 0L) {
            return false;
        }

        return true;
    }

    /**
     * Gets the cached token value from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the token value was stored.
     * @return the cached token value, or null.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static String getToken(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return bundle.getString(TOKEN_KEY);
    }

    /**
     * Puts the token value into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the token value should be stored.
     * @param value
     *            The String representing the token value, or null.
     *
     * @throws NullPointerException if the passed in Bundle or token value are null
     */
    public static void putToken(Bundle bundle, String value) {
        Validate.notNull(bundle, "bundle");
        Validate.notNull(value, "value");
        bundle.putString(TOKEN_KEY, value);
    }

    /**
     * Gets the cached expiration date from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the expiration date was stored.
     * @return the cached expiration date, or null.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static Date getExpirationDate(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return getDate(bundle, EXPIRATION_DATE_KEY);
    }

    /**
     * Puts the expiration date into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the expiration date should be stored.
     * @param value
     *            The Date representing the expiration date.
     *
     * @throws NullPointerException if the passed in Bundle or date value are null
     */
    public static void putExpirationDate(Bundle bundle, Date value) {
        Validate.notNull(bundle, "bundle");
        Validate.notNull(value, "value");
        putDate(bundle, EXPIRATION_DATE_KEY, value);
    }

    /**
     * Gets the cached expiration date from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the expiration date was stored.
     * @return the long representing the cached expiration date in milliseconds
     *         since the epoch, or 0.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static long getExpirationMilliseconds(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return bundle.getLong(EXPIRATION_DATE_KEY);
    }

    /**
     * Puts the expiration date into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the expiration date should be stored.
     * @param value
     *            The long representing the expiration date in milliseconds
     *            since the epoch.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static void putExpirationMilliseconds(Bundle bundle, long value) {
        Validate.notNull(bundle, "bundle");
        bundle.putLong(EXPIRATION_DATE_KEY, value);
    }

    /**
     * Gets the cached list of permissions from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the list of permissions was stored.
     * @return the cached list of permissions.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static List<String> getPermissions(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return bundle.getStringArrayList(PERMISSIONS_KEY);
    }

    /**
     * Puts the list of permissions into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the list of permissions should be stored.
     * @param value
     *            The List&lt;String&gt; representing the list of permissions,
     *            or null.
     *
     * @throws NullPointerException if the passed in Bundle or permissions list are null
     */
    public static void putPermissions(Bundle bundle, List<String> value) {
        Validate.notNull(bundle, "bundle");
        Validate.notNull(value, "value");

        ArrayList<String> arrayList;
        if (value instanceof ArrayList<?>) {
            arrayList = (ArrayList<String>) value;
        } else {
            arrayList = new ArrayList<String>(value);
        }
        bundle.putStringArrayList(PERMISSIONS_KEY, arrayList);
    }

    /**
     * Puts the list of declined permissions into a Bundle.
     *
     * @param bundle
     *            A Bundle in which the list of permissions should be stored.
     * @param value
     *            The List&lt;String&gt; representing the list of permissions,
     *            or null.
     *
     * @throws NullPointerException if the passed in Bundle or permissions list are null
     */
    public static void putDeclinedPermissions(Bundle bundle, List<String> value) {
        Validate.notNull(bundle, "bundle");
        Validate.notNull(value, "value");

        ArrayList<String> arrayList;
        if (value instanceof ArrayList<?>) {
            arrayList = (ArrayList<String>) value;
        } else {
            arrayList = new ArrayList<String>(value);
        }
        bundle.putStringArrayList(DECLINED_PERMISSIONS_KEY, arrayList);
    }


    /**
     * Gets the cached enum indicating the source of the token from the Bundle.
     *
     * @param bundle
     *            A Bundle in which the enum was stored.
     * @return enum indicating the source of the token
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static AccessTokenSource getSource(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        if (bundle.containsKey(TokenCachingStrategy.TOKEN_SOURCE_KEY)) {
            return (AccessTokenSource) bundle.getSerializable(TokenCachingStrategy.TOKEN_SOURCE_KEY);
        } else {
            boolean isSSO = bundle.getBoolean(TokenCachingStrategy.IS_SSO_KEY);
            return isSSO ? AccessTokenSource.FACEBOOK_APPLICATION_WEB : AccessTokenSource.WEB_VIEW;
        }
    }
    /**
     * Puts the enum indicating the source of the token into a Bundle.
     *
     * @param bundle
     *            A Bundle in which the enum should be stored.
     * @param value
     *            enum indicating the source of the token
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static void putSource(Bundle bundle, AccessTokenSource value) {
        Validate.notNull(bundle, "bundle");
        bundle.putSerializable(TOKEN_SOURCE_KEY, value);
    }

    /**
     * Gets the cached last refresh date from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the last refresh date was stored.
     * @return the cached last refresh Date, or null.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static Date getLastRefreshDate(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return getDate(bundle, LAST_REFRESH_DATE_KEY);
    }

    /**
     * Puts the last refresh date into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the last refresh date should be stored.
     * @param value
     *            The Date representing the last refresh date, or null.
     *
     * @throws NullPointerException if the passed in Bundle or date value are null
     */
    public static void putLastRefreshDate(Bundle bundle, Date value) {
        Validate.notNull(bundle, "bundle");
        Validate.notNull(value, "value");
        putDate(bundle, LAST_REFRESH_DATE_KEY, value);
    }

    /**
     * Gets the cached last refresh date from a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the last refresh date was stored.
     * @return the cached last refresh date in milliseconds since the epoch.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static long getLastRefreshMilliseconds(Bundle bundle) {
        Validate.notNull(bundle, "bundle");
        return bundle.getLong(LAST_REFRESH_DATE_KEY);
    }

    /**
     * Puts the last refresh date into a Bundle.
     * 
     * @param bundle
     *            A Bundle in which the last refresh date should be stored.
     * @param value
     *            The long representing the last refresh date in milliseconds
     *            since the epoch.
     *
     * @throws NullPointerException if the passed in Bundle is null
     */
    public static void putLastRefreshMilliseconds(Bundle bundle, long value) {
        Validate.notNull(bundle, "bundle");
        bundle.putLong(LAST_REFRESH_DATE_KEY, value);
    }

    static Date getDate(Bundle bundle, String key) {
        if (bundle == null) {
            return null;
        }

        long n = bundle.getLong(key, INVALID_BUNDLE_MILLISECONDS);
        if (n == INVALID_BUNDLE_MILLISECONDS) {
            return null;
        }

        return new Date(n);
    }

    static void putDate(Bundle bundle, String key, Date date) {
        bundle.putLong(key, date.getTime());
    }
}