export function merge() {
  return mutate({}, ...arguments);
}

export function clone(target) {
  if (Array.isArray(target)) {
    return target.map((element) => clone(element));
  } else if (target && typeof target === "object") {
    return mutate({}, target);
  } else {
    return target;
  }
}

export function mutate() {
  let target = arguments[0];
  for (let i = 1; i < arguments.length; i++) {
    const source = arguments[i];
    if (is_object_literal(source)) {
      if (!is_object_literal(target)) {
        target = {};
      }
      for (const name of Object.keys(source)) {
        if (/__proto__|prototype/.test(name)) {
          // See
          // https://github.com/adaltas/node-mixme/issues/1
          // https://github.com/adaltas/node-mixme/issues/2
          // continue if /__proto__|constructor|prototype|eval|function|\*|\+|;|\s|\(|\)|!/.test name
          // Unless proven wrong, I consider ok to copy any properties named eval
          // or function, we are not executing those, only copying.
          continue;
        }
        target[name] = mutate(target[name], source[name]);
      }
    } else if (Array.isArray(source)) {
      target = source.map((element) => clone(element));
    } else if (source !== undefined) {
      target = source;
    }
  }
  return target;
}

export function snake_case(source, convert = true) {
  let target = {};
  if (is_object_literal(source)) {
    const u =
      typeof convert === "number" && convert > 0 ? convert - 1 : convert;
    for (let name of Object.keys(source)) {
      let src = source[name];
      if (convert) {
        name = snake_case_str(name);
      }
      target[name] = snake_case(src, u);
    }
  } else {
    target = source;
  }
  return target;
}

export function compare(el1, el2) {
  if (is_object_literal(el1)) {
    if (!is_object_literal(el2)) {
      return false;
    }
    const keys1 = Object.keys(el1).sort();
    const keys2 = Object.keys(el2).sort();
    if (keys1.length !== keys2.length) {
      return false;
    }
    for (let i = 0; i < keys1.length; i++) {
      const key = keys1[i];
      if (key !== keys2[i]) {
        return false;
      }
      if (!compare(el1[key], el2[key])) {
        return false;
      }
    }
  } else if (Array.isArray(el1)) {
    if (!Array.isArray(el2)) {
      return false;
    }
    if (el1.length !== el2.length) {
      return false;
    }
    for (let i = 0; i < el1.length; i++) {
      if (!compare(el1[i], el2[i])) {
        return false;
      }
    }
  } else if (el1 !== el2) {
    return false;
  }
  return true;
}

export function snake_case_str(str) {
  return str
    .replace(/([a-z\d])([A-Z]+)/g, "$1_$2")
    .replace(/[-\s]+/g, "_")
    .toLowerCase();
}

export function is_object(obj) {
  return obj && typeof obj === "object" && !Array.isArray(obj);
}

export function is_object_literal(obj) {
  let test = obj;
  if (typeof obj !== "object" || obj === null) {
    return false;
  } else {
    if (Object.getPrototypeOf(test) === null) {
      return true;
    }
    while (
      Object.getPrototypeOf((test = Object.getPrototypeOf(test))) !== null
    ) {
      true;
    }
    return Object.getPrototypeOf(obj) === test;
  }
}
