const observable = initial => {
  let internal = initial;
  const listeners = new Set();
  const obs = {};
  obs.get = () => internal;
  obs.set = newValue => {
    const oldValue = internal;
    internal = newValue;
    listeners.forEach(listener => listener(newValue, oldValue));
  };
  obs.sub = callback => {
    listeners.add(callback);
    return () => {
      listeners.delete(callback);
    };
  };
  // ADDED FOR MOCKING
  obs.reset = () => {
    internal = initial;
  };

  // MOCKING MAKES THIS A DIRECT GET
  const useValue = obs.get;

  return [obs, useValue];
};

const storeProxy = (store, actions) => {
  const read = key => store[key].get();
  const write = (key, value) => store[key].set(value);

  return new Proxy(
    changes =>
      Object.entries(changes).forEach(([key, change]) =>
        write(key, typeof change === "function" ? change(read(key)) : change)
      ),
    {
      ownKeys: () => [...Reflect.ownKeys(store), ...Reflect.ownKeys(actions)],
      get: (_, key) => actions[key] ?? read(key),
      set: (_, key, value) => {
        if (Object.prototype.hasOwnProperty.call(store, key)) {
          write(key, value);
          return true;
        }
        return false;
      },
    }
  );
};

const create = definition => {
  const store = Object.create(null);
  const hooks = Object.create(null);
  const actions = Object.create(null);

  const watch = {
    onChange: callback =>
      Object.entries(store).forEach(([key, obs]) =>
        obs.sub((newValue, oldValue) => callback(key, newValue, oldValue))
      ),
    onCall: callback =>
      Object.entries(actions).forEach(([key, act]) => {
        actions[key] = (...args) => {
          callback(key, ...args);
          return act(...args);
        };
      }),
    setGlobal: storeName => {
      window[storeName] = storeProxy(store, actions);
    },
  };

  Object.entries(definition(storeProxy(store, actions))).forEach(
    ([key, value]) => {
      if (typeof value === "function") {
        actions[key] = value;
      } else {
        const hookName = `use${key.charAt(0).toUpperCase()}${key.slice(1)}`;
        [store[key], hooks[hookName]] = observable(value);
      }
    }
  );

  // ADDED FOR MOCKING
  actions.GET_STORED_VALUE = key => store[key].get();
  // ADDED FOR MOCKING
  actions.SET_STORED_VALUE = (key, value) => store[key].set(value);
  // ADDED FOR MOCKING
  actions.RESET_STORE = () =>
    Object.entries(store).forEach(([, { reset }]) => reset());

  return [hooks, actions, watch];
};

export default create;
