/**
 * A mapping of multi-part addresses (using the Java naming convention) to any
 * type. Used extensively by the RPC channel.
 * @author Nathan Pennie <kb1rd@kb1rd.net>
 */
/** */

import { isUndef } from './utils'

type MultistringAddress = string[]
type WildcardMultistringAddress = (string | undefined | null)[]

const DefaultEntryKey = Symbol('DefaultEntry')
const WildcardEntryKey = Symbol('WildcardEntry')

type AddressMapFlat<T> = {
  [key: string]: AddressMapFlat<T>
  [WildcardEntryKey]?: AddressMapFlat<T>
  [DefaultEntryKey]?: T
}

function getFromAddrMapFlat<T>(
  map: AddressMapFlat<T>,
  addr: MultistringAddress,
  wc_values?: string[]
): T | undefined {
  const part = addr.shift()

  if (!part) {
    return map[DefaultEntryKey]
  }

  let data: T | undefined = undefined
  if (map[part]) {
    data = getFromAddrMapFlat(map[part], addr, wc_values)
  }
  if (!data && map[WildcardEntryKey]) {
    if (wc_values) {
      wc_values.push(part)
    }
    data = getFromAddrMapFlat(
      map[WildcardEntryKey] as AddressMapFlat<T>,
      addr,
      wc_values
    )
  }

  addr.unshift(part)
  return data
}

class AddressMap<T> {
  table: AddressMapFlat<T> = {}
  put(addr: WildcardMultistringAddress, value: T | undefined): void {
    let last_table = this.table
    addr.forEach((part) => {
      const key = part || WildcardEntryKey
      if (isUndef(last_table[key])) {
        last_table[key] = {}
      }
      last_table = last_table[key] as AddressMapFlat<T>
    })
    // TODO: Delete parents if empty
    if (isUndef(value)) {
      delete last_table[DefaultEntryKey]
    } else {
      last_table[DefaultEntryKey] = value
    }
  }

  get(addr: MultistringAddress, wc_values?: string[]): T | undefined {
    const value = getFromAddrMapFlat(this.table, addr, wc_values)
    return isUndef(value) ? this.table[DefaultEntryKey] : value
  }

  clear(): void {
    for (const k of Object.keys(this.table)) {
      delete this.table[k]
    }
  }

  toString(): string {
    const entries: string[] = []
    const addr_str: string[] = []
    function traverse(map: AddressMapFlat<T>) {
      if (map[DefaultEntryKey]) {
        entries.push(`${addr_str.join('.')}: ${map[DefaultEntryKey]}`)
      }
      if (map[WildcardEntryKey]) {
        addr_str.push('*')
        traverse(map[WildcardEntryKey] as AddressMapFlat<T>)
        addr_str.pop()
      }
      Object.keys(map).forEach((key) => {
        addr_str.push(`[${key}]`)
        traverse(map[key])
        addr_str.pop()
      })
    }
    traverse(this.table)
    return `AddrMap [\n${entries.map((s) => '  ' + s).join('\n')}\n]`
  }
}

export {
  MultistringAddress,
  WildcardMultistringAddress,
  AddressMap,
  DefaultEntryKey,
  WildcardEntryKey
}
