/*
 * Copyright (c) 2014-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.stetho.inspector.domstorage;

import android.content.Context;
import android.content.SharedPreferences;
import com.facebook.stetho.inspector.console.CLog;
import com.facebook.stetho.inspector.helper.ChromePeerManager;
import com.facebook.stetho.inspector.helper.PeerRegistrationListener;
import com.facebook.stetho.inspector.helper.PeersRegisteredListener;
import com.facebook.stetho.inspector.protocol.module.Console;
import com.facebook.stetho.inspector.protocol.module.DOMStorage;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DOMStoragePeerManager extends ChromePeerManager {
  private final Context mContext;

  public DOMStoragePeerManager(Context context) {
    mContext = context;
    setListener(mPeerListener);
  }

  public void signalItemRemoved(DOMStorage.StorageId storageId, String key) {
    DOMStorage.DomStorageItemRemovedParams params =
        new DOMStorage.DomStorageItemRemovedParams();
    params.storageId = storageId;
    params.key = key;
    sendNotificationToPeers("DOMStorage.domStorageItemRemoved", params);
  }

  public void signalItemAdded(DOMStorage.StorageId storageId, String key, String value) {
    DOMStorage.DomStorageItemAddedParams params =
        new DOMStorage.DomStorageItemAddedParams();
    params.storageId = storageId;
    params.key = key;
    params.newValue = value;
    sendNotificationToPeers("DOMStorage.domStorageItemAdded", params);
  }

  public void signalItemUpdated(
      DOMStorage.StorageId storageId,
      String key,
      String oldValue,
      String newValue) {
    DOMStorage.DomStorageItemUpdatedParams params =
        new DOMStorage.DomStorageItemUpdatedParams();
    params.storageId = storageId;
    params.key = key;
    params.oldValue = oldValue;
    params.newValue = newValue;
    sendNotificationToPeers("DOMStorage.domStorageItemUpdated", params);
  }

  private final PeerRegistrationListener mPeerListener = new PeersRegisteredListener() {
    private final List<DevToolsSharedPreferencesListener> mPrefsListeners =
        new ArrayList<DevToolsSharedPreferencesListener>();

    @Override
    protected synchronized void onFirstPeerRegistered() {
      // TODO: We list the tags in Page.getResourceTree as well and those are the real fixed
      // tags that will be observed by the peer.  We can fix this by making the page frames
      // dynamically update in response to DOMStorage events.  This would also allow us to
      // add new SharedPreferences tags as we observe them being created by way of
      // android.os.FileObserver.
      List<String> tags = SharedPreferencesHelper.getSharedPreferenceTags(mContext);
      for (String tag : tags) {
        SharedPreferences prefs = mContext.getSharedPreferences(tag, Context.MODE_PRIVATE);
        DevToolsSharedPreferencesListener listener =
            new DevToolsSharedPreferencesListener(prefs, tag);
        prefs.registerOnSharedPreferenceChangeListener(listener);
        mPrefsListeners.add(listener);
      }
    }

    @Override
    protected synchronized void onLastPeerUnregistered() {
      for (DevToolsSharedPreferencesListener prefsListener : mPrefsListeners) {
        prefsListener.unregister();
      }
      mPrefsListeners.clear();
    }
  };

  private class DevToolsSharedPreferencesListener
      implements SharedPreferences.OnSharedPreferenceChangeListener {
    private final SharedPreferences mPrefs;
    private final DOMStorage.StorageId mStorageId;

    /**
     * Maintains a copy of the prefs data structure so that we can invoke
     * {@code DOMStorage.domStorageItemUpdated}.  This method requires that we know the old
     * value to perform updates.  Using {@code domStorageItemRemoved}/{@code Added} causes a UI
     * glitch where the item is moved to the end of the list, unfortunately.
     */
    private final Map<String, Object> mCopy;

    public DevToolsSharedPreferencesListener(SharedPreferences prefs, String tag) {
      mPrefs = prefs;
      mStorageId = new DOMStorage.StorageId();
      mStorageId.securityOrigin = tag;
      mStorageId.isLocalStorage = true;
      mCopy = prefsCopy(prefs.getAll());
    }

    public void unregister() {
      mPrefs.unregisterOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
      Map<String, ?> entries = sharedPreferences.getAll();
      boolean existedBefore = mCopy.containsKey(key);
      boolean existsNow = entries.containsKey(key);
      Object newValue = existsNow ? entries.get(key) : null;
      if (existedBefore && existsNow) {
        signalItemUpdated(
            mStorageId,
            key,
            SharedPreferencesHelper.valueToString(mCopy.get(key)),
            SharedPreferencesHelper.valueToString(newValue));
        mCopy.put(key, newValue);
      } else if (existedBefore) {
        signalItemRemoved(mStorageId, key);
        mCopy.remove(key);
      } else if (existsNow) {
        signalItemAdded(
            mStorageId,
            key,
            SharedPreferencesHelper.valueToString(newValue));
        mCopy.put(key, newValue);
      } else {
        throw new RuntimeException("Prefs change detected for unknown key=" + key);
      }
    }
  }

  private static Map<String, Object> prefsCopy(Map<String, ?> src) {
    HashMap<String, Object> dst = new HashMap<String, Object>(src.size());
    for (Map.Entry<String, ?> entry : src.entrySet()) {
      String key = entry.getKey();
      Object value = entry.getValue();
      if (value instanceof Set) {
        dst.put(key, shallowCopy((Set<String>)value));
      } else {
        dst.put(key, value);
      }
    }
    return dst;
  }

  private static <T> Set<T> shallowCopy(Set<T> src) {
    HashSet<T> dst = new HashSet<T>();
    for (T item : src) {
      dst.add(item);
    }
    return dst;
  }
}

@@ -427,16 +427,59 @@
 rences;%0A
+import com.facebook.stetho.common.LogUtil;%0A
 import c
@@ -5622,77 +5622,242 @@
     
-throw new RuntimeException(%22Prefs change detected for unknown key=%22 +
+// This can happen due to the async nature of the onSharedPreferenceChanged callback.  A%0A        // rapid put/remove as two separate commits on a background thread would cause this.%0A        LogUtil.i(%22Detected rapid put/remove of %25s%22,
  key

Fix crash when rapidly adding/removing SharedPreferences keys