import get from 'lodash/get';
import omit from 'lodash/omit';
import find from 'lodash/find';
import map from 'lodash/map';
import reduce from 'lodash/reduce';
import keyBy from 'lodash/keyBy';
import isObject from 'lodash/isObject';
import transform from 'lodash/transform';
import template from 'lodash/template';
import checkEmpty from 'lodash/isEmpty';
import filter from 'lodash/filter';
import words from 'lodash/words';
import uniqid from 'lodash/uniqueId';
import mergeWith from 'lodash/mergeWith';
import orderBy from 'lodash/orderBy';
import cloneDeep from 'lodash/cloneDeep';
import isEqual from 'lodash/isEqual';
import unflatten from 'unflatten';

import moment from 'moment';
import { content } from 'config/content';
import { config } from 'config/general';

/**
 * Helper functions
 */
export function camelToUnderscore(str: string) {
  return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}

export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function firstLowerCase(str: string): string {
  return str.charAt(0).toLowerCase() + str.slice(1);
}

export function uppercase(str: string): string {
  return str.toUpperCase();
}

export function isOdd(num) {
  if (num % 2 === 0) {
    return false;
  }
  return true;
}

export function testPrimitive(any: any): boolean {
  if (!any) return true;
  return typeof any === 'string' || typeof any === 'number' || typeof any === 'boolean';
}
/**
 * Util functions
 */
export const getTranslatedContent = (obj: any | string, lg?: string): string => {
  if (typeof obj === 'object') {
    return obj[lg] || (config.defaultLanguage && obj[config.defaultLanguage]) || obj;
  }
  return obj;
};

export const handleReplace = (txt?: string, replaceObj?: any): string => {
  if (!replaceObj || !txt) {
    return txt;
  }

  if (txt && txt.includes('<%=')) {
    txt = template(txt)(replaceObj);
    return txt;
  }

  const keys = replaceObj && Object.keys(replaceObj);
  if (replaceObj && keys && keys.length > 0) {
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      txt = txt.split(`$${key}`).join(replaceObj[key]);
    }
  }

  return txt;
};

/**
 * @deprecated use lodash/camelCase instead
 * @param text string to put in camelCase
 * @return {string} the camelCase text
 */
export const camelize = (text: string): string => {
  return (
    text
      .replace(/(?:^\w|[A-Z]|\b\w)/g, function(word, index) {
        return index === 0 ? word.toLowerCase() : word.toUpperCase();
      })
      // eslint-disable-next-line
      .replace(/[\s\.\-_]+/g, '')
  );
};

export const getContent = (contentObj: any, path: string | string[], lg?: string, replaceObj?: any): string | boolean | any[] => {
  if (!contentObj) {
    return false;
  }

  let txt = get(contentObj, path);

  txt = txt ? getTranslatedContent(txt, lg) : txt;
  txt = typeof txt === 'string' && replaceObj ? handleReplace(txt, replaceObj) : txt;
  return txt;
};

export const getList = (contentObj: any, path: string | string[]): string | any[] | boolean => {
  if (!contentObj) {
    return false;
  }

  let items = get(contentObj, path);

  if (typeof items === 'object') items = Object.values(items);

  return cloneDeep(items);
};

export const getContentImage = (contentObj: any, path: string | string[]): string | any[] | boolean => {
  if (!contentObj) {
    return false;
  }

  let img = get(contentObj, path);

  img = img ? img.src && img.src.large : img;
  return img;
};

export const getInContent = (path: string): string | any[] | boolean => getContent(content, path);

export const getContentList = (
  contentObj: any,
  listPath: string | string[],
  value: string,
  lg?: string,
  replaceObj?: any,
): string | any[] | boolean => {
  if (!contentObj) {
    return false;
  }
  const list = get(contentObj, listPath);

  const item = find(list, (it): boolean => it.label === value);

  if (!item) return false;

  let txt = getTranslatedContent(item, lg);
  txt = typeof txt === 'string' && replaceObj ? handleReplace(txt, replaceObj) : txt;
  return txt;
};

const transformValue = (item: any | string | number | boolean, key?: string): boolean => {
  let itemValue;
  const label = key || 'value';

  if (item && item[label] !== undefined) itemValue = item[label];
  else itemValue = item;

  if (itemValue === undefined || itemValue === null) return undefined;

  // to be sure
  if (testPrimitive(itemValue)) itemValue = itemValue.toString();

  return itemValue;
};

