'use strict';

import * as Joi from 'joi';
import {pluralize} from 'inflection';
import {isUndefined} from 'util';
import {ModelPropertyMetadata} from './ModelPropertyMetadata';
import {ModelRelationMetadata} from './ModelRelationMetadata';
import {
  IBaseModelConstructor, 
  IModelMetadataOptions,
  IModelMetadata, 
  IModelPropertyMetadata,
  IModelRelationMetadata,
  IPersistedModelConstructor,
  IModelPropertyMetadataOptions,
  IModelRelationMetadataOptions
} from './interfaces';

const DEFAULT_CONNECTION_NAME = 'default';
const ASYNC_VALIDATORS = ['custom'];

export class ModelMetadata implements IModelMetadata {
  
  protected _name: string;
  protected _pluralName: string;
  protected _autoId: boolean;
  protected _connectionName: string;
  protected _properties: Map<string, IModelPropertyMetadata>;
  protected _relations: Map<string, IModelRelationMetadata>;
  protected _model: IBaseModelConstructor;
  protected _raw: any;
  
  constructor(options: IModelMetadataOptions, properties?: {[key: string]: IModelPropertyMetadataOptions}, relations?: {[key: string]: IModelRelationMetadataOptions}) {
    this._raw = {
      options,
      properties,
      relations
    }
    this._name = options.name;
    this._pluralName = isUndefined(options.pluralName) ? pluralize(options.name) : options.pluralName;
    this._autoId = isUndefined(options.autoId) ? true : options.autoId;
    this._properties = new Map();
    this._relations = new Map();
    this._connectionName = isUndefined(options.connection) ? DEFAULT_CONNECTION_NAME : options.connection;        
  }
  
  get name(): string {
    return this._name;
  }
  
  get pluralName(): string {
    return this._pluralName;
  }
  
  get autoId(): boolean {
    return this._autoId;
  }
  
  get properties(): IterableIterator<IModelPropertyMetadata> {
    return this._properties.values();
  }
  
  get relations(): IterableIterator<IModelRelationMetadata> {
    return this._relations.values();
  }
  
  get connectionName(): string {
    return this._connectionName;
  }
  
  get idProperty(): IModelPropertyMetadata {
    for(const property of this.properties) {
      if(property.id) {
        return property;
      }
    }    
  }
  
  get validationSchema(): {[key: string]: any} {
    const schema = {};
    for(const property of this.properties) {
      schema[property.name] = Joi;
      for(const validation of property.validations) {
        if(ASYNC_VALIDATORS.indexOf(validation[0]) === -1 && schema[property.name][validation[0]]) {        
          schema[property.name] = schema[property.name][validation[0]](validation[1]);
        }
      }
    }
    return schema;
  }
  
  get asyncValidationSchema(): {[key: string]: any} {
    const schema = {};
    for(const property of this.properties) {
      schema[property.name] = [];
      for(const validation of property.validations) {
        if(ASYNC_VALIDATORS.indexOf(validation[0]) !== -1) {
          schema[property.name].push(validation[1]);
        }
      }
    }
    return schema;
  }
  
  defineProperty(options: IModelPropertyMetadataOptions): void {
    if(options.id) {
      this._autoId = false;
    }
    const property = new ModelPropertyMetadata(options);
    this._properties.set(options.name, property);
    
    if(this._model) {
      property.apply(this._model);
    }
  }
  
  buildPropertyId(name: string, type: any): void {
    if(this.autoId && !this.idProperty) {
      this.defineProperty({
        name: name,
        type: type,
        id: true
      });      
    }
  }
  
  getProperty(name: string): IModelPropertyMetadata {
    return this._properties.get(name);
  }
  
  hasProperty(name: string): boolean {
    return this._properties.has(name);
  }
  
  isAllowedProperty(name: string): boolean {
    return this.hasProperty(name);
  }
  
  defineRelation(options: IModelRelationMetadataOptions): void {
    const relation = new ModelRelationMetadata(options);
    this._relations.set(relation.name, relation);
    
    if(this._model) {
      relation.apply(<IPersistedModelConstructor>this._model);
    }
  }
  
  apply(model): void {
    
    if(!isUndefined(this._raw.properties)) {
      Object.keys(this._raw.properties).forEach(propertyName => {
        this.defineProperty(Object.assign({name: propertyName}, this._raw.properties[propertyName]));
      });
    }
    
    if(!isUndefined(this._raw.relations)) {
      Object.keys(this._raw.relations).forEach(relationName => {
        this.defineRelation(Object.assign({name: relationName}, this._raw.relations[relationName]));
      });
    }
        
    this._model = model;  
    
    for(const property of this.properties) {
      property.apply(model);
    }
    
    for(const relation of this.relations) {
      relation.apply(model);
    }    
  }

}