//////////////////////////////////////////////////////
// This file is generated by build-LaraJoinPoint.js //
//////////////////////////////////////////////////////

/* eslint-disable @typescript-eslint/no-explicit-any */

import JavaTypes from "./lara/util/JavaTypes.js";

/**
 * Type for type equality assertion. If T is equal to U, return Y, otherwise return N.
 * Source: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
 * @example
 * type A = Equals<string, string, "Y", "N">; // "Y"
 * type B = Equals<string, number, "Y", "N">; // "N"
 */
type Equals<T, U, Y = unknown, N = never> =
  (<G>() => G extends T ? 1 : 2) extends
  (<G>() => G extends U ? 1 : 2) ? Y : N;

type DefaultAttributeHelper<
  T extends typeof LaraJoinPoint,
  DefaultAttributeMap,
  PrivateMapper,
> = {
  [K in keyof DefaultAttributeMap]: K extends keyof PrivateMapper
    ? Equals<T, PrivateMapper[K], DefaultAttributeMap[K], never>
    : never;
}[keyof DefaultAttributeMap];

// Extract the type A from A | undefined
type ExtractedType<T> = T extends undefined ? never : T;

export type DefaultAttribute<T extends typeof LaraJoinPoint> = DefaultAttributeHelper<
  T,
  ExtractedType<T["_defaultAttributeInfo"]["map"]>,
  ExtractedType<T["_defaultAttributeInfo"]["type"]>
>;

type NameFromWrapperClassHelper<T extends typeof LaraJoinPoint, U> = {
  [K in keyof U]: Equals<T, U[K], K, never>;
}[keyof U];

export type NameFromWrapperClass<T extends typeof LaraJoinPoint> = NameFromWrapperClassHelper<
  T,
  ExtractedType<T["_defaultAttributeInfo"]["jpMapper"]>
>;

export class LaraJoinPoint {
  /**
   * @internal
   */
  static readonly _defaultAttributeInfo: {readonly map?: any, readonly name: string | null, readonly type?: any, readonly jpMapper?: any} = {
    name: null,
  };
  /**
   * @internal
   */
  _javaObject!: any;
  constructor(obj: any) {
    this._javaObject = obj;
  }
  get dump(): string { return wrapJoinPoint(this._javaObject.getDump()) }
  get joinPointType(): string { return wrapJoinPoint(this._javaObject.getJoinPointType()) }
  get node(): object { return (this._javaObject.getNode()) }
  get self(): LaraJoinPoint { return wrapJoinPoint(this._javaObject.getSelf()) }
  get super(): LaraJoinPoint { return wrapJoinPoint(this._javaObject.getSuper()) }
  get children(): LaraJoinPoint[] { return wrapJoinPoint(this._javaObject.getChildren()) }
  get descendants(): LaraJoinPoint[] { return wrapJoinPoint(this._javaObject.getDescendants()) }
  get scopeNodes(): LaraJoinPoint[] { return wrapJoinPoint(this._javaObject.getScopeNodes()) }
  insert(position: "before" | "after" | "replace", code: string): LaraJoinPoint[];
  insert(position: "before" | "after" | "replace", joinpoint: LaraJoinPoint): LaraJoinPoint[];
  insert(p1: "before" | "after" | "replace", p2: string | LaraJoinPoint): LaraJoinPoint[] { return wrapJoinPoint(this._javaObject.insert(unwrapJoinPoint(p1), unwrapJoinPoint(p2))); }
  toString(): string { return wrapJoinPoint(this._javaObject.toString()); }
  equals(jp: LaraJoinPoint): boolean { return wrapJoinPoint(this._javaObject.equals(unwrapJoinPoint(jp))); }
  instanceOf(name: string): boolean;
  instanceOf(names: string[]): boolean;
  instanceOf(p1: string | string[]): boolean { return wrapJoinPoint(this._javaObject.instanceOf(unwrapJoinPoint(p1))); }
}

/**
 * Converts Java join point objects to TypeScript objects.
 */
export interface JoinpointMapper {
  toJpClass(jpTypename: string): typeof LaraJoinPoint | undefined;
  toJpInstance(jpTypename: string, javaJp: any): LaraJoinPoint | undefined;
  fromJpClass(jpType: typeof LaraJoinPoint): string | undefined;
}

