import      _                                /**/ from 'lodash'
import      * as NodeUtil                         from 'node:util'
import type * as TY                               from '../types.ts'
// import      { nextTick }                       from 'node:process'
import      { AtoZlos }                          from '../lexicon/LexiconConsts.ts'
import      * as JSPrintf                         from 'sprintf-js'
//
export type *                                     from '../types.ts'
export      *                                     from './Streaming.ts'
export      *                                     from './Random.ts'
export      *                                     from '../UtilityConsts.ts'
export      *                                     from './Rot13.ts'
export      { badOutcome, throwable }             from './Outcome.ts'

export const { sprintf, vsprintf } = JSPrintf

export type ObjKey        = string          | symbol
export type ClxnKey       = string | number | symbol
export type ReadonlyCollection<KT extends ClxnKey = string, VT = any> = Record<KT,         VT> | readonly VT[]
export type ObjKVFunc<RT,  VT = any, KT extends ClxnKey = string> = (value: VT, keyOrIndex: KT,          index: number, ...args: any) => RT
export type ArrKVFunc<RT,  VT = any>                              = (value: VT, keyOrIndex: number,      index: number, ...args: any) => RT
export type KVFunc<RT,     VT = any, KT = string | number>        = (value: VT, keyOrIndex: KT, index: number, ...args: any) => RT

/** @returns a bag with keys a, b, ... z and values of type VT */
export function alphabetLookupBag<VT>(seed: VT | ((ltr: TY.AtoZlo) => VT)): Record<TY.AtoZlo, VT> {
  if (_.isFunction(seed)) {
    return objectify(AtoZlos, (ltr) => seed(ltr))
  }
  return objectify(AtoZlos, () => (seed))
}

/** Assign a **non-enumerable**, writable, configurable property to an object
 * See also {@link setNormalProp} and {@link decorate}
 * @param   obj     - The object to decorate
 * @param   key     - The key to decorate the object with
 * @param   value   - The value to decorate the object with
 * @returns           The value
 */
export function adorn<VT>(obj: object, key: string, value: VT): VT {
  Object.defineProperty(obj, key, { value, enumerable: false, writable: false, configurable: true })
  return value
}
/** Assign an enumerable, writable, configurable property to an object
 * See also {@link adorn} and {@link decorate}
 * @param   obj     - The object to decorate
 * @param   key     - The key to decorate the object with
 * @param   value   - The value to decorate the object with
 * @returns           The value
 */
export function setNormalProp<VT>(obj: object, key: string, value: VT): VT {
  Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true })
  return value
}

export function setNormalProps<OT extends Record<string, any>, VT extends Record<string, any>>(obj: OT, vals: VT): OT & VT {
  Object.entries(vals).forEach(([key, value]) => {
    Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true })
  })
  return obj as OT & VT
}

export function setHiddenProps<OT extends Record<string, any>, VT extends Record<string, any>>(obj: OT, vals: VT): OT & VT {
  Object.entries(vals).forEach(([key, value]) => {
    Object.defineProperty(obj, key, { value, enumerable: false, writable: true, configurable: true })
  })
  return obj as OT & VT
}
/** Assign non-enumerable, writable, configurable properties to an object
 * See also {@link adorn} and {@link decorate}
 * @param   obj - The object to decorate
 * @param   vals - The values to decorate the object with
 * @returns The decorated object
 */
export function decorate<OT extends Record<string, any>, VT extends Record<string, any>>(obj: OT, vals: VT): OT & VT {
  Object.entries(vals).forEach(([key, value]) => {
    Object.defineProperty(obj, key, { value, enumerable: false, writable: false, configurable: true })
  })
  return obj as OT & VT
}

/** Get the own properties of an object
 *
 * @param   obj - The object to get the own properties of
 * @returns       The own properties of the object; empty object if nil
 */
export function ownProps(obj: object | null | undefined): TY.Bag<TypedPropertyDescriptor<any>> {
  if (_.isNil(obj)) { return {} }
  return Object.getOwnPropertyDescriptors(obj)
}

/** Get the own property names of an object
 *
 * @param   obj - The object to get the own property names of
 * @returns       The own property names of the object; empty array if nil
 */
export function ownPropnames(obj: object | null | undefined): string[] {
  if (_.isNil(obj)) { return [] }
  return Object.getOwnPropertyNames(obj)
}