export const compareItems = (item1: any | string | number | boolean, item2: any | string | number | boolean, key?: string): boolean => {
  const val1 = transformValue(item1, key);
  const val2 = transformValue(item2, key);

  if (val1 === val2 && val1 && val2) {
    return true;
  }

  return false;
};

export const compareFieldsList = (list1, list2) => {
  const oldNames = list1?.reduce((current, it) => {
    current.push(it.name);
    return current;
  }, []);
  const newNames = list2?.reduce((current, it) => {
    current.push(it.name);
    return current;
  }, []);

  return isEqual(oldNames, newNames);
};

export const compareSearch = (search1: any, search2: any): boolean => {
  const elem1: any = {
    ...search1,
    page: undefined,
  };
  const elem2: any = {
    ...search2,
    page: undefined,
  };

  return JSON.stringify(elem1) === JSON.stringify(elem2);
};

export const compareSearchObj = (search1: any, search2: any, ignore: string[] = []): boolean => {
  if (!search1 || !search2) return false;

  let obj1 = search1;
  let obj2 = search2;
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);

  if (keys2.length > keys1.length) {
    obj1 = search2;
    obj2 = search1;
    keys1 = Object.keys(obj1);
    keys2 = Object.keys(obj2);
  }

  const keysToIgnore = ignore;

  for (let i = 0; i < keys1.length; i += 1) {
    const key1 = keys1[i];
    const elem1 = obj1[key1];

    let keyMatch = false;

    for (let j = 0; j < keys2.length; j += 1) {
      const key2 = keys2[j];
      const elem2 = obj2[key2];

      if (key1 === key2 && keysToIgnore.indexOf(key1) < 0 && (elem1 || elem2)) {
        keyMatch = true;

        if (!elem1 || !elem2) {
          return false;
        }

        if (typeof elem1 === 'object' || Array.isArray(elem1)) {
          if (!checkEmpty(elem1) || !checkEmpty(elem2)) {
            const isSame = compareSearchObj(elem1, elem2);
            if (!isSame) return false;
          }
        } else if (elem1 !== elem2) {
          return false;
        }
      }
    }
    if (!keyMatch && keysToIgnore.indexOf(key1) < 0 && elem1) {
      if ((typeof elem1 !== 'object' && !Array.isArray(elem1)) || !checkEmpty(elem1)) {
        return false;
      }
    }
  }

  return true;
};

export const isEmpty = (obj: any): boolean => {
  // eslint-disable-next-line no-restricted-syntax
  for (const key in obj) {
    // eslint-disable-next-line no-prototype-builtins
    if (obj.hasOwnProperty(key)) return false;
  }
  return true;
};

export const clone = obj => {
  if (!obj) return false;

  return JSON.parse(JSON.stringify(obj));
};

export const getDuration = (duration, options) => {
  const localeOptions = options || {};
  if (!duration) return '';

  const lastFactor = localeOptions.milli ? 1000 : 1;

  const language = localeOptions.lg || config.defaultLanguage;

  let time = duration;
  const days = Math.floor(duration / (24 * 60 * 60 * lastFactor));
  time -= days * (24 * 60 * 60 * lastFactor);
  const hours = Math.floor(time / (60 * 60 * lastFactor));
  time -= hours * (60 * 60 * 1);
  const minutes = Math.floor(time / (60 * lastFactor));

  let str = ''; // `${hours}h${(minutes < 10 && '0') + minutes}m`;
  if (minutes > 0 && !localeOptions.noMin) {
    moment.relativeTimeThreshold('m', 60);
    str = moment
      .duration(minutes, 'minutes')
      .locale(language)
      .humanize();
  }
  if (hours > 0 && !localeOptions.noHours) {
    const hoursText = moment
      .duration(hours, 'hours')
      .locale(language)
      .humanize();

    str = `${hoursText}${str && ' '}${str}`;
  }
  if (days > 0) {
    const daysText = moment
      .duration(days, 'days')
      .locale(language)
      .humanize();

    str = `${daysText} ${str}`;
  }
  return str;
};

export const formMutators = {
  setValue: ([field, value], state, { changeValue }) => {
    changeValue(state, field, () => value);
  },
};

