# Source: compoundPath.js

    /**
     *  Accessible via `require('@buggyorg/graphtools').CompoundPath`
     *
     * A compound path is a unique representation of a node in the graph. It is defined as an array of
     * parent nodes starting at the root level, e.g. `['A', 'B', 'C']` points to the node `C` whose parent
     * is `B` and the parent of `B` is `A`. All methods accept the array notation or the shorthand string
     * notation. The shorthand string notation starts with a `»` (ALT-GR+Y) and separates each node with
     * a `»`, e.g. `»A»B»C` describes the exact same path as above. For elements on the root level it is
     * okay to omit the `»`, i.e. `»A` is the same as `A`.
     * @module CompoundPath */

    import curry from 'lodash/fp/curry'
    import _ from 'lodash'

    /**
     * Converts a compound path into its string representation. The seperate parts are divided by a '»'.
     * @param {String[]} compoundPathArr An array of node IDs reperesenting the compound path.
     * @returns {String} The string representation of the compound path.
     */
    export function toString (compoundPathArr) {
      if (compoundPathArr.length === 1) return compoundPathArr[0]
      return compoundPathArr.reduce((acc, n) => acc + '»' + n, '')
    }

    /**
     * Converts a compound path string into its array representation. The seperate parts must be divided by a '»'.
     * @param {String} compoundPathStr A string reperesenting the compound path divded by '»'.
     * @returns {String[]} An array of node IDs representing the compound path.
     */
    export function fromString (compoundPathStr) {
      if (compoundPathStr.indexOf('»') === -1) return [compoundPathStr]
      return compoundPathStr.split('»').slice(1)
    }

    /**
     * Returns whether a string represents a compound path or not.
     * @param {string} path The path string to test.
     * @returns {boolean} True if the path represents a compound path, false otherwise.
     */
    export function isCompoundPath (path) {
      return Array.isArray(path) || (typeof (path) === 'string' && path[0] === '»')
    }

    /**
     * Convert a path representation into its normalized array form.
     * @param {string|string[]} path The path as a string or array.
     * @returns {CompoundPath} The normalized path.
     */
    export function normalize (path) {
      if (Array.isArray(path)) {
        return _.compact(path)
      } else {
        return _.compact(fromString(path))
      }
    }

    /**
     * Joins two paths into one.
     * @param {CompoundPath} base The prefix of the new path
     * @param {CompoundPath} rest The postfix of the new path.
     * @returns {CompoundPath} The new path in the form `<base>»<rest>`.
     */
    export function join (base, rest) {
      if (!base) return rest
      return _.concat(normalize(base), normalize(rest))
    }

    /**
     * Returns whether a path points to the root element or not.
     * @param {CompoundPath} path The path to check
     * @returns {boolean} True if the path points to the root element ('', '»' or []), false otherwise.
     */
    export function isRoot (path) {
      if (typeof (path) === 'string') {
        if (path === '') return true
        path = fromString(path)
      }
      return path.length === 0
    }

    /**
     * Returns the parent of a compound path.
     * @param {CompoundPath|string} path The path either as a string or an array.
     * @returns {CompoundPath|string} The parent of the path in the same format as the input.
     * @throws {Error} If the input format is invalid.
     */
    export function parent (path) {
      if (typeof (path) === 'string') {
        return toString(parent(fromString(path)))
      } else if (Array.isArray(path)) {
        return path.slice(0, -1)
      } else {
        throw new Error('Malformed compound path. It must either be a string or an array of node IDs. Compounds paths was: ' + JSON.stringify(path))
      }
    }

    /**
     * Returns the root node in the path, i.e. the first node indicated by the path.
     * @params {CompoundPath} path The path
     * @returns {CompoundPath} The id of the base/root element in the path.
     */
    export function base (path) {
      if (typeof (path) === 'string') {
        return toString(base(fromString(path)))
      } else if (Array.isArray(path)) {
        if (path.length === 1) {
          return []
        } else {
          return path.slice(0, 1)
        }
      } else {
        throw new Error('Malformed compound path. It must either be a string or an array of node IDs. Compounds paths was: ' + JSON.stringify(path))
      }
    }

    /**
     * Returns the node element, i.e. the last element in the path.
     * @param {CompoundPath} path The path
     * @returns {String} The id of the the element at the end of the path chain.
     */
    export function node (path) {
      if (typeof (path) === 'string') {
        return node(fromString(path))
      } else if (Array.isArray(path)) {
        return path.slice(-1)
      } else {
        throw new Error('Malformed compound path. It must either be a string or an array of node IDs. Compounds paths was: ' + JSON.stringify(path))
      }
    }

    /**
     * Returns a new path that omits the root component.
     * @param {CompoundPath} path The path
     * @returns {CompoundPath} A path that omits the root component. E.g. rest([a, b, c]) -> [b, c].
     */
    export function rest (path) {
      if (typeof (path) === 'string') {
        return rest(fromString(path))
      } else if (Array.isArray(path)) {
        return path.slice(1)
      } else {
        throw new Error('Malformed compound path. It must either be a string or an array of node IDs. Compounds paths was: ' + JSON.stringify(path))
      }
    }

    /**
     * @function
     * @name relativeTo
     * @description Creates a new path that shortens path1 assuming that path2 is a prefix to path1.
     * @param {CompoundPath} path1 The path to shorten
     * @param {CompoundPath} path2 The path that is a prefix of path1 and by what path1 is shortend.
     * @returns {CompoundPath} A new shortend path that has the prefix path2 removed.
     * @throws {Error} Of path2 is no prefix of path2.
     */
    export const relativeTo = curry((path1, path2) => {
      if (path2.length > path1.length) {
        throw new Error('Cannot calculate relative path to a longer path. Tried to get express: ' + path1 + ' relative to: ' + path2)
      }
      if (path2.length === 0) {
        return path1
      } else if (path1[0] !== path2[0]) {
        throw new Error('Pathes are are not subsets and thus the relative path cannot be calculated.')
      } else {
        return relativeTo(rest(path1), rest(path2))
      }
    })

    const isPrefix = (path1, path2, idx = 0) => {
      if (path2.length <= idx) return true
      if (path1[idx] === path2[idx]) {
        return isPrefix(path1, path2, idx + 1)
      } else return false
    }

    /**
     * @function
     * @name prefix
     * @description Prefixes path1 by path2. Does not duplicate entries in path1 if they are already part of path1.
     * Every entry in the path is a unique id and if path1 already has a subset of path2 this subset is not prefixed too.
     * @example
     * // Compound paths contain IDs which are usually not that short. Only for brevity.
     * prefix([d, e], [a, b]) => [a, b, d, e]
     * prefix([b, c], [a, b]) => [a, b, c]
     * prefix([a, b, c], [a, b]) => [a, b, c]
     * prefix([a, c], [a, b]) => Throws exception as path2 cannot be a prefix!
     * @param {CompoundPath} path1 The path that gets its prefix.
     * @param {CompoundPath} path2 The prefix path.
     * @returns {CompoundPath} A new path that prefixed path2 onto path1
     */
    export const prefix = curry((path1, path2) => {
      if (path2.length === 0) return path1
      if (path2[0] !== path1[0]) {
        return [path2[0]].concat(prefix(path1, rest(path2)))
      } else if (isPrefix(path1, path2)) {
        return path2
      } else {
        throw new Error('Unable to apply inconsistent prefix: ' + JSON.stringify(path1) + ' , ' + JSON.stringify(path2))
      }
    })

    /**
     * @function
     * @name equal
     * @description Returns whether two compound paths are equal
     * @param {CompoundPath} path1 The first path to compare.
     * @param {CompoundPath} path2 The second path to compare.
     * @returns {boolean} True if the paths are the same, false otherwise.
     */
    export const equal = curry((path1, path2) => {
      return _.isEqual(path1, path2)
    })

    /**
     * Returns if the pathes have the same parent node.
     * @param {CompoundPath} path1 One of the paths.
     * @param {CompoundPath} path2 The other path.
     * @returns {boolean} True if path1 and path2 have the same parents, false otherwise.
     */
    export function sameParents (path1, path2) {
      return path1 && path2 && path1.length > 0 && path2.length > 0 && equal(parent(path1), parent(path2))
    }