/// <reference types="reflect-metadata" />

/**
 * @alterior/annotations
 * A class library for handling Typescript metadata decorators via "annotation" classes
 * 
 * (C) 2017-2019 William Lahti
 * 
 */

import { NotSupportedError } from '@alterior/common';

/**
 * Represents an annotation which could be stored in the standard annotation lists 
 * on a class.
 */
export interface IAnnotation {
    $metadataName? : string;
}

// These are the properties on a class where annotation metadata is deposited 
// when annotation decorators are executed. Note that these are intended to 
// be compatible with Angular 6's model

export const ANNOTATIONS_KEY = '__annotations__';
export const CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY = '__parameters__';
export const PROPERTY_ANNOTATIONS_KEY = '__prop__metadata__';
export const METHOD_PARAMETER_ANNOTATIONS_KEY = '__parameter__metadata__';

/**
 * Represents an Annotation subclass from the perspective of using it to 
 * construct itself by passing an options object.
 */
interface AnnotationConstructor<AnnoT extends Annotation, TS extends any[]> {
    new (...args : TS) : AnnoT;
    getMetadataName();
}

export type AnnotationClassDecorator<TS extends any[]> = (...args: TS) => ((target: any) => void);
export type AnnotationPropertyDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => void);
export type AnnotationMethodDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol) => void);
export type AnnotationParameterDecorator<TS extends any[]> = (...args: TS) => ((target: any, propertyKey: string | symbol, index: number) => void);
// (...args: TS): (target, ...args) => void;

type UnionToIntersection<U> = 
  (U extends any ? (x: U)=>void : never) extends ((x: infer I)=>void) ? I : never
  
type DecoratorTypeUnionForValidTargets<Targets> = 
      Targets extends 'class' ? ClassDecorator 
    : Targets extends 'method' ? MethodDecorator
    : Targets extends 'property' ? PropertyDecorator
    : Targets extends 'parameter' ? ParameterDecorator
    : never;
;

type DecoratorTypeForValidTargets<Targets> = UnionToIntersection<DecoratorTypeUnionForValidTargets<Targets>>;

/**
 * Represents a decorator which accepts an Annotation's options object.
 */
export type AnnotationDecorator<TS extends any[]> = (...args: TS) => 
    ClassDecorator &
    PropertyDecorator &
    MethodDecorator &
    ParameterDecorator
;

export interface DecoratorSite {
    type : 'class' | 'method' | 'property' | 'parameter';
    target : any;
    propertyKey? : string;
    propertyDescriptor? : PropertyDescriptor;
    index? : number;
}

export type AnnotationDecoratorTarget = 'class' | 'property' | 'method' | 'parameter';

export interface AnnotationDecoratorOptions<AnnoT, TS extends any[] = []> {
    factory? : (target : DecoratorSite, ...args : TS) => AnnoT | void;
    validTargets? : AnnotationDecoratorTarget[];
    allowMultiple? : boolean;
}

/**
 * Thrown when a caller attempts to decorate an annotation target when the 
 * annotation does not support that target.
 */
export class AnnotationTargetError extends NotSupportedError {
    constructor(annotationClass, invalidType: string, supportedTypes: string[], message?: string) {
        super(message || `You cannot decorate a ${invalidType} with annotation ${annotationClass.name}. Valid targets: ${supportedTypes.join(', ')}`);

        this._invalidType = invalidType;
        this._annotationClass = annotationClass;
        this._supportedTypes = supportedTypes;
    }

    private _invalidType : string;
    private _annotationClass : Function;
    private _supportedTypes : string[];
    
    get invalidType() : string {
        return this._invalidType;
    }

    get supportedTypes(): string[] {
        return this._supportedTypes;
    }

    get annotationClass(): Function {
        return this._annotationClass;
    }
}

/**
 * Create a decorator suitable for use along with an Annotation class.
 * This is the core of the Annotation.decorator() method.
 * 
 * @param ctor 
 * @param options 
 */