export const combineSchedules = (schedule1, schedule2) => {
  const newData = clone(schedule1);
  if (!schedule2) return newData;
  const externalKeys = Object.keys(schedule2);

  for (let i = 0; i < externalKeys.length; i++) {
    const externalDayKey = externalKeys[i];
    const externalDay = schedule2[externalDayKey];

    const currentDay = newData[externalDayKey];

    if (currentDay && externalDay) {
      newData[externalDayKey] = [...currentDay, ...externalDay];
    } else if (externalDay) {
      newData[externalDayKey] = [...externalDay];
    }
  }

  return newData;
};

export const getTotalSchedule = schedule => {
  let total = 0;
  if (!schedule) return 0;
  const keys = Object.keys(schedule);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const day = schedule[key];

    for (let j = 0; j < day.length; j++) {
      const slot = day[j];

      const from = moment(slot[0], 'HH:mm');
      const to = moment(slot[1], 'HH:mm');

      total += to.diff(from) / (60 * 60 * 1000);
    }
  }

  return total;
};

export const copyClipboard = text => {
  /* Copy the text inside the text field */
  const dummy = document.createElement('input');
  // to avoid breaking orgain page when copying more words
  // cant copy when adding below this code
  // dummy.style.display = 'none'
  document.body.appendChild(dummy);
  // Be careful if you use texarea. setAttribute('value', value), which works with "input" does not work with "textarea". – Eduard
  dummy.value = text;
  dummy.select();
  document.execCommand('copy');
  document.body.removeChild(dummy);
};

export const groupByValue = list => {
  const grouped = {};

  for (let i = 0; i < list.length; i++) {
    const item = list[i];

    if (!grouped[item]) grouped[item] = 1;
    else grouped[item]++;
  }
  return grouped;
};

export function deepPick(data: any, keysToKeep: string | string[], deep = true) {
  if (!keysToKeep) return data;

  const keysToKeepList = Array.isArray(keysToKeep) ? keysToKeep : [keysToKeep]; //

  const handleKeep = current => {
    return transform(current, (result, value, key) => {
      // transform to a new object
      let found = false;
      for (let i = 0; i < keysToKeepList.length; i++) {
        const keepKey = keysToKeepList[i];

        if (key?.match?.(keepKey)) {
          found = true;
          break;
        }
      }
      if (!found) return;

      result[key] = deep && isObject(value) ? handleKeep(value) : value; // if the key is an object run it through the inner function - omitFromObject
    });
  };

  return handleKeep(data); // return the inner function result
}

export function deepOmit(data: any, keysToOmit: string | string[], deep = true) {
  if (!keysToOmit) return data;

  const keysToOmitList = Array.isArray(keysToOmit) ? keysToOmit : [keysToOmit]; // create an index object of the keys that should be omitted

  const handleOmit = current => {
    // Do not omit on array because array are then transformed into objects
    const treated = Array.isArray(current) ? current : omit(current, keysToOmitList);

    // the inner function which will be called recursively
    return transform(treated, (result, value, key) => {
      // transform to a new object

      for (let i = 0; i < keysToOmitList.length; i++) {
        const omitKey = keysToOmitList[i];

        // regex match (get.* for example)
        if (key?.match?.(omitKey)) {
          return;
        }
      }

      result[key] = deep && isObject(value) ? handleOmit(value) : value; // if the key is an object run it through the inner function - omitFromObject
    });
  };
  return handleOmit(data);
}

// TODO: return result ordered by accumulator
/**
 *
 */
