import { createContext, createElement, useContext, Fragment, Component, useEffect, useMemo, useRef, useState } from 'react';
import _get from 'lodash/fp/get';
import _omit from 'lodash/fp/omit';
import equal from 'fast-deep-equal';
import _merge from 'lodash/fp/merge';
import sort from 'array-sort';
import _unionBy from 'lodash/fp/unionBy';
import _flattenDepth from 'lodash/fp/flattenDepth';
import _pipe from 'lodash/fp/pipe';
import { ulid } from 'ulid';
import match from 'match-sorter';
import _throttle from 'lodash/fp/throttle';

const DefaultNotFound = () => createElement(Fragment, null, "Not found");

const DefaultLoading = () => createElement(Fragment, null, "Loading");

const DefaultPage = ({
  children
}) => createElement(Fragment, null, children);

const DefaultPlayground = ({
  component,
  code
}) => createElement(Fragment, null, component, code);

const defaultComponents = {
  loading: DefaultLoading,
  playground: DefaultPlayground,
  notFound: DefaultNotFound,
  page: DefaultPage
};
const ctx = createContext({});
const ComponentsProvider = ({
  components: themeComponents = {},
  children
}) => createElement(ctx.Provider, {
  value: Object.assign({}, defaultComponents, themeComponents)
}, children);
const useComponents = () => {
  return useContext(ctx);
};

const isFn = value => typeof value === 'function';
function flatArrFromObject(arr, prop) {
  const reducer = (arr, obj) => {
    const value = _get(prop)(obj);

    return value ? arr.concat([value]) : arr;
  };

  return Array.from(new Set(arr.reduce(reducer, [])));
}
function compare(a, b, reverse) {
  if (a < b) return reverse ? 1 : -1;
  if (a > b) return reverse ? -1 : 1;
  return 0;
}

function create(initial) {
  var _a;

  const ctx = createContext(initial);
  const listeners = new Set();

  const dispatch = fn => {
    listeners.forEach(listener => listener(fn));
  };

  return {
    context: ctx,
    set: fn => dispatch(fn),
    Provider: (_a = class Provider extends Component {
      constructor() {
        super(...arguments);
        this.state = this.props.initial || initial || {};
      }

      static getDerivedStateFromProps(props, state) {
        if (!equal(props.initial, state)) return props.initial;
        return null;
      }

      componentDidMount() {
        listeners.add(fn => this.setState(fn));
      }

      componentWillUnmount() {
        listeners.clear();
      }

      render() {
        return createElement(ctx.Provider, {
          value: this.state
        }, this.props.children);
      }

    }, _a.displayName = 'DoczStateProvider', _a)
  };
}

const doczState = create({});

const useConfig = () => {
  const state = useContext(doczState.context);
  const {
    linkComponent,
    transform,
    config,
    themeConfig = {}
  } = state;

  const newConfig = _merge(themeConfig, config ? config.themeConfig : {});

  const transformed = transform ? transform(newConfig) : newConfig;
  return Object.assign({}, config, {
    linkComponent,
    themeConfig: transformed
  });
};

const updateState = ev => {
  const {
    type,
    payload
  } = JSON.parse(ev.data);
  const prop = type.startsWith('state.') && type.split('.')[1];

  if (prop) {
    doczState.set(state => Object.assign({}, state, {
      [prop]: payload
    }));
  }
};

const useDataServer = url => {
  useEffect(() => {
    if (!url) return;
    const socket = new WebSocket(url);
    socket.onmessage = updateState;
    return () => socket.close();
  }, []);
};

const useDocs = () => {
  const {
    entries = []
  } = useContext(doczState.context);
  const arr = entries.map(({
    value
  }) => value);
  return sort(arr, (a, b) => compare(a.name, b.name));
};

const noMenu = entry => !entry.menu;

const fromMenu = menu => entry => entry.menu === menu;

const entryAsMenu = entry => ({
  name: entry.name,
  route: entry.route,
  parent: entry.parent
});

const entriesOfMenu = (menu, entries) => entries.filter(fromMenu(menu)).map(entryAsMenu);

const parseMenu = entries => name => ({
  name,
  menu: entriesOfMenu(name, entries)
});

const menusFromEntries = entries => {
  const entriesWithoutMenu = entries.filter(noMenu).map(entryAsMenu);
  const menus = flatArrFromObject(entries, 'menu').map(parseMenu(entries));
  return _unionBy('name', menus, entriesWithoutMenu);
};