/** Get the property names of the **first parent prototype** of an object
 *
 * @param   obj - The object to get the prototype property names of
 * @returns       The prototype property names of the object; empty array if nil
 */
export function protoPropnames(obj: object | null | undefined): string[] {
  if (_.isNil(obj)) { return [] }
  const proto = Object.getPrototypeOf(obj)
  return ownPropnames(proto)
}

/** Get the property descriptor of a property of the **first parent prototype** of an object
 *
 * @param   obj       - The object to get the property descriptor of
 * @param   propname  - The name of the property to get the descriptor of
 * @returns           The property descriptor of the property; undefined if not found
 */
export function protoProp<VT>(obj: object, propname: TY.Fieldname): TypedPropertyDescriptor<VT> | undefined {
  return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), propname)
}

/** Get the property descriptor of a property of an object
 *
 * @param   obj       - The object to get the property descriptor of
 * @param   propname  - The name of the property to get the descriptor of
 * @returns           The property descriptor of the property; undefined if not found
 */
export function ownProp<VT>(obj: object, propname: TY.Fieldname): TypedPropertyDescriptor<VT> | undefined {
  return Object.getOwnPropertyDescriptor(obj, propname)
}

/** Get the first property descriptor found ascending the prototype chain
 * for a given property name
 *
 * @param   obj       - The object to get the property descriptor of
 * @param   propname  - The name of the property to get the descriptor of
 * @param   depth     - The depth of the prototype chain to search
 * @returns             The property descriptor of the property; undefined if not found
 */
export function getProp<VT>(obj: object, propname: TY.Fieldname, depth: number = 0): TypedPropertyDescriptor<VT> | undefined {
  if (depth < 0) { return undefined }
  const val = Object.getOwnPropertyDescriptor(obj, propname)
  if (val) { return val }
  const proto = Object.getPrototypeOf(obj)
  if  (! proto) { return undefined }
  return getProp(proto, propname, depth - 1)
}

export function bagsize(bag: TY.AnyBag | any[]): number {
  if (_.isArray(bag)) { return bag.length }
  return _.keys(bag).length
}

export interface PrettifyFieldOpts {
  /** per line for arrays    */ chunkSize?: number | undefined
  /** width per arr item     */ colwd?:     number | undefined
  /** primary key width      */ keywd?:     number | undefined
  /** prefix bag with key    */ key?:       string | undefined
  /** omit the brackets?     */ naked?:     boolean | undefined
  /** indent for arrays      */ indent?:    string | number | undefined,
}
export interface PrettifyOpts extends PrettifyFieldOpts {
  /** omit the brackets?        */ naked?:        boolean | undefined,
  /** always align array slots? */ chunkArrays?:  boolean | undefined,
}

function indentPaddingFor(indent: string | number | undefined) {
  if (! indent) { return '' }
  if (_.isString(indent)) { return indent }
  return _.repeat(' ', indent)
}

/**
 * Pretty-print an array of strings in chunks
 *
 * @param   arr - The array to pretty-print
 * @param   opts - The options for pretty-printing
 * @returns       The pretty-printed array
 */
export function prettifyInChunks(arr: readonly string[], { chunkSize = 20, colwd = 12, key, indent = 2 }: PrettifyOpts = {}) {
  const indentPadding = indentPaddingFor(indent)
  const lines = _.chunk(arr, chunkSize).map((chunk) => chunk.map((val) => _.padEnd(inspectify(val) + ',', colwd)).join(' '))
  const body = lines.join('\n' + indentPadding)
  return (key ? kfy(key) : '') + '[\n    ' + body + '\n]'
}
export function prettifyArray(arr: readonly any[], opts: PrettifyOpts = {}) {
  const inspected = inspectify(arr)
  if ((inspected.length < 180) && (! opts.chunkArrays)) { return opts.naked ? inspected.slice(1, -1).trim() : inspected }
  return prettifyInChunks(arr, opts)
}
export function prettifySet(set: Set<any>, { key, ...opts }: PrettifyOpts = {}) {
  const elements = prettifyArray(Array.from(set), opts)
  return (key ? kfy(key) : '') + 'new Set(' + elements + ')'
}
export function inspectify(val: any, _opts: NodeUtil.InspectOptions = {}) {
  return NodeUtil.inspect(val, { depth: 10, colors: false, breakLength: Infinity, maxArrayLength: Infinity, maxStringLength: Infinity, compact: true, numericSeparator: true })
}
const FieldnameRE = /^[a-zA-Z_]\w*$/
export function kfy(key: any, opts: PrettifyOpts = {}) {
  const str = FieldnameRE.test(key) ? key : inspectify(key)
  return _.padEnd(str + ':', opts.keywd)
}

