import { useLayoutEffect, useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { createFilter } from '@vizality/util/filter';
import { getObjectURL } from '@vizality/util/file';

import { get, post, put, del } from '../http';

/**
 *
 * @param {string} requestType Request type. One of get, post, put, or del.
 * @param {url} url URL to call
 * @param {string|object} [headers] Headers
 */
export const useFetch = (requestType, url, headers) => {
  const [ response, setResponse ] = useState(null);
  const [ loading, setLoading ] = useState(true);
  const [ hasError, setHasError ] = useState(false);

  useEffect(() => {
    switch (requestType.toLowerCase()) {
      case 'get':
        return get(url, headers)
          .then(res => {
            setResponse(res.data);
            setLoading(false);
          })
          .catch(() => {
            setHasError(true);
            setLoading(false);
          });
      case 'post':
        return post(url, headers)
          .then(res => {
            setResponse(res.data);
            setLoading(false);
          })
          .catch(() => {
            setHasError(true);
            setLoading(false);
          });
      case 'put':
        return put(url, headers)
          .then(res => {
            setResponse(res.data);
            setLoading(false);
          })
          .catch(() => {
            setHasError(true);
            setLoading(false);
          });
      case 'del':
        return del(url, headers)
          .then(res => {
            setResponse(res.data);
            setLoading(false);
          })
          .catch(() => {
            setHasError(true);
            setLoading(false);
          });
      default:
        return get(url, headers)
          .then(res => {
            setResponse(res.data);
            setLoading(false);
          })
          .catch(() => {
            setHasError(true);
            setLoading(false);
          });
    }
  }, [ url ]);

  return [ response, loading, hasError ];
};

/**
 * Hook that uses `util.file.getObjectURL` to get an async collection of blob object URLs.
 * @param {string} path Path to the file or directory to be included
 * @param {string|Array} [allowedExtensions] List of desired file-type extensions to be included
 * @example
 * ```
 * const [ images, loading, error ] = useFetchImageObjectURL(dir);
 * return (
 *   {error && 'Uhoh, something went wrong.'}
 *   {loading && <Spinner />}
 *   {images?.length && (
 *     <SomeReallyCoolComponent>
 *       <SomeChidren />
 *     </SomeReallyCoolComponent>
 *   )}
 * )
 * ```
 * @note You will still need to revoke the URLs from this. A good idea is usually to revoke on unmount
 * which can be done as such:
 * @example
 * ```
 * useEffect(() => {
 *   return () => images?.forEach(image => URL.revokeObjectURL(image.url));
 * }, [ images ]);
 * ```
 */
export const useFetchImageObjectURL = (path, allowedExtensions) => {
  const [ response, setResponse ] = useState(null);
  const [ loading, setLoading ] = useState(true);
  const [ hasError, setHasError ] = useState(false);

  useEffect(() => {
    getObjectURL(path, allowedExtensions)
      .then(res => {
        setResponse(res);
        setLoading(false);
      })
      .catch(() => {
        setHasError(true);
        setLoading(false);
      });
  }, [ path, allowedExtensions ]);

  return [ response, loading, hasError ];
};

/**
 * Simple hook to force a component to rerender.
 * @copyright Copyright (c) 2019 Hovhannes Babayan
 * @license MIT
 * @see {@link https://github.com/bhovhannes/use-force-update-hook}
 * @example
 * ```
 * const forceUpdate = useForceUpdate();
 * ...
 * forceUpdate();
 * ```
 */
export const useForceUpdate = () => {
  const setValue = useState(0)[1];
  return useRef(() => setValue(v => ~v)).current;
};

/**
 * In rare cases you may need to do something right after component forceUpdate finishes.
 * In that case useForceUpdateWithCallback can be useful.
 * @copyright Copyright (c) 2019 Hovhannes Babayan
 * @license MIT
 * @see {@link https://github.com/bhovhannes/use-force-update-hook}
 * @param {*} callback Callback
 * @example
 * ```
 * function handleUpdate () {
 *   console.log('Just updated.')
 * }
 *
 * const forceUpdate = useForceUpdateWithCallback(handleUpdate)
 *
 * return (
 *   <div>
 *     <button onClick={forceUpdate}>
 *       Click to rerender MyAwesomeComponent
 *     </button>
 *   </div>
 * );
 * ```
 */
export const useForceUpdateWithCallback = callback => {
  const [ value, setValue ] = useState(0);
  const isUpdating = useRef(0);
  useLayoutEffect(() => {
    if (isUpdating.current) {
      isUpdating.current = 0;
      return callback();
    }
  }, [ callback, value ]);
  return useRef(() => {
    isUpdating.current = 1;
    setValue(v => ~v);
  }).current;
};

export const usePreviousProps = value => {
  /*
   * The ref object is a generic container whose current property is mutable
   * and can hold any value, similar to an instance property on a class.
   */
  const ref = useRef();
  /**
   * Store the current value in ref.
   */
  useEffect(() => {
    ref.current = value;
  }, [ value ]); // Only re-run if value changes
  /**
   * Return previous value (happens before update in useEffect above).
   */
  return ref.current;
};

/**
 * Toggles a value.
 * @param {boolean} [initialState=false] Initial state
 * @example
 * ```
 * function App() {
 *   const [isTextChanged, setIsTextChanged] = useToggle();
 *
 *   return (
 *     <button onClick={setIsTextChanged}>
 *       {isTextChanged ? 'Toggled' : 'Click to Toggle'}
 *     </button>
 *   );
 * }
 * ```
 */
export const useToggle = (initialState = false) => {
  /**
   * Initialize the state.
   */
  const [ state, setState ] = useState(initialState);

  /**
   * Define and memorize toggler function in case we pass down the comopnent,
   * This function change the boolean value to it's opposite value
   */
  const toggle = useCallback(() => setState(state => !state), []);

  return [ state, toggle ];
};

/**
 * Filters data based on a query, with the option to filter by keys and
 * includes some other options.
 * @param {object} filter Filter parameters
 * @param {Array<string>} filter.data Data
 * @param {Array<string>|string} [filter.keys] Keys in the data to process
 * @param {object} [filter.options] Extra filter options
 * @param {boolean} [options.fuzzy] Whether or not the search should be fuzzy (return results that do not match exactly)
 * @param {boolean} [options.caseSensitive] Whether or not the search should be case sensitive
 * @param {boolean} [options.sortByRelevance] Whether or not the search should be sorted by relevance (only applicable if fuzzy option is true)
 * @param {boolean} [options.exactMatch] Whether or not the search should return exact matches only
 * @example
 * ```
 * const App = () => {
 *   const [ inputText, setInputText, filtered ] = useFilter({
 *     keys: [ 'user.name', 'subject', 'dest.name' ],
 *     data: sampleData,
 * options: 
 *   });
 *
 *   return (
 *     <div className='App'>
 *       <input
 *         type='text'
 *         value={inputText}
 *         onChange={evt => setInputText(evt.target.value)}
 *       />
 *       {JSON.stringify(filtered)}
 *     </div>
 *   );
 * };
 * ```
 */
export const useFilter = ({ data, keys, options }) => {
  const [ query, setQuery ] = useState('');
  const filtered = useMemo(() => data.filter(createFilter(query, keys)), [ data, query, options ]);
  return [ query, setQuery, filtered ];
};

/**
 *
 * Sourced from @see {@link https://codesandbox.io/s/animate-height-framer-motion-yn59l?file=/src/use-callback-ref.js:48-209}
 * @returns
 */
export const useCallbackRef = () => {
  const [ ref, setRef ] = useState(null);
  const fn = useCallback(node => {
    setRef(node);
  }, []);
  return [ ref, fn ];
};

/**
 *
 * Sourced from @see {@link https://codesandbox.io/s/animate-height-framer-motion-yn59l?file=/src/use-measure.js}
 * @returns
 */
export const useMeasure = ref => {
  const [ element, attachRef ] = useCallbackRef();
  const [ bounds, setBounds ] = useState({});

  useEffect(() => {
    function onResize ([ entry ]) {
      setBounds({
        height: entry.contentRect.height
      });
    }

    const observer = new ResizeObserver(onResize);

    if (element?.current) {
      observer.observe(element.current);
    }

    return () => observer.disconnect();
  }, [ element ]);

  useEffect(() => {
    attachRef(ref);
  }, [ attachRef, ref ]);

  return bounds;
};

export default this;