const parseItemStr = item => typeof item === 'string' ? {
  name: item
} : item;

const normalize = item => {
  const selected = parseItemStr(item);
  return Object.assign({}, selected, {
    id: selected.id || ulid(),
    parent: _get('parent', selected) || _get('parent', item),
    menu: Array.isArray(selected.menu) ? selected.menu.map(normalize) : selected.menu
  });
};

const clean = item => item.href || item.route ? _omit('menu', item) : item;

const normalizeAndClean = _pipe(normalize, clean);

const mergeMenus = (entriesMenu, configMenu) => {
  const first = entriesMenu.map(normalizeAndClean);
  const second = configMenu.map(normalizeAndClean);

  const merged = _unionBy('name', first, second);

  return merged.map(item => {
    if (!item.menu) return item;
    const found = second.find(i => i.name === item.name);
    const foundMenu = found && found.menu;
    return Object.assign({}, item, {
      menu: foundMenu ? mergeMenus(item.menu, foundMenu) : item.menu || found.menu
    });
  });
};

const UNKNOWN_POS = Infinity;

const findPos = (item, orderedList = []) => {
  const name = typeof item !== 'string' ? _get('name', item) : item;
  const pos = orderedList.findIndex(item => item === name);
  return pos !== -1 ? pos : UNKNOWN_POS;
};

const compareWithMenu = (to = []) => (a, b) => {
  const list = to.map(i => i.name || i);
  return compare(findPos(a, list), findPos(b, list));
};

const sortByName = (a, b) => {
  return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
};

const sortMenus = (first, second = []) => {
  const sorted = sort(first, compareWithMenu(second), sortByName);
  return sorted.map(item => {
    if (!item.menu) return item;
    const found = second.find(menu => menu.name === item.name);
    const foundMenu = found && found.menu;
    return Object.assign({}, item, {
      menu: foundMenu ? sortMenus(item.menu, foundMenu) : sort(item.menu, sortByName)
    });
  });
};

const search = (val, menu) => {
  const items = menu.map(item => [item].concat(item.menu || []));

  const flattened = _flattenDepth(2, items);

  const flattenedDeduplicated = [...new Set(flattened)];
  return match(flattenedDeduplicated, val, {
    keys: ['name']
  });
};

const filterMenus = (items, filter) => {
  if (!filter) return items;
  return items.filter(filter).map(item => {
    if (!item.menu) return item;
    return Object.assign({}, item, {
      menu: item.menu.filter(filter)
    });
  });
};

const useMenus = opts => {
  const {
    query = ''
  } = opts || {};
  const {
    entries,
    config
  } = useContext(doczState.context);
  if (!entries) return null;
  const arr = entries.map(({
    value
  }) => value);
  const entriesMenu = menusFromEntries(arr);
  const sorted = useMemo(() => {
    const merged = mergeMenus(entriesMenu, config.menu);
    const result = sortMenus(merged, config.menu);
    return filterMenus(result, opts && opts.filter);
  }, [entries, config]);
  return query && query.length > 0 ? search(query, sorted) : sorted;
};

const usePrevious = (value, defaultValue) => {
  const ref = useRef(defaultValue);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

const isClient = typeof window === 'object';

const getSize = (initialWidth, initialHeight) => ({
  innerHeight: isClient ? window.innerHeight : initialHeight,
  innerWidth: isClient ? window.innerWidth : initialWidth,
  outerHeight: isClient ? window.outerHeight : initialHeight,
  outerWidth: isClient ? window.outerWidth : initialWidth
});

const useWindowSize = (throttleMs = 300, initialWidth = Infinity, initialHeight = Infinity) => {
  const [windowSize, setWindowSize] = useState(getSize(initialHeight, initialHeight));

  const tSetWindowResize = _throttle(throttleMs, () => setWindowSize(getSize(initialHeight, initialHeight)));

  useEffect(() => {
    window.addEventListener('resize', tSetWindowResize);
    return () => void window.removeEventListener('resize', tSetWindowResize);
  }, []);
  return windowSize;
};

export { isFn as a, useComponents as b, doczState as c, useConfig as d, useDataServer as e, useDocs as f, useMenus as g, usePrevious as h, useWindowSize as i, ComponentsProvider as j };