function makeDecorator<AnnoT extends Annotation, TS extends any[]>(
    ctor : AnnotationConstructor<AnnoT, TS>, 
    options? : AnnotationDecoratorOptions<AnnoT, TS>
): AnnotationDecorator<TS> 
{
    if (!ctor)
        throw new Error(`Cannot create decorator: Passed class reference was undefined/null: This can happen due to circular dependencies.`);

    let factory : (target : DecoratorSite, ...args : TS) => AnnoT | void = null;
    let validTargets : string[] = null;
    let allowMultiple = false;

    if (options) {
        if (options.factory)
            factory = options.factory;
        if (options.validTargets)
            validTargets = options.validTargets as any;
        if (options.allowMultiple)
            allowMultiple = options.allowMultiple;
    }

    if (!factory)
        factory = (target, ...args) => new ctor(...args);
    
    if (!validTargets)
        validTargets = ['class', 'method', 'property', 'parameter'];
    
    return (...decoratorArgs : TS) => {
        return (target, ...args) => {

            // Note that checking the length is not enough, because for properties
            // two arguments are passed, but the property descriptor is `undefined`.
            // So we make sure that we have a valid property descriptor (args[1]) 

            if (args.length === 2 && args[1] !== undefined) {
                if (typeof args[1] === 'number') {
                    // Parameter decorator on a method or a constructor (methodName will be undefined)
                    let methodName : string = args[0];
                    let index : number = args[1];

                    if (!validTargets.includes('parameter'))
                        throw new AnnotationTargetError(ctor, 'parameter', validTargets);

                    if (!allowMultiple) {
                        let existingParamDecs = Annotations.getParameterAnnotations(target, methodName, true);
                        let existingParamAnnots = existingParamDecs[index] || [];
                        if (existingParamAnnots.find(x => x.$metadataName === ctor['$metadataName'])) 
                            throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
                    }

                    if (methodName) {
                        let annotation = factory({
                            type: 'parameter',
                            target, 
                            propertyKey: methodName,
                            index
                        }, ...decoratorArgs);

                        if (!annotation)
                            return;

                        annotation.applyToParameter(target, methodName, index);
                    } else {
                        let annotation = factory({
                            type: 'parameter',
                            target,
                            index
                        }, ...decoratorArgs);

                        if (!annotation)
                            return;
                            
                        annotation.applyToConstructorParameter(target, index);
                    }
                } else {
                    // Method decorator
                    let methodName : string = args[0];
                    let descriptor : PropertyDescriptor = args[1];
                    
                    if (!validTargets.includes('method'))
                        throw new AnnotationTargetError(ctor, 'method', validTargets);
                       
                    if (!allowMultiple) {
                        let existingAnnots = Annotations.getMethodAnnotations(target, methodName, true);
                        if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName'])) 
                            throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
                    }

                    let annotation = factory({
                        type: 'method',
                        target,
                        propertyKey: methodName,
                        propertyDescriptor: descriptor
                    }, ...decoratorArgs);

                    if (!annotation)
                        return;

                    annotation.applyToMethod(target, methodName);
                }
            } else if (args.length >= 1) { 
                // Property decorator
                let propertyKey : string = args[0];
                
                if (!validTargets.includes('property'))
                    throw new AnnotationTargetError(ctor, 'property', validTargets);

                if (!allowMultiple) {
                    let existingAnnots = Annotations.getPropertyAnnotations(target, propertyKey, true);
                    if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName'])) 
                        throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
                }

                let annotation = factory({
                    type: 'property',
                    target,
                    propertyKey
                }, ...decoratorArgs);

                if (!annotation)
                    return;

                annotation.applyToProperty(target, propertyKey);

            } else if (args.length === 0) {
                // Class decorator

                if (!validTargets.includes('class'))
                    throw new AnnotationTargetError(ctor, 'class', validTargets);
                    
                if (!allowMultiple) {
                    let existingAnnots = Annotations.getClassAnnotations(target);
                    if (existingAnnots.find(x => x.$metadataName === ctor['$metadataName'])) 
                        throw new Error(`Annotation ${ctor.name} can only be applied to an element once.`);
                }

                let annotation = factory({
                    type: 'class',
                    target
                }, ...decoratorArgs);

                if (!annotation)
                    return;

                annotation.applyToClass(target);
            } else {
                // Invalid, or future decorator types we don't support yet.
                throw new Error(`Encountered unknown decorator invocation with ${args.length + 1} parameters.`);
            }
        };
    }
}

