import {
  Type,
  MetadataKeyType,
} from '@loafer/core';

import {
  Annotation,
  Class,
  Constructor,
  Field,
  Method,
  Parameter,
  Metadata,
} from '@loafer/core/reflect';

import {
  TypeUtil,
} from '@loafer/core/util/TypeUtil';

import { Decorator } from './Decorator';
import { DecoratorUtil } from './util';
import { DecoratorType } from './type';
import { NotSupportedDecoratorError } from './error';


export class DecoratorFactory {
  public static create = <Attribute = {}>(DecoratorClass: Type<Decorator<Attribute>>) => {
    return (attribute: Attribute) => {
      const annotation: Decorator<Attribute> = new DecoratorClass(attribute);
      const name: string = DecoratorClass.name;

      return (...decoratorArgs: any[]) => {
        const decoratorType: DecoratorType = DecoratorUtil.getDecoratorType(decoratorArgs);

        const [target, propertyKey, descriptorOrParameterIndex] = decoratorArgs;

        const clazz: Class = Class._defineClass(TypeUtil.getType(target));
        let field: Field = null;
        let method: Method = null;
        let parameter: Parameter = null;
        let cons: Constructor = null;

        switch (decoratorType) {
        case DecoratorType.CLASS:
          try {
            cons = clazz._defineConstructor(Metadata.getOwnParamTypes(target));
            clazz._addAnnotation(annotation);
            return annotation.classDecorator.call(annotation, target);
          } catch (e) {
            if (e instanceof NotSupportedDecoratorError) {
              throw new NotSupportedDecoratorError(`Cannot apply @${name} decorator on Class.`);
            }
            throw e;
          }
        case DecoratorType.PROPERTY:
          try {
            field = clazz._defineField(propertyKey, Metadata.getOwnType(target, propertyKey));
            field._addAnnotation(annotation);
            return annotation.propertyDecorator.call(annotation, target, propertyKey);
          } catch (e) {
            if (e instanceof NotSupportedDecoratorError) {
              throw new NotSupportedDecoratorError(`Cannot apply @${name} decorator on Property.`);
            }
            throw e;
          }
        case DecoratorType.METHOD:
          try {
            method = clazz._defineMethod(propertyKey,
                                         Metadata.getOwnParamTypes(target, propertyKey),
                                         Metadata.getOwnReturnType(target, propertyKey));
            method._addAnnotation(annotation);

            return annotation.methodDecorator.call(annotation, target, propertyKey, descriptorOrParameterIndex);
          } catch (e) {
            if (e instanceof NotSupportedDecoratorError) {
              throw new NotSupportedDecoratorError(`Cannot apply @${name} decorator on Method.`);
            }
            throw e;
          }
        case DecoratorType.PARAMETER:
          try {
            if (undefined === propertyKey) {
              cons = clazz.getConstructor();
              parameter = cons.getParameter(descriptorOrParameterIndex);
            } else {
              method = clazz._defineMethod(propertyKey,
                                           Metadata.getOwnParamTypes(target, propertyKey),
                                           Metadata.getOwnReturnType(target, propertyKey));
              parameter = method.getParameter(descriptorOrParameterIndex);
            }
            parameter._addAnnotation(annotation);

            return annotation.parameterDecorator.call(annotation, target, propertyKey, descriptorOrParameterIndex);
          } catch (e) {
            if (e instanceof NotSupportedDecoratorError) {
              throw new NotSupportedDecoratorError(`Cannot apply @${name} decorator on Parameter.`);
            }
            throw e;
          }
        default:
          throw new NotSupportedDecoratorError(`Cannot determine decorator[@${name}] type.`);
        }
      };
    };
  }

}
