export interface ComposedRouteState {
  map: Array<(string | Record<string, string>[])[]> // [ composition, propertyObjects, ...slots]
  anchor: string | null
}

// Parses a URL into a structured state object
export const compose = (url: string): ComposedRouteState => {
  const [resourcePart, hashStateCommands, anchor] = url.split("#", 3)
  const tmpURL = new URL(
    compose.useHash ? resourcePart : url,
    "http://localhost",
  )
  const pathStateCommands = tmpURL.pathname
  const realAnchor = compose.useHash ? anchor : tmpURL.hash.slice(1)

  const state: ComposedRouteState = {
    map: [],
    anchor: realAnchor || null,
  }
  const propertyCache = {} as Record<string, Record<string, string>[]>

  const process = (s: string) => {
    const cmds = s.split("/").filter((e) => e)
    let currentComposition = ""

    for (const cmd of cmds) {
      // get the syntactic meaningful characters
      const eq_i = cmd.indexOf("=")
      const target = cmd.substr(0, eq_i <= 0 ? cmd.length : eq_i)
      const cmp_i = target.lastIndexOf(":")
      const prp_i = target.lastIndexOf(".")
      const propName = target.substr(Math.max(cmp_i, prp_i) + 1)
      const propValue = decodeURIComponent(
        cmd.substr(eq_i >= 0 ? eq_i + 1 : cmd.length),
      )
      const obj =
        eq_i > 0 && target === propName
          ? currentComposition
          : target.substr(0, prp_i >= 0 ? prp_i : target.length)

      if (eq_i > 0 && cmp_i < eq_i && cmp_i >= 0 && prp_i < 0) {
        continue
      }

      if (eq_i < 0 && obj) {
        // if you don't assign a property value, you are setting a composition and remember it
        propertyCache[obj] = (propertyCache[obj] || []).concat({}) // always add a new property Object when a new composition is set
        currentComposition = obj
        continue
      }

      if (eq_i > 0) {
        // you alway add properties to the last property Object in the composition, if it already exist in the cache
        // the composition can decide for itself whether to merge all property objects, or whether to use them for different gui elements
        const compositionProps = propertyCache[obj || currentComposition] || [
          {},
        ]

        compositionProps.at(-1)[propName] = propValue
        propertyCache[obj || currentComposition] = compositionProps
      }
    }
  }

  if (compose.usePath && pathStateCommands) {
    process(pathStateCommands)
  }
  if (compose.useHash && hashStateCommands) {
    process(hashStateCommands)
  }

  for (const [key, propObjects] of Object.entries(propertyCache)) {
    const slots_composition = key.split(":")
    const composition = slots_composition.at(-1)
    const slots = slots_composition.slice(0, -1)

    if (!composition && slots.length) {
      continue
    }
    const filtered = propObjects.filter((o) => Object.keys(o).length)

    if (compose.filterNopStates && !filtered.length && !slots.length) {
      continue
    }
    const map = [
      composition, // composition or widget name
      filtered.length == 0 ? filtered : propObjects, // property objects, if no properties are set anywhere, empty the list
      ...slots, // slot realized path
    ]

    state.map.push(map)
  }

  return state
}
compose.usePath = true
compose.useHash = false
compose.filterNopStates = true

export const urlCompose = (state: ComposedRouteState): string => {
  const cmds = []

  for (const [composition, properties, ...slots] of state.map) {
    const path = [...slots, composition].join(":")

    if (slots.length && !properties.length) {
      cmds.push(path)
    } else {
      for (const propObject of properties) {
        cmds.push(path)
        for (const [prop, value] of Object.entries(propObject)) {
          cmds.push(prop + "=" + encodeURIComponent(value))
        }
      }
    }
  }

  return cmds.join("/") + (state.anchor ? "#" + state.anchor : "")
}
