import {$$typeToString, $$ident, $$define} from 'ts-macros'

/*****
 * Every typocomath type has some associated types; they need
 * to be published in the following interface. The key is the
 * name of the type, and within the subinterface for that key,
 * the type of the 'type' property is the actual TypeScript type
 * we are associating the other properties to. Note the interface
 * is generic with one parameter, corresponding to the fact that
 * typocomath currently only allows generic types with a single
 * generic parameter. This way, AssociatedTypes<SubType> can give the
 * associated types for a generic type instantiated with SubType.
 * That's not necessary for the 'undefined' type (or if you look in the
 * `numbers` subdirectory, the 'number' type) or any concrete type,
 * but that's OK, the generic parameter doesn't hurt in those cases.
 ****/

type ValueIntersectionByKeyUnion<T,  TKey extends keyof T> = {
  [P in TKey]: (k: T[P])=>void
} [TKey] extends ((k: infer I)=>void) ? I : never

export interface AssociatedTypes<T> {
   undefined: {
      type: undefined
      zero: undefined
      one: undefined
      nan: undefined
      real: undefined
   }
}

type AssociatedTypeNames = keyof AssociatedTypes<unknown>['undefined']
type ALookup<T, Name extends AssociatedTypeNames> = ValueIntersectionByKeyUnion<{
   [K in keyof AssociatedTypes<T>]:
   T extends AssociatedTypes<T>[K]['type'] ? AssociatedTypes<T>[K][Name] : unknown},
   keyof AssociatedTypes<T>>

// For everything to compile, zero and one must be subtypes of T:
export type ZeroType<T> = ALookup<T, 'zero'> & T
export type OneType<T> = ALookup<T, 'one'> & T
// But I believe 'nan' really might not be, like I think we will have to use
// 'undefined' for the nan of 'bigint', as it has nothing at all like NaN,
// so don't force it:
export type NaNType<T> = ALookup<T, 'nan'>
export type RealType<T> = ALookup<T, 'real'>

/*****
 * The global signature patterns for all operations need to be published in the
 * following interface. Each key is the name of an operation (but note that
 * the Dispatcher will automatically merge operations that have the same
 * name when the first underscore `_` and everything thereafter is stripped).
 * The type of each key should be an interface with two properties: 'params'
 * whose type is the type of the parameter list for the operation, and
 * 'returns' whose type is the return type of the operation on those
 * parameters. These types are generic in a parameter type T which should
 * be interpreted as the type that the operation is supposed to "primarily"
 * operate on, although note that some of the parameters and/or return types
 * may depend on T rather than be exactly T.
 * So note that the example 're' below provides essentially the same
 * information that e.g.
 * `type ReOp<T> = (t: T) => RealType<T>`
 * would, but in a way that is much easier to manipulate in TypeScript,
 * and it records the name of the operation as 're' also by virtue of the
 * key 're' in the interface.
 ****/
export interface Signatures<T> {
   zero: (a: T) => ZeroType<T>
   one:  (a: T) => OneType<T>
   // nan needs to be able to operate on its own output for everything
   // else to compile. That's why its parameter type is widened:
   nan:  (a: T | NaNType<T>) => NaNType<T>
   re:   (a: T) => RealType<T>
}

type SignatureKey<T> = keyof Signatures<T>

export type Signature<Name extends SignatureKey<T>, T> = Signatures<T>[Name]
export type Returns<Name extends SignatureKey<T>, T> = ReturnType<Signatures<T>[Name]>
type Deps<T> =  T extends unknown ? { [K in keyof T]: T[K] } : never;
export type Dependencies<Name extends SignatureKey<T>, T> = Deps<{[K in Name]: Signature<K, T>}>

export type AliasOf<Name extends string, T> = T & {aliasOf?: Name}

// For defining implementations with type reflection
export function $implement<Impl>(name: string, expr: Impl) {
   $$define!(name, expr, false, true); // Final `true` is export
   $$ident!(name).reflectedType = $$typeToString!<Impl>();
}