

function withParentProps(item, parent, childrenKey, prop, currentValue) {
	if (!Array.isArray(parent)) {
		return;
	}

	let currentPropValue = currentValue || parent[prop];

	for (const entry of parent) {
		if (entry[prop]) {
			currentPropValue = entry[prop];
		}

		if (entry === item) {
			return [item, currentPropValue];
		}

		if (entry[childrenKey]) {
			const res = withParentProps(item, entry[childrenKey], childrenKey, prop, currentPropValue);
            if (res[1]) {
              return res;
            }
		}
	}
	return [];
}

function dms (lat, lon) {
  const λh = lat < 0 ? "S" : "N";
  const φh = lon < 0 ? "W" : "E";

  const λn = Math.abs(lat);
  const φn = Math.abs(lon);

  const λi = Math.trunc(λn);
  const φi = Math.trunc(φn);

  const λf = λn - λi;
  const φf = φn - φi;

  const λs = ((λf*3600)%60).toFixed(1);
  const φs = ((φf*3600)%60).toFixed(1);

  const λm = Math.trunc(λf*60);
  const φm = Math.trunc(φf*60);

  const λ =
    String(λi).padStart(2, "0") + "°" +
    String(λm).padStart(2, "0") + "'" +
    String(λs).padStart(4, "0") + '" ' +
    λh;

  const φ =
    String(φi).padStart(3, "0") + "°" +
    String(φm).padStart(2, "0") + "'" +
    String(φs).padStart(4, "0") + '" ' +
    φh;

  return λ+" "+φ;
}

function geometryAsString (item, opts = {}) {
  const key = "key" in opts ? opts.key : "geometry";
  const formatDMS = opts.dms;

  let str = "";

  if (key in item) {
    const geometry = item[key];
    if (geometry && "coordinates" in geometry) {
      if (geometry.type == "Point") {
        if (formatDMS) {
          str = dms(geometry.coordinates[1], geometry.coordinates[0]);
        } else {
          str = `${geometry.coordinates[1].toFixed(6)}, ${geometry.coordinates[0].toFixed(6)}`;
        }
      }

      if (str) {
        if (opts.url) {
          if (typeof opts.url === 'string') {
            str = `[${str}](${opts.url.replace("$x", geometry.coordinates[0]).replace("$y", geometry.coordinates[1])})`;
          } else {
             str = `[${str}](geo:${geometry.coordinates[0]},${geometry.coordinates[1]})`;
          }
        }
      }
    }
  }

  return str;
}

/** Extract preferences by prefix.
 *
 * This function returns a lambda which, given
 * a key or a prefix, extracts the relevant
 * preferences from the designated preferences
 * store.
 *
 * For instance, assume preferences = {
 *   "a.b.c.d": 1,
 *   "a.b.e.f": 2,
 *   "g.h": 3
 * }
 *
 * And λ = preferencesλ(preferences). Then:
 *
 * λ("a.b") → { "a.b.c.d": 1, "a.b.e.f": 2 }
 * λ("a.b.e.f") → { "a.b.e.f": 2 }
 * λ("g.x", {"g.x.": 99}) → { "g.x.": 99 }
 * λ("a.c", {"g.x.": 99}) → { "g.x.": 99 }
 *
 * Note from the last  two examples that a default value
 * may be provided and will be returned if a key does
 * not exist or is not searched for.
 */
function preferencesλ (preferences) {

  return function (key, defaults={}) {
    const keys = Object.keys(preferences).filter(str => str.startsWith(key+".") || str == key);

    const settings = {...defaults};
    for (const str of keys) {
      const k = str == key ? str : str.substring(key.length+1);
      const v = preferences[str];
      settings[k] = v;
    }

    return settings;
  }

}

/** Compare two possibly complex values for
 * loose equality, going as deep as required in the
 * case of complex objects.
 */
function deepCompare (a, b) {
  if (typeof a == "object" && typeof b == "object") {
    return !Object.entries(a).some( ([k, v]) => !deepCompare(v, b[k])) &&
      !Object.entries(b).some( ([k, v]) => !deepCompare(v, a[k]));
  } else {
    return a == b;
  }
}

/** Compare two possibly complex values for
 * strict equality.
 */
function deepEqual (a, b) {
  if (typeof a === "object" && typeof b === "object") {
    return !Object.entries(a).some( ([k, v]) => !deepEqual(v, b[k])) &&
      !Object.entries(b).some( ([k, v]) => !deepEqual(v, a[k]));
  } else {
    return a === b;
  }
}

/** Traverses an object and sets a nested value.
 *
 * Example:
 *
 * const obj = {a: {b: {c: "X"} } }
 * deepSet(obj, ["a", "b", "c"], "d")
 * → {a: {b: {c: "d"} } }
 *
 * This would be the equivalent of:
 *
 * obj?.a?.b?.c = "d";
 *
 * Except that the above is not a legal expression.
 *
 * If a non-leaf property does not exist, this function
 * creates it as an empty object ({}) and keeps traversing.
 *
 * The last member of `path` may be `null`, in which case,
 * if the object pointed to by the next to last member is
 * an array, an insert operation will take place.
 *
 */
function deepSet (obj, path, value) {
  const key = path.shift();
  if (!path.length) {
    if (key === null && Array.isArray(obj)) {
      obj.push(value);
    } else {
      obj[key] = value;
    }
  } else {
    if (!Object.hasOwn(obj, key)) {
      obj[key] = {};
    }
    deepSet(obj[key], path, value);
  }
}

/** Returns a nested property.
 *
 * Example:
 *
 * const obj = {a: {b: {c: "d"} } }
 * deepSet(obj, ["a", "b", "c"])
 * → "d"
 *
 * If `path` is known in advance, this is effectively
 * the same as:
 *
 * obj?.a?.b?.c
 *
 * This might be useful when `path` is dynamic.
 */
function deepValue (obj, path) {
  if (obj !== undefined) {
    const key = path.shift();
    if (!path.length) {
      if (key === undefined) {
        return obj;
      } else {
        return obj[key];
      }
    } else {
      return deepValue(obj[key], path);
    }
  }
}

// Just to have all the deep*()s in one place
import deepMerge from './deepMerge'

export {
  withParentProps,
  geometryAsString,
  preferencesλ,
  deepMerge,
  deepCompare,
  deepEqual,
  deepSet,
  deepValue
}
