import React, { useMemo as useMemo$1 } from 'react';

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }

  return obj;
}

class Cache {
  constructor(cacheSize = DEFAULT_CACHE_SIZE) {
    _defineProperty(this, "_cache", new Map([]));

    this.cacheSize = cacheSize;
    this.softResetCache = null;
  }

  get keys() {
    return [...this._cache.keys()];
  }

  get entries() {
    return [...this._cache];
  }

  get(key) {
    return this._cache.get(key);
  }

  set(key, results) {
    this._cache.set(key, results);
  }

  removeItem(key) {
    this._cache.delete(key);
  }

  clearCache() {
    this._cache.clear();
  }

  clone(filter = x => true) {
    const result = new Cache(this.cacheSize);
    result._cache = new Map([...this._cache].filter(filter));
    return result;
  }

  setPendingResult(graphqlQuery, promise) {
    let cache = this._cache; //front of the line now, to support LRU ejection

    cache.delete(graphqlQuery);

    if (cache.size === this.cacheSize) {
      //maps iterate entries and keys in insertion order - zero'th key should be oldest
      cache.delete([...cache.keys()][0]);
    }

    cache.set(graphqlQuery, promise);
  }

  setResults(promise, cacheKey, resp, err) {
    let cache = this._cache; //cache may have been cleared while we were running. If so, we'll respect that, and not touch the cache, but we'll still use the results locally

    if (cache.get(cacheKey) !== promise) return;

    if (err) {
      cache.set(cacheKey, {
        data: null,
        error: err
      });
    } else {
      if (resp.errors) {
        cache.set(cacheKey, {
          data: null,
          error: resp.errors
        });
      } else {
        cache.set(cacheKey, {
          data: resp.data,
          error: null
        });
      }
    }
  }

  getFromCache(key, ifPending, ifResults, ifNotFound = () => {}, byPassSoftReset = false) {
    let cache = this._cache;
    let cachedEntry = cache.get(key);

    if (!byPassSoftReset && this.softResetCache) {
      if (this.softResetCache[key]) {
        return ifResults(this.softResetCache[key]);
      }
    }

    if (cachedEntry) {
      if (typeof cachedEntry.then === "function") {
        ifPending(cachedEntry);
      } else {
        //re-insert to put it at the fornt of the line
        cache.delete(key);
        this.set(key, cachedEntry);
        ifResults(cachedEntry);
        this.softResetCache = null;
      }
    } else {
      ifNotFound();
    }
  }

}
const DEFAULT_CACHE_SIZE = 10;

class Client {
  constructor(props = {
    cacheSize: DEFAULT_CACHE_SIZE
  }) {
    Object.assign(this, props);
    this.caches = new Map([]);
    this.mutationListeners = new Set([]);
    this.forceListeners = new Map([]);
  }

  get cacheSizeToUse() {
    return this.cacheSize > 0 ? this.cacheSize : DEFAULT_CACHE_SIZE;
  }

  preload(query, variables) {
    let cache = this.getCache(query);

    if (!cache) {
      cache = this.newCacheForQuery(query);
    }

    let graphqlQuery = this.getGraphqlQuery({
      query,
      variables
    });
    let promiseResult;
    cache.getFromCache(graphqlQuery, promise => {
      promiseResult = promise;
      /* already preloading - cool */
    }, cachedEntry => {
      /* already loaded - cool */
    }, () => {
      let promise = this.runUri(graphqlQuery);
      cache.setPendingResult(graphqlQuery, promise);
      promiseResult = promise;
      promise.then(resp => {
        cache.setResults(promise, graphqlQuery, resp);
      });
    });
    return promiseResult;
  }

  read(query, variables) {
    let cache = this.getCache(query);

    if (!cache) {
      cache = this.newCacheForQuery(query);
    }

    let graphqlQuery = this.getGraphqlQuery({
      query,
      variables
    });
    let cachedResult;
    cache.getFromCache(graphqlQuery, promise => {
      /* already preloading - cool */
    }, cachedEntry => {
      /* already loaded - cool */
      cachedResult = cachedEntry;
    }, () => {
      let promise = this.runUri(graphqlQuery);
      cache.setPendingResult(graphqlQuery, promise);
      promise.then(resp => {
        cache.setResults(promise, graphqlQuery, resp);
      });
    });

    return cachedResult;
  }