export function prettifyField(val: any, opts: PrettifyOpts = {}) {
  if (_.isArray(val))  { return prettifyArray(val, opts) }
  if (_.isSet(val))    { return prettifySet(val,   opts) }
  return inspectify(val)
}

export function prettify(obj: object, { key, naked = false, ...opts }: PrettifyOpts = {}) {
  const firstkey     = _.isNil(key) ? '' : (kfy(key) + ' ')
  const startBracket = naked ? '' : '{'
  const endBracket   = naked ? '' : '}'
  const lines: string[] = []
  const indentPadding = indentPaddingFor(opts.indent)
  _.each(obj, (val, key) => {
    const keypart = kfy(key, { keywd: 12, ...opts })
    const valpart = prettifyField(val, opts)
    lines.push(indentPadding + keypart + ' ' + valpart)
  })
  return firstkey + startBracket +'\n' + lines.join(',\n') + ',\n' + endBracket
}

/**
 * Calls func with every element of collection (array, object, etc)
 * producing a bag using the [key, value] pairs returned by func
 *
 * * func: a function that accepts (val, collectionKey, seq, collection) => [resultKey, resultVal]
 *   - val: the value of the element in the collection
 *   - key: the key of the element in the collection: a number (for arrays) or string (for objects)
 *   - seq: the index of the element in the collection, 0...size
 *   - collection: the collection being processed
 *   - returns [resultKey, resultVal]
 * @returns bag with keys of type KT and values of type RVT (the return value of func)
 *
 * @example
 *
 *     rebag(bag, (val, key) => { const obj = lookup(key); return [obj.id, obj] }
 *     // { 'mbr.lotr:sam': { ... }, 'mbr.lotr:frodo': { ... } }
 */
export function rebag<RVT = any, IVT = any,                        RKT extends ObjKey = string>(clxn: readonly IVT[], func: ArrKVFunc<[RKT, RVT], IVT>): Record<RKT, RVT>
export function rebag<RVT = any, OT extends object = object, RKT extends ObjKey = string, IKT = any>(clxn: OT,  func: ObjKVFunc<[RKT, RVT], OT[keyof OT], keyof OT>): Record<RKT, RVT>
export function rebag<RVT = any, IVT = any, RKT extends ObjKey = ObjKey, IKT extends ObjKey = string>(clxn: ReadonlyCollection<IKT, IVT>, func: KVFunc<[RKT, RVT], IVT, IKT>): Record<RKT, RVT>
export function rebag<RVT = any, IVT = any, RKT extends ObjKey = ObjKey, IKT extends ObjKey = string>(clxn: ReadonlyCollection<IKT, IVT>, func: KVFunc<[RKT, RVT], IVT, IKT>): Record<RKT, RVT> {
  let seq = 0
  return Object.fromEntries(_.map<IVT, [RKT, RVT]>(clxn as any, (val, key) => func(val, key as any, seq++, clxn))) as Record<RKT, RVT>
}

/**
 * Calls func with every element of collection (array, object, etc)
 * and returns an object with the result, using the values of the original
 * elements as keys of the new bag.
 *
 * func should expect same args as map and return the value for each prop
 *    func(val, key/ii, collection) => val // for an object
 *
 * * func: a function that accepts (val, collectionKey, seq, collection) => val
 *   - val: the value of the element in the collection
 *   - key: the key of the element in the collection: a number (for arrays) or string (for objects)
 *   - seq: the index of the element in the collection, 0...size
 *   - collection: the collection being processed
 * @returns bag with keys of type KT and values of type RVT (the return value of func)
 *   - given an array of KT[],               returns a record with keys of type KT and values of type RVT
 *   - given a bag with values of type VT,   returns a record with keys of type VT and values of type RVT
 *   - given a bag with string keys,         returns a record with keys of type string and values of type RVT
 *
 * In all cases, the **values** of the collection become the **keys** of the new bag.
 *
 * @example
 *   objectify(['a', 'b'],   (vv, ii, ii, clxn) => vv + ii)      // { a: 'a0', b: 'b1' }
 *   objectify({x: 1, y: 2}, (vv, kk, ii, clxn) => vv * ii)      // { x: 0, y: 2 }
 *   objectify(['sam', 'frodo'], (name) => `${name}_id`)         // { sam: 'sam_id', frodo: 'frodo_id' }
 */