const JoinpointMappers: JoinpointMapper[] = [];

/**
 * This function is for internal use only. DO NOT USE IT!
 */
export function clearJoinpointMappers(): void {
  JoinpointMappers.length = 0;
}

/**
 * This function is for internal use only. DO NOT USE IT!
 */
export function getJoinpointMappers(): JoinpointMapper[] {
  return JoinpointMappers;
}

/**
 * For mappers based on objects
 */
export type JoinpointMapperType = { [key: string]: typeof LaraJoinPoint };

export function registerJoinpointMapper(mapper: JoinpointMapperType): void {
  // Create mapper from object
  const jpMapper: JoinpointMapper = {
    toJpClass(jpTypename: string) {
      return mapper[jpTypename];
    },

    toJpInstance(jpTypename: string, javaJp: any) {
      const jpClass = this.toJpClass(jpTypename);
      if (jpClass) {
        return new jpClass(javaJp);
      }

      return undefined;
    },

    fromJpClass(jpType: typeof LaraJoinPoint) {
      return Object.keys(mapper).find((key) => mapper[key] === jpType);
    },
  };

  JoinpointMappers.push(jpMapper);
}

/**
 * For mappers based on functions
 */
export type JoinpointMapperFunction = (
  jpTypename: string
) => typeof LaraJoinPoint | undefined;

export function registerJoinpointMapperFunction(
  mapper: JoinpointMapperFunction
): void {
  // Create mapper from function
  const jpMapper = {
    toJpClass(jpTypename: string): typeof LaraJoinPoint | undefined {
      return mapper(jpTypename);
    },

    toJpInstance(jpTypename: string, javaJp: any): LaraJoinPoint | undefined {
      const jpClass = this.toJpClass(jpTypename);
      if (jpClass) {
        return new jpClass(javaJp);
      }

      return undefined;
    },

    // Not possible to implement with just a function
    fromJpClass(jpType: typeof LaraJoinPoint): string | undefined {
      return undefined;
    },
  };

  JoinpointMappers.push(jpMapper);
}
  
export function wrapJoinPoint(obj: any): any {
  if (JoinpointMappers.length === 0) {
    return obj;
  }

  if (obj === undefined) {
    return obj;
  }

  if (obj instanceof LaraJoinPoint) {
    return obj;
  }

  if (ArrayBuffer.isView(obj)) {
    return Array.from(obj as any).map(wrapJoinPoint);
  }

  if (typeof obj !== "object") {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(wrapJoinPoint);
  }

  if (!JavaTypes.isJavaObject(obj)) {
    return obj;
  }

  if (
    JavaTypes.instanceOf(obj, "pt.up.fe.specs.jsengine.node.UndefinedValue")
  ) {
    return undefined;
  }

  if (
    JavaTypes.instanceOf(obj, "java.util.ArrayList")
  ) {
    return obj.toArray().map(wrapJoinPoint);
  }

  if (
    JavaTypes.instanceOf(obj, "org.suikasoft.jOptions.DataStore.DataClass") &&
    !JavaTypes.instanceOf(obj, "pt.up.fe.specs.clava.ClavaNode")
  ) {
    return obj;
  }

  if (obj.getClass().isEnum()) {
    return obj.toString();
  }

  const isJavaJoinPoint = JavaTypes.JoinPoint.isJoinPoint(obj);
  if (!isJavaJoinPoint) {
    throw new Error(
      `Given Java join point is a Java class but is not a JoinPoint: ${obj.getClass()}`
    );
  }

  const jpType: string = obj.getJoinPointType();
  // Iterate in reverse order, to give priority to most recently added mappers
  for (let i = JoinpointMappers.length - 1; i >= 0; i--) {
    const mapper = JoinpointMappers[i];
    const laraJp = mapper.toJpInstance(jpType, obj);
    if (laraJp) {
      return laraJp;
    }
  }
  throw new Error("No mapper found for join point type: " + jpType);
}

export function unwrapJoinPoint(obj: any): any {
  if (obj instanceof LaraJoinPoint) {
    return obj._javaObject;
  }

  if (Array.isArray(obj)) {
    return obj.map(unwrapJoinPoint);
  }

  return obj;
}
