import React from "react";
export const DataContext = React.createContext({});

export const InternalContext = React.createContext({
  requests: [],
  resolved: false,
  current: 0,
});

import { useContext, useState, useEffect, DependencyList, Props } from "react";

interface IInternalContext {
  requests: {
    promise: Promise<any>;
    id: number;
    cancel: Function;
  }[];
  resolved: boolean;
  current: number;
}
interface IDataContext {
  [k: string]: any;
}

declare global {
  interface Window {
    [k: string]: any;
    _initialDataContext: object;
  }
}

/**
 *
 * @param effect runction returning promise
 * @param dependencies  list of dependencies like in useEffect
 */
export function useSSE<T>(
  effect: () => Promise<any>,
  dependencies?: DependencyList
): T[] {
  const internalContext: IInternalContext = useContext(InternalContext);
  let callId = internalContext.current;
  internalContext.current++;
  const ctx: IDataContext = useContext(DataContext);
  const [data, setData] = useState(ctx[callId]?.data || null);
  const [error, setError] = useState(ctx[callId]?.error || null);

  if (!internalContext.resolved) {
    let cancel = Function.prototype;

    const effectPr = new Promise((resolve) => {
      cancel = () => {
        if (!ctx[callId]) {
          ctx[callId] = { error: { messgae: "timeout" }, id: callId };
        }
        resolve(callId);
      };
      return effect()
        .then((res) => {
          return res;
        })
        .then((res) => {
          ctx[callId] = { data: res };
          resolve(callId);
        })
        .catch((error) => {
          ctx[callId] = { error: error };
          resolve(callId);
        });
    });

    internalContext.requests.push({
      id: callId,
      promise: effectPr,
      cancel: cancel,
    });
  }

  useEffect(() => {
    if (internalContext.resolved && !ctx[callId]) {
      effect()
        .then((res) => {
          setData(res);
        })
        .catch((error) => {
          setError(error);
        });
    }
    delete ctx[callId];
  }, dependencies);

  return [data, error];
}

export const createBroswerContext = (
  variableName: string = "_initialDataContext"
) => {
  const initial = window && window[variableName] ? window[variableName] : {};
  let internalContextValue: IInternalContext = {
    current: 0,
    resolved: true,
    requests: [],
  };
  function BroswerDataContext<T>(props: Props<T>) {
    return (
      <InternalContext.Provider value={internalContextValue}>
        <DataContext.Provider value={initial}>
          {props.children}
        </DataContext.Provider>
      </InternalContext.Provider>
    );
  }

  return BroswerDataContext;
};

const wait = (time: number) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject({ error: "timeout" });
    }, time);
  });
};

export const createServerContext = () => {
  let ctx: IDataContext = {};
  let internalContextValue: IInternalContext = {
    current: 0,
    resolved: false,
    requests: [],
  };
  function ServerDataContext<T>(props: Props<T>) {
    return (
      <InternalContext.Provider value={internalContextValue}>
        <DataContext.Provider value={ctx}>
          {props.children}
        </DataContext.Provider>
      </InternalContext.Provider>
    );
  }
  const resolveData = async (timeout?: number) => {
    const effects = internalContextValue.requests.map((item) => item.promise);

    if (timeout) {
      const timeOutPr = wait(timeout);

      await Promise.all(
        internalContextValue.requests.map((effect, index) => {
          return Promise.race([effect.promise, timeOutPr]).catch(() => {
            return effect.cancel();
          });
        })
      );
    } else {
      await Promise.all(effects);
    }

    internalContextValue.resolved = true;
    internalContextValue.current = 0;
    return {
      data: ctx,
      toJSON: function () {
        return this.data;
      },
      toHtml: function (variableName: string = "_initialDataContext") {
        return `<script>window.${variableName} = ${JSON.stringify(
          this
        )};</script>`;
      },
    };
  };
  return {
    ServerDataContext,
    resolveData,
  };
};