import Types from './types';

/**
 * @public
 * @class ConfigField
 */
class ConfigField {
  /**
   * @param {Object} config - ConfigField attributes.
   * @param {string} config.name - Name of this field.
   * @param {string} config.comment - Comment for this field.
   * @param {string} config.sample - Sample of value.
   * @param {ConfigFieldBaseType} config.type - Type of field.
   * @param {any} config.default - Default value for this field.
   * @param {boolean=} config.required - Is this field required. If "default" field is undefined, default value of "required" field will be 'true'.
   * @param {function(value:string).<any>[]} config.preTransforms - Array of transformation for value, apply before validation.
   * @param {{ pre: function(value:string).<any>[], post: function(value:string).<any>[] }} config.transforms - Transforms for value.
   * @param {function(value:string).<any>[]} config.postTransforms - Array of transformation for value, apply after validation.
   * @param {function(value: any).<boolean>[]} config.validators - Array of validators for value.
   * @param {function(value: any, key: string, env: Map.<string, string>).<(Map.<string, *>|{})>} config.splitter - Array of splitters functions.
   */
  constructor(config) {
    /**
     * @type {string}
     */
    this.name = config.name;
    /**
     * @type {string}
     */
    this.comment = config.comment || '';
    /**
     * @type {string}
     */
    this.sample = config.sample || config.default || '';

    /**
     * @type {*}
     */
    this.default = config.default;

    // if 'default' value is undefined, by default required flag set to true
    /**
     * @type {boolean}
     */
    this.required = ((this.default === null || this.default === undefined) &&
    (config.required === null || config.required === undefined || config.required === true));

    // eslint-disable-next-line import/no-named-as-default-member
    if (config.type instanceof Types.ConfigFieldBaseType) {
      /**
       * @type {ConfigFieldBaseType}
       */
      this.type = config.type;
    } else if (typeof config.type === 'string') {
      const typeName = `${config.type[0].toUpperCase()}${config.type.toLowerCase().slice(1)}Type`;
      const CurrentType = Types[typeName];

      if (!CurrentType) {
        throw new TypeError(`Can't find type ${config.type} (as ${typeName})`);
      }

      this.type = new CurrentType(config);
    } else {
      throw new Error('Type of field must be string or object extend from base ConfigFieldType');
    }


    /**
     * @type {{ pre: function[], post: function[] }}
     */
    this.transformators = {
      pre: [...this.type.transformators.pre],
      post: [...this.type.transformators.post],
    };

    if (config.transforms) {
      if (config.transforms.pre && config.transforms.pre instanceof Array) {
        this.transformators.pre.push(...config.transforms.pre);
      }
      if (config.transforms.post && config.transforms.post instanceof Array) {
        this.transformators.post.push(...config.transforms.post);
      }
    }

    if (config.preTransforms && config.preTransforms instanceof Array) {
      this.transformators.pre.push(...config.preTransforms);
    }

    if (config.postTransforms && config.postTransforms instanceof Array) {
      this.transformators.pre.push(...config.postTransforms);
    }

    /**
     * @type {function.<boolean>[]}
     */
    this.validators = [...this.type.validators, ...(config.validators || [])];

    /**
     * @type {function(value:*, key:string, env:Map.<string, string>).<(Map.<string, *>|{})>}
     */
    this.splitterFunction = config.splitter;
  }

  /**
   * @function validate
   * @param {any=} value - Value to validation and transformations.
   * @returns {any} - Value after transformations.
   */
  validate(value) {
    let newValue;

    if ((value === undefined || value === null)) {
      if (this.required) {
        throw new Error('This field is required');
      }

      newValue = this.default;
    }

    if (!(value === undefined || value === null)) {
      try {
        newValue = this.applyPreTransforms(value);
      } catch (err) {
        const newError = new Error('Can\'t apply pre-transform to value');
        newError.error = err;
        throw newError;
      }
    }

    this.applyValidators(newValue);

    try {
      newValue = this.applyPostTransforms(newValue);
    } catch (err) {
      const newError = new Error('Can\'t apply post-transforms to value');
      newError.error = err;
      throw newError;
    }

    return newValue;
  }

  /**
   * @function splitter
   * @param {*} value - Value of field.
   * @param {string} key - Name of field.
   * @param {Map.<string, string>} env - Map with environment variables.
   * @returns {Map.<string, *>} - Map of new values.
   */
  splitter(value, key, env) {
    if (!this.splitterFunction) {
      return new Map([[key, value]]);
    }

    const result = this.splitterFunction(value, key, env);

    if (result instanceof Map) {
      return result;
    }

    const validResult = (result instanceof Map || typeof result === 'object');
    if (!result || !validResult) {
      return new Map([[key, value]]);
    }

    return new Map(Object.entries(result));
  }

  /**
   * @function applyPreTransforms
   * @param {any} value - Value for transformation.
   * @returns {any} - Value after transformation.
   */
  applyPreTransforms(value) {
    return this.transformators.pre.reduce((val, fn) => fn(val), value);
  }

  /**
   * @function applyValidators
   * @param {any} value - Value for validation.
   * @returns {boolean} - Value after validation.
   */
  applyValidators(value) {
    return this.validators.every(fn => fn(value));
  }

  /**
   * @function applyPostTransforms
   * @param {any} value - Value for transformation.
   * @returns {any} - Value after transformation.
   */
  applyPostTransforms(value) {
    return this.transformators.post.reduce((val, fn) => fn(val), value);
  }
}

export default ConfigField;

@@ -1,20 +1,22 @@
 import 
+get
 Type
-s
  from '.
@@ -20,13 +20,22 @@
  './
+utils/get-
 type
-s
 ';%0A%0A
@@ -1870,144 +1870,16 @@
 );%0A%0A
-    // eslint-disable-next-line import/no-named-as-default-member%0A    if (config.type instanceof Types.ConfigFieldBaseType) %7B%0A  
     /**%0A
     
@@ -1874,18 +1874,16 @@
     /**%0A
-  
      * @
@@ -1909,18 +1909,16 @@
 seType%7D%0A
-  
      */%0A
@@ -1917,26 +1917,24 @@
      */%0A    
-  
 this.type = 
@@ -1937,489 +1937,24 @@
 e = 
-config.type;%0A    %7D else if (typeof config.type === 'string') %7B%0A      const typeName = %60$%7Bconfig.type%5B0%5D.toUpperCase()%7D$%7Bconfig.type.toLowerCase().slice(1)%7DType%60;%0A      const CurrentType = Types%5BtypeName%5D;%0A%0A      if (!CurrentType) %7B%0A        throw new TypeError(%60Can't find type $%7Bconfig.type%7D (as $%7BtypeName%7D)%60);%0A      %7D%0A%0A      this.type = new CurrentType(config);%0A    %7D else %7B%0A      throw new Error('Type of field must be string or object extend from base ConfigFieldType');%0A    %7D%0A
+getType(config);
 %0A%0A  

Connect and use utils/get-type, so add object-defined type.