export function MetadataName(name : string) {
    return target => Object.defineProperty(target, '$metadataName', { value: name });
}

export interface MutatorDefinition {
    invoke: (site : DecoratorSite) => void;
    options?: AnnotationDecoratorOptions<void>;
}

/**
 * Represents a metadata annotation which can be applied to classes,
 * constructor parameters, methods, properties, or method parameters 
 * via decorators. 
 * 
 * Custom annotations are defined as subclasses of this class. 
 * By convention, all custom annotation classes should have a name 
 * which ends in "Annotation" such as "NameAnnotation". 
 * 
 * To create a new annotation create a subclass of `Annotation` 
 * with a constructor that takes the parameters you are interested in
 * storing, and save the appropriate information onto fields of the 
 * new instance. For your convenience, Annotation provides a default 
 * constructor which takes a map object and applies its properties onto
 * the current instance, but you may replace it with a constructor that
 * takes any arguments you wish.
 * 
 * You may wish to add type safety to the default constructor parameter.
 * To do so, override the constructor and define it:
 * 
```
class XYZ extends Annotation {
    constructor(
        options : MyOptions
    ) {
        super(options);
    }
}
```
 *
 * Annotations are applied by using decorators. 
 * When you define a custom annotation, you must also define a 
 * custom decorator:
 * 
```
const Name = 
    NameAnnotation.decorator();
```
 * You can then use that decorator:
```
@Name()
class ABC {
    // ...
}
```
 * 
 */
export class Annotation implements IAnnotation {
    constructor(
        props? : any
    ) {
        this.$metadataName = this.constructor['$metadataName'];
        if (!this.$metadataName || !this.$metadataName.includes(':')) {
            throw new Error(
                `You must specify a metadata name for this annotation in the form of ` 
                + ` 'mynamespace:myproperty'. You specified: '${this.$metadataName || '<none>'}'`
            );
        }

        Object.assign(this, props || {});
    }

    readonly $metadataName : string;

    toString() {
        return `@${this.constructor.name}`;
    }

    static getMetadataName(): string {
        if (!this['$metadataName'])
            throw new Error(`Annotation subclass ${this.name} must have @MetadataName()`);

        return this['$metadataName'];
    }
    
    /**
     * Construct a decorator suitable for attaching annotations of the called type 
     * onto classes, constructor parameters, methods, properties, and parameters.
     * Must be called while referencing the subclass of Annotation you wish to construct
     * the decorator for. E.g., for FooAnnotation, call FooAnnotation.decorator().
     * 
     * @param this The Annotation subclass for which the decorator is being constructed
     * @param options Allows for specifying options which will modify the behavior of the decorator. 
     *  See the DecoratorOptions documentation for more information.
     */
    public static decorator<
        T extends Annotation, 
        TS extends any[], 
        U extends AnnotationDecoratorTarget
    >(
        this: AnnotationConstructor<T, TS>, 
        options? : Exclude<AnnotationDecoratorOptions<T, TS>, 'validTargets'> & { validTargets: U[] }
    ): (...args: TS) => DecoratorTypeForValidTargets<U>;
    public static decorator<T extends Annotation, TS extends any[]>(this: AnnotationConstructor<T, TS>, options?: AnnotationDecoratorOptions<T, TS>): AnnotationDecorator<TS>;
    public static decorator<T extends Annotation, TS extends any[]>(this: AnnotationConstructor<T, TS>, options?: AnnotationDecoratorOptions<T, TS>): AnnotationDecorator<TS> {
        if ((this as any) === Annotation) {
            if (!options || !options.factory) {
                throw new Error(`When calling Annotation.decorator() to create a mutator, you must specify a factory (or use Mutator.decorator())`);
            }
        }
        return makeDecorator(this, options);
    }

    /**
     * Clone this annotation instance into a new one. This is not a deep copy.
     */
    public clone(): this {
        return Annotations.clone(this);
    }

