/*
 * 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 {IValidator, ValidationResult} from "./index";
import {keys} from "memory-cache";

type StringRule =
    | { type: "equal", value: string }
    | { type: "notEqual", value: string }
    | { type: "minLength", min: number }
    | { type: "maxLength", max: number }

export class StringValidator implements IValidator<string> {
    constructor(private rules?: StringRule[]) {
        if (!Array.isArray(this.rules)) {
            this.rules = [];
        }
    }

    /**
     * Adds a rule to the array of rules, or replaces a rule if it already exists.
     * Use this function to prevent having multiple rules of the same type.
     */
    private addRule: (rule: StringRule) => StringRule[] = rule => {
        // Filter the current rule set, removing any rule that has the same type of the one being added
        const filtered = this.rules.filter(r => r.type !== rule.type);

        // Add the new rule to the filtered rule array
        return [...filtered, rule]
    }

    /**
     * Fails if the value being validated is not equal to @param value.
     */
    equals: (value: string) => StringValidator = value => {
        this.rules = this.addRule({ type: "equal", value: value });
        return this;
    }

    /**
     * Fails if the value being validated is equal to @param value.
     */
    notEquals: (value: string) => StringValidator = value => {
        this.rules = this.addRule({ type: "notEqual", value: value });
        return this;
    }

    /**
     * Fails if the string's length is less than @param min.
     */
    minLength: (min: number) => StringValidator = min => {
        this.rules = this.addRule({ type: "minLength", min: min });
        return this;
    }

    /**
     * Fails if the string's length is greater than @param max.
     */
    maxLength: (max: number) => StringValidator = max => {
        this.rules = this.addRule({ type: "maxLength", max: max });
        return this;
    }

    /**
     * Fails if the string is empty.
     */
    notEmpty: () => StringValidator = () => {
        // We don't need to use a specific rule for notEmpty here, we can just set a min length of 1!
        this.rules = this.addRule({ type: "minLength", min: 1 });
        return this;
    }

    /**
     * Fails if the string is not empty. NOTE that an empty string is _not_ the same as a null or undefined value.
     */
    empty: () => StringValidator = () => {
        // Again, we don't need a specific rule for empty, we just set a max length of 0
        this.rules = this.addRule({ type: "maxLength", max: 0 });
        return this;
    }

    /**
     * Checks an individual rule against the value being validated.
     */
    checkRule: (rule: StringRule, value: string, key: string) => ValidationResult<string> = (rule: StringRule, value : string, key: string) => {
        const err = (msg: string) : ValidationResult<string> => ({ success: false, message: msg });
        const ok = () : ValidationResult<string> => ({ success: true, value: value });

        switch (rule.type) {
            case "equal":
                return rule.value !== value
                    ? err(`${key} was expected to be ${rule.value} but was ${value}.`)
                    : ok();

            case "notEqual":
                return rule.value === value
                    ? err(`${key} must not be ${rule.value}.`)
                    : ok();

            case "minLength":
                return value.length < rule.min
                    ? err(`${key} length must be greater than or equal to ${rule.min} but was ${value.length}.`)
                    : ok();

            case "maxLength":
                return value.length > rule.max
                    ? err(`${key} length must be less than or equal to ${rule.max} but was ${value.length}.`)
                    : ok();
        }
    }

    go: (value: unknown, key?: string) => ValidationResult<string> = (value, key) => {
        // Since the value is unknown, we must check that the type is string before validating each rule

        key = key ?? 'value';

        if (value === null) {
            return {
                success: false,
                message: "expected "+key+" to be string but received null."
            }
        } else if (value === undefined) {
            return {
                success: false,
                message: "expected "+key+" to be string but received undefined."
            }
        } else if (typeof value !== "string") {
            return {
                success: false,
                message: `expected ${key} to be string but received ${typeof value}.`
            }
        }

        // TypeScript compiler now knows that value is a string
        // Iterate over all rules and short-circuit to return an error if any rule fails
        for (let rule of this.rules) {
            const result = this.checkRule(rule, value, key);

            if (result.success === false) {
                return result;
            }
        }

        // If none of the rules in the loop had an error, the value passed validation!
        return {
            success: true,
            value: value
        }
    }
}