export function objectify<RT = any, KT extends ClxnKey   = string>(clxn: readonly KT[],                            func: ArrKVFunc<RT, KT>): Record<KT, RT>
export function objectify<RT = any, OT extends TY.AnyBag = TY.AnyBag, VT extends ClxnKey = OT[keyof OT]>(clxn: OT, func: ObjKVFunc<VT, OT[keyof OT], keyof OT>): Record<VT, RT>
export function objectify<RT = any, KT extends ClxnKey   = string, VT = any>(clxn: ReadonlyCollection<KT, VT>,     func: KVFunc<RT, KT, VT>): Record<KT, RT>
export function objectify<RT = any, KT extends ClxnKey   = string, VT = any>(clxn: ReadonlyCollection<KT, VT>,     func: KVFunc<RT, KT, VT>): Record<KT, RT> {
  return rebag(clxn as any, (val: any, key: any, seq: number, ...args: any): [any, any] => [val, func(val, key, seq, ...args)])
}

export function bagslice<VT extends object>(bag: VT, start: number | undefined, end: number | undefined): Partial<VT> {
  const keys = _.keys(bag).slice(start, end)
  return _.pick(bag, keys)
}

type PairSortFn<VT extends object, KT extends keyof VT = keyof VT> = ((entry: [key: KT, val: VT[KT]]) => any)

export function sortOnKeys<VT    extends object, KT extends keyof VT = keyof VT>([key, _val]: [KT, VT[KT]]) { return key }
export function sortOnNumkeys<VT extends object, KT extends keyof VT = keyof VT>([key, _val]: [KT, VT[KT]]) { return Number(key) }

/** Sort a bag by a funtion -- by default, its keys.
 * @note **by default, keys that parse as positive integers (1, 2, 3, ...) will appear first in retrieval order**.
 * This is part of the spec for Object.
 * @example { '1': 1, '2': 2, '-1': -1, '0.9': 0.9, '1.1': 1.1 }
 * If you use the magic function `sortOnNumkeys`,
 * keys that stringify as integers will be reinserted as `x.0`:
 * @example { '-1.0': -1, '0.9': 0.9, '1.0': 1, '1.1': 1.1, '2.0': 2 }
 */
export function bagsort<VT extends object, KT extends keyof VT = keyof VT>(bag: VT, sortfn: PairSortFn<VT, KT> = sortOnKeys, { mungeNumKeys = true }: { mungeNumKeys?: boolean } = {}): VT {
  const result = {} as VT
  const sorted = _.orderBy(_.entries(bag), sortfn) as [KT, VT[KT]][]
  if (! mungeNumKeys) {
    const pairs = _.orderBy(_.entries(bag), sortfn) as [KT, VT][]
    return _.fromPairs(pairs) as VT
  }
  for (const [key, val] of sorted) {
    const mungedKey = (Number.isInteger(Number(key))) ? (String(key) + '.0') : key
    result[mungedKey as KT] = val as VT[KT]
  }
  return result
}

const nextTicker = (globalThis as any).nextTick ?? ((func: () => void) => (func()))

/**
 * Sleep for one tick (i.e. let everyone else have a turn)
 * @returns true
 */
export async function sleepNextTick(): Promise<true> {
  return new Promise((yay) => { nextTicker(() => yay(true)) })
}

/**
 * Sleep for a given number of milliseconds
 * @param ms       - The number of milliseconds to sleep
 * @param nextTick - Whether to sleep for one tick before starting the sleep
 * @note IMPORTANT: if nextTick is true, the sleep will be delayed by
 *   the duration given PLUS an unknowable amount of time
 * @returns true
 */
export async function sleep(ms: number, awakeNextTick: boolean = false): Promise<true> {
  if (awakeNextTick) { await sleepNextTick() }
  return new Promise((resolve) => setTimeout(resolve, ms)).then(() => true)
}

export async function * catiters(...iters: TY.AnyIterable<any>[]) {
  for (const iter of iters) {
    yield * (iter as Iterable<any>)
  }
}

export function isNode(): boolean {
  return (typeof process !== 'undefined' && process.release && process.release.name === 'node')
}

export function isBrowser(): boolean {
  // if (isNode()) { return false }
  return (!! (import.meta as any).client)
}