/*!
 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
// import Ajv from 'ajv';
import cloneDeep from 'lodash/cloneDeep.js';
import JSONPointer from './JSONPointer.js';

export default class JSONSchema {
    setSchema(schema) {
        this._schema = schema;
        this._fragmentsCache = new Map();
        // this._ajv = new Ajv({
        // 	useDefaults: true,
        // 	coerceTypes: true
        // });
        // this._validate = this._ajv.compile(schema);
    }

    processData(data) {
        data = cloneDeep(data);
        // this._validate(data);
        return data;
    }

    // array of names of validation functions
    setSupportedValidationFunctions(supportedValidationFunctions) {
        this._supportedValidationFunctions = supportedValidationFunctions;
    }

    /* FYI - I was going to support "required" but then found out that server never sends it in its schema (it was a business decision. so leaving the code commented for now */
    flattenSchema(supportedValidationFunctions) {
        if (supportedValidationFunctions) {
            this.setSupportedValidationFunctions(supportedValidationFunctions);
        }
        let genericFieldInfo = {};
        if (this._schema && this._schema.properties) {
            this.travelProperties(
                this._schema.properties,
                genericFieldInfo /*, this._schema.required*/
            );
        }
        return { genericFieldInfo };
    }

    extractGenericFieldInfo(item) {
        let validationsArr = [];
        let additionalInfo = { isValid: true, errorText: '' };
        for (let value in item) {
            if (this._supportedValidationFunctions.includes(value)) {
                let validationItem = this.extractValidations(item, value);
                validationsArr[validationsArr.length] = validationItem;
            } else {
                let enumResult = this.extractEnum(item, value);
                if (enumResult !== null) {
                    additionalInfo.enum = enumResult;
                } else {
                    additionalInfo[value] = item[value];
                }
                /*if (required.includes (property)) {
				 additionalInfo[value].isRequired = true ;
				 }*/
            }
        }

        additionalInfo.validations = validationsArr;
        return additionalInfo;
    }

    extractValidations(item, value) {
        let validationItem;
        let data = item[value];
        if (value === 'maximum') {
            if (item.exclusiveMaximum) {
                value = 'maximumExclusive';
            }
        }
        if (value === 'minimum') {
            if (item.exclusiveMinimum) {
                value = 'minimumExclusive';
            }
        }
        validationItem = { type: value, data: data };
        return validationItem;
    }

    extractEnum(item, value) {
        let enumResult = null;
        if (value === 'type' && item[value] === 'array') {
            let items = item.items;
            if (items && items.enum && items.enum.length > 0) {
                let values = items.enum
                    .filter(value => value)
                    .map(value => ({ enum: value, title: value }));
                enumResult = values;
            }
        } else if (value === 'enum') {
            let items = item[value];
            if (items && items.length > 0) {
                let values = items
                    .filter(value => value)
                    .map(value => ({ enum: value, title: value.toString() }));
                enumResult = values;
            }
        }
        return enumResult;
    }

    travelProperties(
        properties,
        genericFieldDefs,
        /*required = [],*/ pointer = ''
    ) {
        let newPointer = pointer;
        for (let property in properties) {
            newPointer = newPointer ? newPointer + '/' + property : property;
            if (properties[property].properties) {
                this.travelProperties(
                    properties[property].properties,
                    genericFieldDefs /*, properties[property].required*/,
                    newPointer
                );
            } else if (properties[property].$ref) {
                let fragment = this._getSchemaFragmentByRef(
                    properties[property].$ref
                );
                if (fragment.properties) {
                    this.travelProperties(
                        fragment.properties,
                        genericFieldDefs /*, properties[property].required*/,
                        newPointer
                    );
                } else {
                    genericFieldDefs[newPointer] = this.extractGenericFieldInfo(
                        fragment.properties
                    );
                }
            } else {
                genericFieldDefs[newPointer] = this.extractGenericFieldInfo(
                    properties[property]
                );
            }
            newPointer = pointer;
        }
    }

    getTitle(pointer) {
        return this._getSchemaFragment(pointer).title;
    }

    exists(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return !!fragment;
    }

    getDefault(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.default;
    }

    getEnum(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return (
            fragment &&
            (fragment.type === 'array' ? fragment.items.enum : fragment.enum)
        );
    }

    isRequired(pointer) {
        const parentPointer = JSONPointer.extractParentPointer(pointer);
        const lastPart = JSONPointer.extractLastPart(pointer);
        let parentFragment = this._getSchemaFragment(parentPointer);
        return (
            parentFragment &&
            parentFragment.required &&
            parentFragment.required.includes(lastPart)
        );
    }

    isNumber(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.type === 'number';
    }

    getMaxValue(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.exclusiveMaximum
            ? fragment.maximum - 1
            : fragment.maximum;
    }

    getMinValue(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.exclusiveMinimum
            ? fragment.minimum
            : fragment.minimum;
    }

    isString(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.type === 'string';
    }

    getPattern(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.pattern;
    }

    getMaxLength(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.maxLength;
    }

    getMinLength(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.minLength;
    }

    isArray(pointer) {
        const fragment = this._getSchemaFragment(pointer);
        return fragment && fragment.type === 'array';
    }

    _getSchemaFragment(pointer) {
        if (this._fragmentsCache.has(pointer)) {
            return this._fragmentsCache.get(pointer);
        }

        let parts = JSONPointer.extractParts(pointer);

        let fragment = parts.reduce((fragment, part) => {
            if (fragment === undefined) {
                return undefined;
            }

            if (fragment.$ref) {
                fragment = this._getSchemaFragmentByRef(fragment.$ref);
            }

            switch (fragment.type) {
                case 'object':
                    return fragment.properties && fragment.properties[part];

                case 'array':
                    return fragment.enum && fragment.enum[part];

                default:
                    // throw new Error(`Incorrect/unsupported JSONPointer "${pointer}" from "${part}"`);
                    return undefined;
            }
        }, this._schema);

        while (fragment && fragment.$ref) {
            fragment = this._getSchemaFragmentByRef(fragment.$ref);
        }

        this._fragmentsCache.set(pointer, fragment);
        return fragment;
    }

    _getSchemaFragmentByRef($ref) {
        let pointer = $ref.substr(1);
        return JSONPointer.getValue(this._schema, pointer);
        // let fragmentAjv = new Ajv();
        // fragmentAjv.addSchema(this._schema);
        // let compiledFragment = fragmentAjv.compile({$ref});
        // let fragment = compiledFragment.refVal[compiledFragment.refs[$ref]];
        // return fragment;
    }
}