export const filterList = (
  list,
  filterTxt,
  options: {
    lg?: string;
    whitelistedKeys?: string[];
    blackListedKeys?: string[];
    linkedValues?: any;
    noOrder?: boolean;
    orderBy?: string;
    content?: any;
  } = {},
) => {
  // avoid modifying the state
  list = cloneDeep(list);

  const contentObj = options?.content;

  const { lg, linkedValues, whitelistedKeys, blackListedKeys } = options;

  const filteredList = filter(list, item => {
    const accumulator = {
      // total match
      match: 0,
    };

    if (!filterTxt) return true;

    const filterValue = filterTxt.toLowerCase();
    const linkedKeys = linkedValues && Object.keys(linkedValues);

    const checkDeep = (obj, acc, arrayKey?: string) => {
      if (!obj) return 0;

      const isArray = Array.isArray(obj);

      let objectMatch = 0;

      const keys = Object.keys(obj);
      for (let i = 0; i < keys.length; i += 1) {
        const key = keys[i];
        const value = obj[key];

        // reset locale match for each element
        let localeMatch = 0;

        // keys that are linked (for arrays => use the array key and not the index key)
        if (linkedKeys?.length && linkedKeys?.indexOf(arrayKey || key) >= 0) {
          const linkedValue = linkedValues[arrayKey || key];
          // linked value can be a path, a
          if (linkedValue) {
            let contentValue;

            if (typeof linkedValue === 'string') {
              contentValue = getContentList(contentObj, linkedValue, value, lg);
            } else if (typeof linkedValue === 'function') {
              contentValue = linkedValue(value);
            }
            contentValue = contentValue?.toLowerCase && contentValue?.toLowerCase();

            if (contentValue && contentValue.indexOf(filterValue) >= 0) {
              acc.match++;
              localeMatch++;
            }
          }
        }

        if (whitelistedKeys?.length && whitelistedKeys.indexOf(key) < 0) continue;

        if (blackListedKeys?.length && blackListedKeys.indexOf(key) >= 0) continue;

        if (value && typeof value === 'string' && value.toLowerCase().indexOf(filterValue) >= 0) {
          acc.match++;
          localeMatch++;
        } else if (typeof value === 'object') {
          const valueIsArray = Array.isArray(value);
          localeMatch += checkDeep(value, acc, valueIsArray ? key : undefined);

          // copy of current locale match for the object
          if (!valueIsArray && value) {
            value.__match = localeMatch;
          }
        }
        // cumulative match
        objectMatch += localeMatch;
      }
      // copy of the object match for the object
      if (!isArray) obj.__match = objectMatch;

      return objectMatch;
    };

    checkDeep(item, accumulator);

    item.__match = accumulator.match;

    return accumulator.match > 0;
  });

  if (options.noOrder) return filteredList;

  return orderBy(filteredList, orderBy || '__match');
};

export const mobileAndTabletCheck = () => {
  let check = false;
  (function(a) {
    if (
      /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
        a,
      ) ||
      /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
        a.substr(0, 4),
      )
    )
      check = true;
  })(navigator.userAgent || navigator.vendor || window.opera);

  return check;
};

export const convertObjectToArray = obj => {
  const arr = [];
  const keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i += 1) {
    const key = keys[i];
    const elem = obj[key];
    if (elem) arr.push(key);
  }
  return arr;
};

export const convertArrayToObject = arr => {
  const obj = {};
  for (let i = 0; i < arr.length; i += 1) {
    const id = arr[i];

    if (id) {
      obj[id] = true;
    }
  }
  return obj;
};

