import {
  Type,
  PropertyKeyType,
} from '../type';

import {
  TypeUtil,
} from '../util/TypeUtil';

import { AccessibleObject } from './AccessibleObject';
import { Annotation } from './Annotation';
import { SystemClassRegistry } from './ClassRegistry';
import { Constructor } from './Constructor';
import { Field } from './Field';
import { Method } from './Method';
import { Metadata } from './Metadata';



export class Class extends AccessibleObject {
  private _type: Type<any>;
  private _constructor: Constructor;
  private _fields: Map<PropertyKeyType, Field>;
  private _methods: Map<PropertyKeyType, Method>;

  /**
   * forClass
   */
  public static forType(type: Type<any>): Class | undefined {
    return SystemClassRegistry.get(type);
  }

  /**
   * _defineClass
   */
  public static _defineClass(type: Type<any>): Class {
    let clazz: Class = Class.forType(type);
    if (undefined === clazz) {
      clazz = new Class(type);
      SystemClassRegistry.set(type, clazz);
    }

    if (null === clazz._constructor) {
      const parameterTypes = Metadata.getOwnParamTypes(type);
      if (undefined !== parameterTypes) {
        clazz._constructor = new Constructor(clazz, parameterTypes);
      }
    }

    return clazz;
  }

  private constructor(type: Type<any>) {
    super();
    this._type = type;
    this._fields = new Map();
    this._methods = new Map();
  }

  /**
   * _defineField
   */
  public _defineConstructor(parameterTypes: any[]): Constructor {
    let cons: Constructor = this._constructor;
    if (undefined === cons) {
      cons = new Constructor(this, parameterTypes);
      this._constructor = cons;
    }
    return cons;
  }

  /**
   * _defineField
   */
  public _defineField(propertyKey: PropertyKeyType, propertyType: any): Field {
    let field: Field = this._fields.get(propertyKey);
    if (undefined === field) {
      field = new Field(this, propertyKey, propertyType);
      this._fields.set(propertyKey, field);
    }
    return field;
  }

  /**
   * _defineMethod
   */
  public _defineMethod(propertyKey: PropertyKeyType, parameterTypes: any[], returnType: any): Method {
    let method: Method = this._methods.get(propertyKey);
    if (undefined === method) {
      method = new Method(this, propertyKey, parameterTypes, returnType);
      this._methods.set(propertyKey, method);
    }
    return method;
  }

  public getType(): Type<any> {
    return this._type;
  }

  public getConstructor(): Constructor {
    if (undefined === this._constructor) {
      this._constructor = new Constructor(this, undefined);
    }
    return this._constructor;
  }

  public getOwnField(propertyKey: PropertyKeyType): Field | undefined {
    return this._fields.get(propertyKey);
  }

  public getOwnFields(): Map<PropertyKeyType, Field> {
    return this._fields;
  }

  public getField(propertyKey: PropertyKeyType): Field | undefined {
    const fields = this.getFields();

    return fields.get(propertyKey);
  }

  public getFields(): Map<PropertyKeyType, Field> {
    const fields: Map<PropertyKeyType, Field> = new Map();

    const types = TypeUtil.ancestorsOf(this._type);
    if (null === types || 0 === types.length) {
      return fields;
    }
    for (let i = 0; i < types.length; i++) {
      const tType = types[i];
      const tClazz = Class.forType(tType);
      if (undefined === tClazz) {
        continue;
      }

      tClazz.getOwnFields().forEach((value: Field, key: PropertyKeyType, map: Map<PropertyKeyType, Field>): void => {
        fields.set(key, value);
      });
    }

    return fields;
  }

  public getOwnMethod(propertyKey: PropertyKeyType): Method | undefined {
    return this._methods.get(propertyKey);
  }

  public getOwnMethods(): Map<PropertyKeyType, Method> {
    return this._methods;
  }

  public getMethod(propertyKey: PropertyKeyType): Method | undefined {
    const methods = this.getMethods();

    return methods.get(propertyKey);
  }

  public getMethods(): Map<PropertyKeyType, Method> {
    const methods: Map<PropertyKeyType, Method> = new Map();

    const types = TypeUtil.ancestorsOf(this._type);
    if (null === types || 0 === types.length) {
      return methods;
    }
    for (let i = 0; i < types.length; i++) {
      const tClazzType = types[i];
      const tClazz = Class.forType(tClazzType);
      if (undefined === tClazz) {
        continue;
      }

      tClazz.getOwnMethods().forEach((value: Method, key: PropertyKeyType, map: Map<PropertyKeyType, Method>): void => {
        methods.set(key, value);
      });
    }

    return methods;
  }

  public getAnnotation<AnnotationType extends Annotation>(annotationClass: Type<AnnotationType>): AnnotationType | undefined {
    const annotations = this.getAnnotations();

    return <AnnotationType>annotations.get(annotationClass);
  }

  public getAnnotations(): Map<Type<any>, Annotation> {
    const annotations: Map<Type<any>, Annotation> = new Map();

    const types = TypeUtil.ancestorsOf(this._type);
    if (null === types || 0 === types.length) {
      return annotations;
    }
    for (let i = 0; i < types.length; i++) {
      const tClazzType = types[i];
      const tClazz = Class.forType(tClazzType);
      if (undefined === tClazz) {
        continue;
      }

      tClazz.getOwnAnnotations().forEach((value: Annotation, key: Type<any>, map: Map<Type<any>, Annotation>): void => {
        annotations.set(key, value);
      });
    }

    return annotations;
  }

  public getAnnotationsByType<AnnotationType extends Annotation>(annotationClass: Type<AnnotationType>)
                                                                                                      : AnnotationType[] | undefined {
    const annotations = this.getAnnotations();
    if (0 === annotations.size) {
      return undefined;
    }
    const results: AnnotationType[] = [];
    for (const classType of Array.from(annotations.keys())) {
      if (classType === annotationClass) {
        results.push(<AnnotationType>annotations.get(classType));
      }
    }
    if (0 === results.length) {
      return undefined;
    }
    return results;
  }

  public getName(): string {
    return this._type.name;
  }
}