  getCache(query) {
    return this.caches.get(query);
  }

  newCacheForQuery(query) {
    let newCache = new Cache(this.cacheSizeToUse);
    this.setCache(query, newCache);
    return newCache;
  }

  setCache(query, cache) {
    this.caches.set(query, cache);
  }

  runQuery(query, variables) {
    return this.runUri(this.getGraphqlQuery({
      query,
      variables
    }));
  }

  runUri(uri) {
    return fetch(uri, this.fetchOptions || void 0).then(resp => resp.json());
  }

  getGraphqlQuery({
    query,
    variables
  }) {
    return `${this.endpoint}?query=${encodeURIComponent(query)}${typeof variables === "object" ? `&variables=${encodeURIComponent(JSON.stringify(variables))}` : ""}`;
  }

  subscribeMutation(subscription, options = {}) {
    if (!Array.isArray(subscription)) {
      subscription = [subscription];
    }

    const packet = {
      subscription,
      options
    };

    if (!options.currentResults) {
      options.currentResults = () => ({});
    }

    this.mutationListeners.add(packet);
    return () => this.mutationListeners.delete(packet);
  }

  forceUpdate(query) {
    let updateListeners = this.forceListeners.get(query);

    if (updateListeners) {
      for (let refresh of updateListeners) {
        refresh();
      }
    }
  }

  registerQuery(query, refresh) {
    if (!this.forceListeners.has(query)) {
      this.forceListeners.set(query, new Set([]));
    }

    this.forceListeners.get(query).add(refresh);
    return () => this.forceListeners.get(query).delete(refresh);
  }

  processMutation(mutation, variables) {
    const refreshActiveQueries = query => this.forceUpdate(query);

    return Promise.resolve(this.runMutation(mutation, variables)).then(resp => {
      let mutationKeys = Object.keys(resp);
      let mutationKeysLookup = new Set(mutationKeys);
      [...this.mutationListeners].forEach(({
        subscription,
        options: {
          currentResults,
          isActive,
          ...rest
        }
      }) => {
        subscription.forEach(singleSubscription => {
          if (typeof isActive === "function") {
            if (!isActive()) {
              return;
            }
          }

          if (typeof singleSubscription.when === "string") {
            if (mutationKeysLookup.has(singleSubscription.when)) {
              singleSubscription.run({
                currentResults: currentResults(),
                refreshActiveQueries,
                ...rest
              }, resp, variables);
            }
          } else if (typeof singleSubscription.when === "object" && singleSubscription.when.test) {
            if ([...mutationKeysLookup].some(k => singleSubscription.when.test(k))) {
              singleSubscription.run({
                currentResults: currentResults(),
                refreshActiveQueries,
                ...rest
              }, resp, variables);
            }
          }
        });
      });
      return resp;
    });
  }

  runMutation(mutation, variables) {
    let {
      headers = {},
      ...otherOptions
    } = this.fetchOptions || {};
    return fetch(this.endpoint, {
      method: "post",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        ...headers
      },
      ...otherOptions,
      body: JSON.stringify({
        query: mutation,
        variables
      })
    }).then(resp => resp.json()).then(resp => resp.data);
  }

}

class DefaultClientManager {
  constructor() {
    _defineProperty(this, "defaultClient", null);

    _defineProperty(this, "setDefaultClient", client => this.defaultClient = client);

    _defineProperty(this, "getDefaultClient", () => this.defaultClient);
  }

}

const defaultClientManager = new DefaultClientManager();

function compress(strings, ...expressions) {
  return strings.map((string, i) => {
    const expression = i < expressions.length ? expressions[i] : "";
    return string.replace(/\s+/g, " ").replace(/ ,/g, ",").replace(/, /g, ",").replace(/ :/g, ":").replace(/: /g, ":").replace(/{ /g, "{").replace(/} /g, "}").replace(/ {/g, "{").replace(/ }/g, "}").replace(/ \(/g, "(").replace(/ \)/g, ")").replace(/\( /g, "(").replace(/\) /g, ")") + expression;
  }).join("").trim();
}

