import {IMetadataProvider, MetadataType} from '@essential-projects/metadata_contracts';
import {Logger} from 'loggerhythm';

import * as merge from 'deepmerge';
import 'reflect-metadata';

const logger: Logger = new Logger('MetadataProvider');

const GLOBAL_METADATA_STORE_KEY: string = '$metadata';

declare var global: any;
declare var window: any;
let globalVariable: any;
if (typeof global !== 'undefined') {
  globalVariable = global;
} else {
  globalVariable = window;
}

function getGlobalMetadataStore(): any {
  if (!globalVariable[GLOBAL_METADATA_STORE_KEY]) {
    globalVariable[GLOBAL_METADATA_STORE_KEY] = {};
  }

  return globalVariable[GLOBAL_METADATA_STORE_KEY];
}

if (typeof globalVariable.existingMetadataProvider === 'undefined') {
  globalVariable.existingMetadataProvider = {

    getForInstance(metadataKey: MetadataType, namespace: string, target: any, targetKey?: string): any {
      if (!isObject(target)) {
        return undefined;
      }

      let result: any = MetadataProvider.getForOwnInstance(metadataKey, namespace, target, targetKey);

      if (result === undefined) {
        result = MetadataProvider.getForInstance(metadataKey, Object.getPrototypeOf(target), targetKey);
      }

      return result;
    },

    getForOwnInstance(metadataKey: MetadataType, namespace: string, target: any, targetKey?: string): any {
      if (!isObject(target)) {
        return undefined;
      }

      const type: string = target.constructor.name;
      let metadataForType: any = {};

      // fetching the metadata for Object would result in a stack overflow
      if (type !== 'Object') {
        metadataForType = MetadataProvider.getForType(metadataKey, namespace, type, targetKey);
      }

      const ownMetadata: any = Reflect.getOwnMetadata(metadataKey, target, targetKey);

      let result: any;

      if (metadataForType === undefined) {

        result = ownMetadata;

      } else if (ownMetadata === undefined) {

        result = metadataForType;

      } else {

        result = merge(metadataForType, ownMetadata);
      }

      return result;
    },

    getForType(metadataKey: MetadataType, namespace: string, type: string, targetKey?: string): any {

      const allTypeMetadata: any = getGlobalMetadataStore();

      const namespaceMetadata: any = allTypeMetadata[namespace];

      if (!namespaceMetadata) {
        return undefined;
      }

      const typeMetadata: any = namespaceMetadata[type];

      if (!typeMetadata) {
        return undefined;
      }

      const typeMetadataForKey: any = typeMetadata[metadataKey];

      if (!typeMetadataForKey) {
        return undefined;
      }

      const typeMetadataForProperty: any = typeMetadataForKey[targetKey];

      return typeMetadataForProperty;
    },

    getAllForType(metadataKey: MetadataType, namespace: string, type: string): any {

      const allTypeMetadata: any = getGlobalMetadataStore();

      const namespaceMetadata: any = allTypeMetadata[namespace];

      if (!namespaceMetadata) {
        return undefined;
      }

      const typeMetadata: any = namespaceMetadata[type];

      if (!typeMetadata) {
        return undefined;
      }

      const typeMetadataForKey: any = typeMetadata[metadataKey];

      return typeMetadataForKey;
    },

    setForInstance(metadataKey: MetadataType, metadataValue: any, target: any, targetKey?: string): void {
      Reflect.defineMetadata(metadataKey, metadataValue, target, targetKey);
    },

    hasType(namespace: string, type: string): boolean {
      const allTypeMetadata: any = getGlobalMetadataStore();

      return allTypeMetadata[namespace][type] !== undefined;
    },

    setForType(metadataKey: MetadataType, metadataValue: any, namespace: string, type: string, targetKey?: string): void {

      const allTypeMetadata: any = getGlobalMetadataStore();

      if (!allTypeMetadata[namespace]) {
        allTypeMetadata[namespace] = {};
      }
      if (!allTypeMetadata[namespace][type]) {
        allTypeMetadata[namespace][type] = {};
      }
      if (!allTypeMetadata[namespace][type][metadataKey]) {
        allTypeMetadata[namespace][type][metadataKey] = {};
      }

      if (metadataValue && !allTypeMetadata[namespace][type][metadataKey][targetKey]) {
        allTypeMetadata[namespace][type][metadataKey][targetKey] = {};
      }

      allTypeMetadata[namespace][type][metadataKey][targetKey] = merge(metadataValue, allTypeMetadata[namespace][type][metadataKey][targetKey]);
    },
  };

}

function isObject(value: any): boolean {
  return value && (typeof value === 'function' || typeof value === 'object');
}

export const MetadataProvider: IMetadataProvider = globalVariable.existingMetadataProvider;
