// Usage
function ProfilePage({ uid }) {
  // Subscribe to Firestore document
  const { data, status, error } = useFirestoreQuery(
    firestore.collection("profiles").doc(uid)
  );
  
  if (status === "loading"){
    return "Loading..."; 
  }
  
  if (status === "error"){
    return `Error: ${error.message}`;
  }

  return (
    <div>
      <ProfileHeader avatar={data.avatar} name={data.name} />
      <Posts posts={data.posts} />
    </div>
  );
}

// Reducer for hook state and actions
const reducer = (state, action) => {
  switch (action.type) {
    case "idle":
      return { status: "idle", data: undefined, error: undefined };
    case "loading":
      return { status: "loading", data: undefined, error: undefined };
    case "success":
      return { status: "success", data: action.payload, error: undefined };
    case "error":
      return { status: "error", data: undefined, error: action.payload };
    default:
      throw new Error("invalid action");
  }
}

// Hook
function useFirestoreQuery(query) {
  // Our initial state
  // Start with an "idle" status if query is falsy, as that means hook consumer is
  // waiting on required data before creating the query object.
  // Example: useFirestoreQuery(uid && firestore.collection("profiles").doc(uid))
  const initialState = { 
    status: query ? "loading" : "idle", 
    data: undefined, 
    error: undefined 
  };
  
  // Setup our state and actions
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // Get cached Firestore query object with useMemoCompare (https://usehooks.com/useMemoCompare)
  // Needed because firestore.collection("profiles").doc(uid) will always being a new object reference
  // causing effect to run -> state change -> rerender -> effect runs -> etc ...
  // This is nicer than requiring hook consumer to always memoize query with useMemo.
  const queryCached = useMemoCompare(query, prevQuery => {
    // Use built-in Firestore isEqual method to determine if "equal"
    return prevQuery && query && query.isEqual(prevQuery);
  });

  useEffect(() => {
    // Return early if query is falsy and reset to "idle" status in case
    // we're coming from "success" or "error" status due to query change.
    if (!queryCached) {
      dispatch({ type: "idle" });
      return;
    }
    
    dispatch({ type: "loading" });
    
    // Subscribe to query with onSnapshot
    // Will unsubscribe on cleanup since this returns an unsubscribe function
    return queryCached.onSnapshot(
      response => {
        // Get data for collection or doc
        const data = response.docs
          ? getCollectionData(response)
          : getDocData(response);
        
        dispatch({ type: "success", payload: data });
      },
      error => {
        dispatch({ type: "error", payload: error });
      }
    );
    
  }, [queryCached]); // Only run effect if queryCached changes

  return state;
}

// Get doc data and merge doc.id
function getDocData(doc) {
  return doc.exists === true ? { id: doc.id, ...doc.data() } : null;
}

// Get array of doc data from collection
function getCollectionData(collection) {
  return collection.docs.map(getDocData);
}