    /**
     * Apply this annotation to a given target. 
     * @param target 
     */
    public applyToClass(target : any): this {
        return Annotations.applyToClass(this, target);
    }

    /**
     * Apply this annotation instance to the given property.
     * @param target 
     * @param name 
     */
    public applyToProperty(target : any, name : string): this {
        return Annotations.applyToProperty(this, target, name);
    }

    /**
     * Apply this annotation instance to the given method.
     * @param target 
     * @param name 
     */
    public applyToMethod(target : any, name : string): this {
        return Annotations.applyToMethod(this, target, name);
    }

    /**
     * Apply this annotation instance to the given method parameter.
     * @param target 
     * @param name 
     * @param index 
     */
    public applyToParameter(target : any, name : string, index : number): this {
        return Annotations.applyToParameter(this, target, name, index);
    }

    /**
     * Apply this annotation instance to the given constructor parameter.
     * @param target 
     * @param name 
     * @param index 
     */
    public applyToConstructorParameter(target : any, index : number): this {
        return Annotations.applyToConstructorParameter(this, target, index);
    }

    /**
     * Filter the given list of annotations for the ones which match this annotation class
     * based on matching $metadataName.
     * 
     * @param this 
     * @param annotations 
     */
    public static filter<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>,
        annotations : IAnnotation[]
    ) : T[] {
        return annotations.filter(
            x => x.$metadataName === this.getMetadataName()
        ) as T[];
    }

    /**
     * Get all instances of this annotation class attached to the given class.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * @param this 
     * @param type The class to check
     */
    public static getAllForClass<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any
    ): T[] {
        return (Annotations.getClassAnnotations(type) as T[])
            .filter(x => x.$metadataName === this.getMetadataName())
        ;
    }

    /**
     * Get a single instance of this annotation class attached to the given class.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type 
     */
    public static getForClass<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any
    ): T {
        return (this as any).getAllForClass(type)[0];
    }

    /**
     * Get all instances of this annotation class attached to the given method.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where the method is defined
     * @param methodName The name of the method to check
     */
    public static getAllForMethod<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any, 
        methodName : string
    ): T[] {
        return (Annotations.getMethodAnnotations(type, methodName) as T[])
            .filter(x => x.$metadataName === this.getMetadataName())
        ;
    }

    /**
     * Get one instance of this annotation class attached to the given method.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where the method is defined
     * @param methodName The name of the method to check
     */
    public static getForMethod<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any,
        methodName : string
    ): T {
        return (this as any).getAllForMethod(type, methodName)[0];
    }
    
    /**
     * Get all instances of this annotation class attached to the given property.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where the property is defined
     * @param propertyName The name of the property to check
     */
    public static getAllForProperty<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any, 
        propertyName : string
    ): T[] {
        return (Annotations.getPropertyAnnotations(type, propertyName) as T[])
            .filter(x => x.$metadataName === this.getMetadataName())
        ;
    }

    /**
     * Get one instance of this annotation class attached to the given property.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where the property is defined
     * @param propertyName The name of the property to check
     */
    public static getForProperty<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any,
        propertyName : string
    ): T {
        return (this as any).getAllForProperty(type, propertyName)[0];
    }
    
    /**
     * Get all instances of this annotation class attached to the parameters of the given method.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where the method is defined
     * @param methodName The name of the method where parameter annotations should be checked for
     */
    public static getAllForParameters<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any, 
        methodName : string
    ): T[][] {
        return (Annotations.getParameterAnnotations(type, methodName) as T[][])
            .map(set => (set || []).filter(x => (this as any) === Annotation ? true : (x.$metadataName === this.getMetadataName())))
        ;
    }

    /**
     * Get all instances of this annotation class attached to the parameters of the constructor
     * for the given class.
     * If called on a subclass of Annotation, it returns only annotations that match 
     * that subclass.
     * 
     * @param this 
     * @param type The class where constructor parameter annotations should be checked for
     */
    public static getAllForConstructorParameters<T extends Annotation, TS extends any[]>(
        this : AnnotationConstructor<T, TS>, 
        type : any
    ): T[][] {
        
        let finalSet = new Array(<any>type.length).fill(undefined);
        let annotations = (Annotations.getConstructorParameterAnnotations(type) as T[][])
            .map(set => (set || []).filter(x => (this as any) === Annotation ? true : (x.$metadataName === this.getMetadataName())))
        ;

        for (let i = 0, max = annotations.length; i < max; ++i)
            finalSet[i] = annotations[i];

        return finalSet;
    }
}