class QueryManager {
  constructor({
    client,
    refreshCurrent,
    hookRefs,
    cache,
    setState,
    suspense
  }) {
    const {
      isActiveRef,
      queryStateRef
    } = hookRefs;
    Object.assign(this, {
      client,
      cache,
      isActiveRef,
      queryStateRef,
      refreshCurrent,
      suspense,
      setState
    });
    this.trackedPromises = new Set([]);
  }

  updateState(newState, existingState) {
    if (!this.setState) {
      return;
    }

    if (existingState) {
      const doUpdate = Object.keys(newState).some(k => newState[k] !== existingState[k]);
      if (!doUpdate) return;
    }

    this.setState(state => Object.assign({}, state, newState));
  }

  sync({
    query,
    variables,
    queryState
  }, firstRun = false) {
    let graphqlQuery = this.client.getGraphqlQuery({
      query,
      variables
    });
    this.cache.getFromCache(graphqlQuery, promise => {
      this.promisePending(promise, queryState);
    }, cachedEntry => {
      this.updateState({
        data: cachedEntry.data,
        error: cachedEntry.error || null,
        loading: false,
        loaded: true,
        currentQuery: graphqlQuery
      }, queryState);
    }, () => {
      let promise = this.execute(graphqlQuery);
      this.trackedPromises.add(promise);
      this.promisePending(promise, queryState);
    }, firstRun);
  }

  refresh() {
    this.refreshCurrent && this.refreshCurrent();
  }

  promisePending(promise, queryState) {
    if (this.suspense) {
      throw promise;
    } else {
      this.updateState({
        loading: true
      }, queryState);

      if (!this.trackedPromises.has(promise)) {
        this.trackedPromises.add(promise);
        promise.then(() => this.refresh()).catch(() => this.refresh());
      }
    }
  }

  execute(graphqlQuery) {
    let promise = this.client.runUri(graphqlQuery);
    this.cache.setPendingResult(graphqlQuery, promise);
    return Promise.resolve(promise).then(resp => {
      this.trackedPromises.delete(promise);
      this.cache.setResults(promise, graphqlQuery, resp);
      this.refresh();
    }).catch(err => {
      this.trackedPromises.delete(promise);
      this.cache.setResults(promise, graphqlQuery, null, err);
      this.refresh();
    });
  }

}

