/*
 * Copyright (c) 2022.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import {isValidator, IValidator, ValidationResult, Shape, PropertyValidator} from "./index";

export class ShapeValidator<T extends object> implements IValidator<T> {
    constructor(private shape: Shape<T>) { }

    go(value: unknown, key?: string) : ValidationResult<T> {
        const err: (msg: string) => ValidationResult<any> = msg => ({ success: false, message: msg });

        // First check that the value is an object and not an array, null, undefined, etc
        if (value === null || value === undefined) {
            return err((key ?? 'value') + " cannot be null or undefined.");
        } else if (Array.isArray(value)) {
            return err((key ?? 'value') + " must be an object but was an array.");
        } else if (typeof value !== "object") {
            return err((key ?? 'value') + ` must be an object but was ${typeof value}.`);
        }

        // Get the keys of both the expected shape, and the value
        const expectedKeys = Object.getOwnPropertyNames(this.shape);
        const actualKeys = Object.keys(value);

        // Check if any expected property is missing from the value
        for (let expected of expectedKeys) {
            if (actualKeys.indexOf(expected) === -1) {
                //return err(`Value is missing expected property ${expected}.`);
            }
        }

        // All properties are accounted for! Now loop through each validator on the expected shape and test the value
        for (let expected of expectedKeys) {
            const result : ValidationResult<T> = this.validateShape(expected as keyof T, value, key);
            if (result.success === false) return result;
        }

        // All validation passed!
        return {
            success: true,
            value: value as T
        }
    }

    private validateShape(key: keyof T, value: unknown, parentKey?: string) : ValidationResult<T> {
        const validator = this.shape[key];
        const propertyValue: unknown = value[key];

        let propertyKey = '';
        if(typeof parentKey !== 'undefined') propertyKey += parentKey+'.';
        propertyKey += key;

        return this.evaluateValidator(validator, propertyValue, propertyKey);
    }

    protected evaluateValidator(propertyValidator: PropertyValidator<any>, propertyValue: unknown, propertyKey: string)  : ValidationResult<T> {
        const result : ValidationResult<T> | IValidator<T> | boolean =
            isValidator(propertyValidator) ?
                propertyValidator.go(propertyValue, propertyKey) :
                propertyValidator(propertyValue, propertyKey);

        if(typeof result === 'boolean') {
            if(!result) return {success: false, message: propertyKey + ' is not valid.'};
        } else {
            if(isValidator(result)) {
                return this.evaluateValidator(result, propertyValue, propertyKey);
            } else {
                if (result.success === false) return result;
            }
        }

        return {success: true, value: propertyValue as T};
    }
}