/**
 * A helper class for managing annotations
 */
export class Annotations {

    /**
     * Copy the annotations defined for one class onto another.
     * @param from The class to copy annotations from
     * @param to The class to copy annotations to
     */
    public static copyClassAnnotations(from : Function, to : Function) {
        let annotations = Annotations.getClassAnnotations(from);
        annotations.forEach(x => Annotations.applyToClass(x, to));
    }

    /**
     * Apply this annotation to a given target. 
     * @param target 
     */
    public static applyToClass<T extends IAnnotation>(annotation : T, target : any): T {
        let list = this.getOrCreateListForClass(target);
        let clone = this.clone(annotation);
        list.push(clone);

        if (Reflect.getOwnMetadata) {
            let reflectedAnnotations = Reflect.getOwnMetadata('annotations', target) || [];
            reflectedAnnotations.push({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
            Reflect.defineMetadata('annotations', reflectedAnnotations, target);
        }

        return clone;
    }

    /**
     * Apply this annotation instance to the given property.
     * @param target 
     * @param name 
     */
    public static applyToProperty<T extends IAnnotation>(annotation : T, target : any, name : string): T {
        let list = this.getOrCreateListForProperty(target, name);
        let clone = this.clone(annotation);
        list.push(clone);
        
        if (Reflect.getOwnMetadata) {
            let reflectedAnnotations = Reflect.getOwnMetadata('propMetadata', target, name) || [];
            reflectedAnnotations.push({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
            Reflect.defineMetadata('propMetadata', reflectedAnnotations, target, name);
        }

        return clone;
    }

    /**
     * Apply this annotation instance to the given method.
     * @param target 
     * @param name 
     */
    public static applyToMethod<T extends IAnnotation>(annotation : T, target : any, name : string): T {
        let list = this.getOrCreateListForMethod(target, name);
        let clone = Annotations.clone(annotation);
        list.push(clone);

        if (Reflect.getOwnMetadata && target.constructor) {
            const meta = Reflect.getOwnMetadata('propMetadata', target.constructor) || {};
            meta[name] = (meta.hasOwnProperty(name) && meta[name]) || [];
            meta[name].unshift({ toString() { return `${clone.$metadataName}`; }, annotation: clone });
            Reflect.defineMetadata('propMetadata', meta, target.constructor);
        }

        return clone;
    }

    /**
     * Apply this annotation instance to the given method parameter.
     * @param target 
     * @param name 
     * @param index 
     */
    public static applyToParameter<T extends IAnnotation>(annotation : T, target : any, name : string, index : number): T {
        let list = this.getOrCreateListForMethodParameters(target, name);
        while (list.length < index)
            list.push(null);

        let paramList = list[index] || [];
        let clone = this.clone(annotation);
        paramList.push(clone);
        list[index] = paramList;

        return clone;
    }

    /**
     * Apply this annotation instance to the given constructor parameter.
     * @param target 
     * @param name 
     * @param index 
     */
    public static applyToConstructorParameter<T extends IAnnotation>(annotation : T, target : any, index : number): T {
        let list = this.getOrCreateListForConstructorParameters(target);
        while (list.length < index)
            list.push(null);

        let paramList = list[index] || [];
        let clone = this.clone(annotation);
        paramList.push(clone);
        list[index] = paramList;

        if (Reflect.getOwnMetadata) {
            let parameterList = Reflect.getOwnMetadata('parameters', target) || [];
            
            while (parameterList.length < index)
                parameterList.push(null);

            let parameterAnnotes = parameterList[index] || [];
            parameterAnnotes.push(clone);
            parameterList[index] = parameterAnnotes;

            Reflect.defineMetadata('parameters', parameterList, target);
        }

        return clone;
    }

    /**
     * Clone the given Annotation instance into a new instance. This is not 
     * a deep copy.
     * 
     * @param annotation 
     */
    public static clone<T extends IAnnotation>(annotation : T): T {
        if (!annotation)
            return annotation;
        
        return Object.assign(Object.create(Object.getPrototypeOf(annotation)), annotation);
    }

    /**
     * Get all annotations (including from Angular and other compatible 
     * frameworks). 
     * 
     * @param target The target to fetch annotations for
     */
    public static getClassAnnotations(target : any): IAnnotation[] {
        return (this.getListForClass(target) || [])
            .map(x => this.clone(x));
    }

    /**
     * Get all annotations (including from Angular and other compatible 
     * frameworks). 
     * 
     * @param target The target to fetch annotations for
     */
    public static getMethodAnnotations(target : any, methodName : string, isStatic : boolean = false): IAnnotation[] {
        return (this.getListForMethod(isStatic ? target : target.prototype, methodName) || [])
            .map(x => this.clone(x));
    }

    /**
     * Get all annotations (including from Angular and other compatible 
     * frameworks). 
     * 
     * @param target The target to fetch annotations for
     */
    public static getPropertyAnnotations(target : any, methodName : string, isStatic : boolean = false): IAnnotation[] {
        return (this.getListForProperty(isStatic ? target : target.prototype, methodName) || [])
            .map(x => this.clone(x));
    }

    /**
     * Get the annotations defined on the parameters of the given method of the given 
     * class.
     * 
     * @param type 
     * @param methodName 
     * @param isStatic Whether `type` itself (isStatic = true), or `type.prototype` (isStatic = false) should be the target.
     *  Note that passing true may indicate that the passed `type` is already the prototype of a class.
     */
    public static getParameterAnnotations(type : any, methodName : string, isStatic : boolean = false): IAnnotation[][] {
        return (this.getListForMethodParameters(isStatic ? type : type.prototype, methodName) || [])
            .map(set => set ? set.map(anno => this.clone(anno)) : []);
    }

    /**
     * Get the annotations defined on the parameters of the given method of the given 
     * class.
     * 
     * @param type 
     * @param methodName 
     */
    public static getConstructorParameterAnnotations(type : any): IAnnotation[][] {
        return (this.getListForConstructorParameters(type) || [])
            .map(set => set ? set.map(anno => this.clone(anno)) : []);
    }

    /**
     * Get a list of annotations for the given class.
     * @param target 
     */
    private static getListForClass(target : Object): IAnnotation[] {
        if (!target)
            return [];
        
        let combinedSet = [];

        let superclass = Object.getPrototypeOf(target);

        if (superclass && superclass !== Function)
            combinedSet = combinedSet.concat(this.getListForClass(superclass));

        if (target.hasOwnProperty(ANNOTATIONS_KEY))
            combinedSet = combinedSet.concat(target[ANNOTATIONS_KEY] || []);

        return combinedSet;
    }

    /**
     * Get a list of own annotations for the given class, or create that list.
     * @param target 
     */
    private static getOrCreateListForClass(target : Object): IAnnotation[] {
        if (!target.hasOwnProperty(ANNOTATIONS_KEY))
            Object.defineProperty(target, ANNOTATIONS_KEY, { enumerable: false, value: [] });
        return target[ANNOTATIONS_KEY];
    }

    /**
     * Gets a map of the annotations defined on all properties of the given class/function. To get the annotations of instance fields,
     * make sure to use `Class.prototype`, otherwise static annotations are returned.
     */
    public static getMapForClassProperties(target : Object, mapToPopulate? : Record<string,IAnnotation[]>): Record<string,IAnnotation[]> {
        let combinedSet = mapToPopulate || {};
        if (!target || target === Function)
            return combinedSet;

        this.getMapForClassProperties(Object.getPrototypeOf(target), combinedSet);

        if (target.hasOwnProperty(PROPERTY_ANNOTATIONS_KEY)) {
            let ownMap : Record<string,IAnnotation[]> = target[PROPERTY_ANNOTATIONS_KEY] || {};
            for (let key of Object.keys(ownMap))
                combinedSet[key] = (combinedSet[key] || []).concat(ownMap[key]);
        }

        return combinedSet;
    }

    private static getOrCreateMapForClassProperties(target : Object): Record<string,IAnnotation[]> {
        if (!target.hasOwnProperty(PROPERTY_ANNOTATIONS_KEY))
            Object.defineProperty(target, PROPERTY_ANNOTATIONS_KEY, { enumerable: false, value: [] });
        return target[PROPERTY_ANNOTATIONS_KEY];
    }

    private static getListForProperty(target : any, propertyKey : string): IAnnotation[] {
        let map = this.getMapForClassProperties(target);

        if (!map)
            return null;
        
        return map[propertyKey];
    }

    private static getOrCreateListForProperty(target : any, propertyKey : string): IAnnotation[] {
        let map = this.getOrCreateMapForClassProperties(target);
        if (!map[propertyKey])
            map[propertyKey] = [];
        
        return map[propertyKey];
    }

    private static getOrCreateListForMethod(target : any, methodName : string): IAnnotation[] {
        return this.getOrCreateListForProperty(target, methodName);
    }

    private static getListForMethod(target : any, methodName : string): IAnnotation[] {
        return this.getListForProperty(target, methodName);
    }

    /**
     * Get a map of the annotations defined on all parameters of all methods of the given class/function.
     * To get instance methods, make sure to pass `Class.prototype`, otherwise the results are for static fields.
     */
    public static getMapForMethodParameters(target : Object, mapToPopulate? : Record<string,IAnnotation[][]>): Record<string,IAnnotation[][]> {
        let combinedMap = mapToPopulate || {};

        if (!target || target === Function)
            return combinedMap;

        // superclass/prototype
        this.getMapForMethodParameters(Object.getPrototypeOf(target), combinedMap);
        
        if (target.hasOwnProperty(METHOD_PARAMETER_ANNOTATIONS_KEY)) {
            let ownMap : Record<string,IAnnotation[][]> = target[METHOD_PARAMETER_ANNOTATIONS_KEY] || {};

            for (let methodName of Object.keys(ownMap)) {
                let parameters = ownMap[methodName];
                let combinedMethodMap = combinedMap[methodName] || [];

                for (let i = 0, max = parameters.length; i < max; ++i) {
                    combinedMethodMap[i] = (combinedMethodMap[i] || []).concat(parameters[i] || []);
                }

                combinedMap[methodName] = combinedMethodMap;
            }
        }

        return combinedMap;
    }

    private static getOrCreateMapForMethodParameters(target : Object): Record<string, IAnnotation[][]> {
        if (!target.hasOwnProperty(METHOD_PARAMETER_ANNOTATIONS_KEY))
            Object.defineProperty(target, METHOD_PARAMETER_ANNOTATIONS_KEY, { enumerable: false, value: {} });
        return target[METHOD_PARAMETER_ANNOTATIONS_KEY];
    }

    private static getListForMethodParameters(target : any, methodName : string): IAnnotation[][] {
        let map = this.getMapForMethodParameters(target);

        if (!map)
            return null;

        return map[methodName];
    }

    private static getOrCreateListForMethodParameters(target : any, methodName : string): IAnnotation[][] {
        let map = this.getOrCreateMapForMethodParameters(target);
        if (!map[methodName])
            map[methodName] = [];

        return map[methodName];
    }

    private static getOrCreateListForConstructorParameters(target : any): IAnnotation[][] {
        if (!target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY])
            Object.defineProperty(target, CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY, { enumerable: false, value: [] });
        return target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
    }

    private static getListForConstructorParameters(target : any): IAnnotation[][] {
        return target[CONSTRUCTOR_PARAMETERS_ANNOTATIONS_KEY];
    }
}

/**
 * An annotation for attaching a label to a programmatic element. 
 * Can be queried with LabelAnnotation.getForClass() for example.
 */
@MetadataName('alterior:Label')
export class LabelAnnotation extends Annotation {
    constructor(readonly text : string) {
        super();
    }
}

export const Label = LabelAnnotation.decorator();