const {
  useState,
  useRef,
  useLayoutEffect
} = React;
const initialState = {
  loading: false,
  loaded: false,
  currentQuery: null,
  data: null,
  error: null
};
function useQuery(query, variables, options = {}, {
  suspense
} = {}) {
  const [deactivateQueryToken, setDeactivateQueryToken] = useState(0);

  const refresh = () => {
    setDeactivateQueryToken(x => x + 1);
  };

  const clientRef = useRef(options.client || defaultClientManager.getDefaultClient());
  const cacheRef = useRef(clientRef.current.getCache(query) || clientRef.current.newCacheForQuery(query));
  const isActive = !("active" in options && !options.active);
  const isActiveRef = useRef(isActive);
  const [queryState, setQueryState] = useState(() => {
    let existingState = {};

    if (isActiveRef.current) {
      const graphqlQuery = clientRef.current.getGraphqlQuery({
        query,
        variables
      });
      cacheRef.current.getFromCache(graphqlQuery, promise => {}, cachedEntry => {
        existingState = {
          data: cachedEntry.data,
          error: cachedEntry.error || null,
          loading: false,
          loaded: true,
          currentQuery: graphqlQuery
        };
      }, void 0, true);
    }

    return { ...initialState,
      ...existingState
    };
  });
  const queryStateRef = useRef(queryState);

  const getQueryManager = cache => new QueryManager({
    client: clientRef.current,
    cache,
    hookRefs: {
      isActiveRef,
      queryStateRef
    },
    setState: setQueryState,
    refreshCurrent: suspense ? null : refresh,
    suspense
  });

  const [queryManager, setQueryManager] = useState(() => getQueryManager(cacheRef.current));

  const resetQueryManager = cacheFilter => {
    const newCache = cacheRef.current.clone(cacheFilter);
    clientRef.current.setCache(query, newCache);
    setQueryManager(getQueryManager(newCache));
  };

  const reload = () => resetQueryManager(([k]) => k != queryStateRef.current.currentQuery);

  const hardReset = () => resetQueryManager(() => false);

  const softReset = newResults => {
    if (!newResults) {
      newResults = queryStateRef.current.data;
    }

    cacheRef.current.clearCache();
    cacheRef.current.softResetCache = {
      [queryStateRef.current.currentQuery]: {
        data: newResults
      }
    };
    setQueryState(oldState => ({ ...oldState,
      data: newResults
    }));
  }; // ------------------------------- effects -------------------------------


  useLayoutEffect(() => {
    isActiveRef.current = isActive;
    queryStateRef.current = queryState;
  }, [isActive, queryState]);
  useLayoutEffect(() => {
    const unregisterQuery = clientRef.current.registerQuery(query, refresh);
    let mutationSubscription;

    if (typeof options.onMutation === "object") {
      const onMutation = !Array.isArray(options.onMutation) ? [options.onMutation] : options.onMutation;
      mutationSubscription = clientRef.current.subscribeMutation(onMutation, {
        cache: cacheRef.current,
        softReset,
        hardReset,
        refresh,
        currentResults: () => queryStateRef.current.data,
        isActive: () => isActiveRef.current
      });
    }

    cacheRef.current = queryManager.cache;
    return () => {
      queryManager.setState = () => {};

      queryManager.refreshCurrent = () => {};

      mutationSubscription && mutationSubscription();
      unregisterQuery();
    };
  }, [queryManager]); // ------------------------------- effects -------------------------------

  const firstRun = useRef(true);
  useLayoutEffect(() => {
    firstRun.current = false;
  }, []);

  if (isActive) {
    queryManager.sync({
      query,
      variables,
      queryState
    }, firstRun.current);
  }

  return useMemo$1(() => {
    return { ...queryState,
      reload,
      clearCache: () => cacheRef.current.clearCache(),
      clearCacheAndReload: hardReset,
      softReset
    };
  }, [queryState]);
}
const useSuspenseQuery = (query, variables, options = {}) => useQuery(query, variables, options, {
  suspense: true
});

class MutationManager {
  constructor({
    client,
    setState
  }, mutation, options) {
    _defineProperty(this, "runMutation", variables => {
      this.setState({
        running: true,
        finished: false,
        runMutation: this.runMutation
      });
      return this.client.processMutation(this.mutation, variables).then(resp => {
        this.setState({
          running: false,
          finished: true,
          runMutation: this.runMutation
        });
        return resp;
      });
    });

    _defineProperty(this, "currentState", { ...MutationManager.initialState,
      runMutation: this.runMutation
    });

    this.client = client;
    this.setState = setState;
    this.mutation = mutation;
    this.setState(this.currentState);
  }

}

_defineProperty(MutationManager, "initialState", {
  running: false,
  finished: false
});

const {
  useState: useState$1,
  useRef: useRef$1,
  useMemo,
  useLayoutEffect: useLayoutEffect$1
} = React;
function useMutation(mutation, options = {}) {
  let [mutationState, setMutationState] = useState$1(null);
  let client = options.client || defaultClientManager.getDefaultClient();
  let [mutationManager] = useState$1(() => {
    return new MutationManager({
      client,
      setState: setMutationState
    }, mutation, options);
  });
  useLayoutEffect$1(() => () => mutationManager.setState = () => {}, []);
  return mutationState || mutationManager.currentState;
}

const {
  setDefaultClient,
  getDefaultClient
} = defaultClientManager;

export { Cache, Client, compress, getDefaultClient, setDefaultClient, useMutation, useQuery, useSuspenseQuery };