export function detectLinkInText(textToCheck) {
  // eslint-disable-next-line
  const expression = /(https?:\/\/)?[\w\-~]+(\.[\w\-~]+)+(\/[\w\-~@:%]*)*(#[\w\-]*)?(\?[^\s]*)?/gi;

  const regex = new RegExp(expression);
  let match;
  const splitText = [];
  let startIndex = 0;

  let continueLoop = (match = regex.exec(textToCheck)) != null;
  while (continueLoop) {
    splitText.push({
      text: textToCheck.substr(startIndex, match.index - startIndex),
      type: 'text',
    });

    const cleanedLink = textToCheck.substr(match.index, match[0].length);
    splitText.push({ text: cleanedLink, type: 'link' });

    startIndex = match.index + match[0].length;

    continueLoop = (match = regex.exec(textToCheck)) != null;
  }

  if (startIndex < textToCheck.length) splitText.push({ text: textToCheck.substr(startIndex), type: 'text' });

  return splitText;
}

export const flattenObj = (ob: any) => {
  const toReturn = {};

  for (let index = 0; index < Object.keys(ob).length; index++) {
    const i = Object.keys(ob)[index];
    if (!Object.prototype.hasOwnProperty.call(ob, i)) continue;

    const isDate = ob[i] instanceof Date;

    if (typeof ob[i] === 'object' && ob[i] !== null && !isDate) {
      const flatObject = flattenObj(ob[i]);
      for (let y = 0; y < Object.keys(flatObject).length; y++) {
        const el = Object.keys(flatObject)[y];
        if (!Object.prototype.hasOwnProperty.call(flatObject, el)) continue;

        toReturn[`${i}.${el}`] = flatObject[el];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }

  return toReturn;
};

export function cleanObj(obj) {
  const copy = flattenObj({ ...obj });
  for (let index = 0; index < Object.keys(copy).length; index++) {
    const element = Object.keys(copy)[index];
    if (copy[element] === null || copy[element] === undefined) {
      delete copy[element];
    }
  }
  return unflatten(copy);
}

export const handleVarInString = (object: Record<string, any>, text: string, otherOptions?: Record<string, any>) => {
  let result = text;

  if (typeof text === 'string' && text.includes('{')) {
    const splittedTitle = words(text, /\{(.*?)\}/g || otherOptions?.regex);

    splittedTitle.forEach(element => {
      const variable = element
        .replace('{', '')
        .replace('}', '')
        .split(':');

      // handle list
      let val = get(object, variable[0]) || get(object, variable[1]) || uniqid();

      if (otherOptions && otherOptions.lg && otherOptions.content) {
        const lg = otherOptions.lg;
        const optionContent = otherOptions.content;
        if (variable.length > 1 && content && lg) {
          val =
            getContent(optionContent, `${variable[1]}.${val}`, lg) ||
            getContent(optionContent, `${variable[0]}.${val}`, lg) ||
            getContentList(optionContent, variable[1], val, lg) ||
            getContentList(optionContent, variable[0], val, lg) ||
            val;
        }
      }
      if (val && result) {
        result = result.replace(element, val);
      }
    });
  }

  return result;
};

export const getTextFromObject = (item: any | string, modelValues: any, contentObj: any, lg: string) => {
  if (!item) {
    return 'NO MATCH';
  }

  // text should not be treated
  if (typeof item === 'string') return item;

  // default behavior
  if (!modelValues || checkEmpty(modelValues)) {
    return item[lg] || item.text || item.description || item.label;
  }

  let text;
  if (typeof modelValues.text === 'function') {
    text = modelValues.text(item);
  }
  // case with variables in text
  else if (typeof modelValues.text === 'string' && modelValues.text.includes('{')) {
    text = handleVarInString(item, modelValues.text);
  }
  // case with object text
  else if (typeof modelValues.text === 'object' && modelValues.text.value) {
    text = get(item, modelValues.text.value);
  } else {
    text = get(item, modelValues.text || modelValues.value);
  }

  // Handle multilanguage
  text = (text && text[lg]) || text;

  // Addition of complementary info
  if (modelValues.complementary) {
    let complementary;

    // allows function for special treatment
    if (typeof modelValues.complementary === 'function') {
      complementary = modelValues.complementary(item);
    } else complementary = get(item, modelValues.complementary);

    if (modelValues.complementaryList) {
      complementary = getContent(contentObj, `${modelValues.complementaryList}.${complementary}`, lg);
    }

    // Handle multilanguage
    complementary = (complementary && complementary[lg]) || complementary;

    // Allows a custom format for the complementary information
    if (complementary && !modelValues.complementaryFormat) text += ` - ${complementary}`;
    if (complementary && modelValues.complementaryFormat) text = modelValues.complementaryFormat(text, complementary);
  }

  return text;
};

export const getValueFromObject = (item: any | string, modelValues?: any, valueKey?: string): string | boolean => {
  if (!item) {
    return false;
  }

  // text should not be treated
  if (typeof item === 'string') return item;

  if (!modelValues || checkEmpty(modelValues)) {
    return (valueKey && item[valueKey]) || item[config.idKey] || item.value || item.label;
  }

  if (typeof modelValues.value === 'function') {
    return modelValues.value(item);
  }

  return get(item, modelValues.value);
};

export const formatAddress = address => {
  return `${address.street} ${address.number}, ${address.zip} ${address.city} ${address.country}`;
};

export const checkCondition = (item, values, base = '', customCheck) => {
  const displayIf = item.displayIf;
  const hideIf = item.hideIf;
  const customCondition = item[customCheck];

  if (customCheck && !customCondition) return false;

  const conditions = customCondition || displayIf || hideIf;
  const conditionType = displayIf || customCondition ? 'display' : 'hide';

  if (!conditions) return true;

  const checkValue = (key, value?: any) => {
    // 1. check current level
    let check = get(values, `${base}.${key}`);
    // 2. check root level
    if (!check) check = get(values, key);

    // console.log('check', key, check, value, conditions);
    if (conditionType === 'display' && (check === undefined || check === null || (value && check?.toString() !== value?.toString()))) {
      return false;
    }
    if (conditionType === 'hide' && check && check.toString() === value?.toString()) {
      return false;
    }

    return true;
  };

  const handleAndCheck = condition => {
    const keys = Object.keys(condition);

    // if display => we start by assuming we will display and hide if hide
    let cumulativeCheck = conditionType === 'display';

    for (let j = 0; j < keys.length; j++) {
      const key = keys[j];
      let check = checkValue(key, condition[key]);

      if (condition[key] === '*') {
        check = checkValue(key);
      }

      // if one display is false => hide
      if (!check && conditionType === 'display') {
        cumulativeCheck = false;
      }
      // if one hide is ok => display
      if (check && conditionType === 'hide') {
        cumulativeCheck = true;
      }
    }
    return cumulativeCheck;
  };

  // Case one : ['key','value']
  if (Array.isArray(conditions) && conditions?.length === 2 && typeof conditions[0] === 'string') {
    if (!checkValue(conditions[0], conditions[1])) return false;
  }
  // Case three : [{ key1: value1 }, { key2: value2, key3: value3 }] => OR with AND
  else if (Array.isArray(conditions)) {
    let oneGoodCondition = false;
    for (let i = 0; i < conditions.length; i++) {
      const condition = conditions[i];

      oneGoodCondition = handleAndCheck(condition);

      if (oneGoodCondition) break;
    }

    if (!oneGoodCondition) return false;
  }
  // Case two : { key1: value2, key2: value2, .... } => ONE OF
  else if (typeof conditions === 'object') {
    return handleAndCheck(conditions);
  }
  // Case just string
  else if (typeof conditions === 'string') {
    return checkValue(conditions);
  }

  return true;
};

export const customMerge = (srcObj: any, newObj: any) => {
  // if newObj is null => return null
  if (newObj === null) return null;

  srcObj = cloneDeep(srcObj);

  // The merge process is as follow :
  // 1. target should add and override all the origin properties
  // 2. Array properties in target should define array length etc...
  const customize = (origin, target) => {
    if (Array.isArray(target)) {
      // handle array
      // console.log('obj', objValue, srcValue);

      const mergedArray = [];

      for (let i = 0; i < target.length; i += 1) {
        const targetItem = target[i];
        const originItem = origin?.[i];

        if (originItem && typeof originItem === 'object') {
          const merged = mergeWith(originItem, targetItem, customize);
          mergedArray.push(merged);
        } else {
          mergedArray.push(targetItem);
        }
      }
      return mergedArray;
    }
    return undefined;
  };

  return mergeWith(srcObj, newObj, customize);
};

export const deepDiff = (a, b) => {
  const finalResult = {
    different: [],
    missingFromFirst: [],
    missingFromSecond: [],
  };

  reduce(
    a,
    function(result, value, key) {
      if (Object.prototype.hasOwnProperty.call(b, key)) {
        if (isEqual(value, b[key])) {
          return result;
        }
        if (typeof a[key] !== typeof {} || typeof b[key] !== typeof {}) {
          // dead end.
          result.different.push(key);
          return result;
        }
        const deeper = deepDiff(a[key], b[key]);
        result.different = result.different.concat(
          map(deeper.different, subKey => {
            return `${key}.${subKey}`;
          }),
        );

        result.missingFromSecond = result.missingFromSecond.concat(
          map(deeper.missingFromSecond, subKey => {
            return `${key}.${subKey}`;
          }),
        );

        result.missingFromFirst = result.missingFromFirst.concat(
          map(deeper.missingFromFirst, subKey => {
            return `${key}.${subKey}`;
          }),
        );
        return result;
      }
      result.missingFromSecond.push(key);
      return result;
    },
    finalResult,
  );

  reduce(
    b,
    function(result, value, key) {
      if (Object.prototype.hasOwnProperty.call(a, key)) {
        return result;
      }
      result.missingFromFirst.push(key);
      return result;
    },
    finalResult,
  );

  return finalResult;
};

/**
 * Apply a function to each non object item for a nested object
 * @param input any input to transform
 * @param transformFunc a function to apply to each non object value
 * @returns cloned transformed input
 */
export const transformDeep = (input, transformFunc) => {
  const output = cloneDeep(input);

  const transformRecursive = src => {
    if (src === null) return null;

    // object or array
    if (typeof src === 'object') {
      Object.keys(src).forEach(key => {
        src[key] = transformRecursive(src[key]);
      });
    }
    return transformFunc(src);
  };

  return transformRecursive(output);
};

export const toMapped = <T = any>(array: T[], key: string | ((val: T, index: number) => string)) =>
  array.reduce((acc, cur, index) => {
    const _key = typeof key === 'function' ? key(cur, index) : key;
    const accKey = get(cur, _key);

    if (accKey in acc) {
      console.warn(`Duplicate key ${accKey}`);
    }

    acc[accKey] = cur;

    return acc;
  }, {});
