/*
 * form-creator-api. RESTful API to manage and answer forms.
 * Copyright (C) 2019 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR
 *
 * This file is part of form-creator-api.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import { FormAnswer } from "../core/formAnswer";
import { Input } from "../core/input";
import { InputAnswer, InputAnswerDict } from "../core/inputAnswer";
import { ValidationType } from "./enumHandler";
import { ValidationDict, ValidationError } from "./validationError";

/**
 * Validation's handler. Manage parse validation through the project.
 */
export class ValidationHandler {

    /**
     * Validate a string according given a regex.
     * @param answer - Answer to be validated.
     * @param regex - Regex to validate answer.
     * @returns - True if answer match regex, else false.
     */
    private static validateByRegex(answer: string, regex: string): boolean {
        const regexp = new RegExp(regex);
        return regexp.test(answer);
    }

    /**
     * Validate if is null, undefined nor "".
     * @param answer - answer to be validated.
     * @returns - True if not null, "" nor undefined, else false.
     */
    private static validateMandatory(answer: string): boolean {
        return ((!answer) === false);
    }

    /**
     * Validate if answer has minimum number of chars.
     * @param answer - Answer to be validated.
     * @param size - Minimum size that answer should have.
     * @returns - True if has at least Size chars, else false.
     */
    private static validateMinChar(answer: string, size: string): boolean {
        return (answer !== null && answer !== undefined && parseInt(size, 10) <= answer.length);
    }

    /**
     * Validate if answer has minimum number of chars.
     * @param answer - Answer to be validated.
     * @param size - Maximum size that answer should have.
     * @returns - True if has at max Size chars, else false.
     */
    private static validateMaxChar(answer: string, size: string): boolean {
        return (answer !== null && answer !== undefined && parseInt(size, 10) >= answer.length);
    }

    /**
     * Validate if answer is of a determined type.
     * @param answer - Answer to be validated.
     * @param type - Type that answer should be.
     * @returns - True if it is of the determined type, else false.
     */
    private static validateTypeOf(answer: string, type: string): boolean {
        // Using string here to avoid validate validations
        if (type === "int") {
            return (!isNaN(parseInt(answer, 10)));
        } else if (type === "float") {
            return (!isNaN(parseFloat(answer)));
        } else if (type === "date") {
            return ((new Date(answer)).toString() !== "Invalid Date");
        } else {
            return (false);
        }
    }

    /**
     * Validate if answer has minimum one checkbox checked.
     * @param input - Input that checkbox belongs to.
     * @param inputAnswer - Answers to checkbox.
     * @returns - true if has at minimum one checkbox marked, else false.
     */
    private static validateSomeCheckbox(input: Input, inputAnswers: InputAnswerDict): boolean {
        let result: boolean = false;
        for (const answer of inputAnswers[input.id]) {
            if ((answer.value === "true") && this.inputSugestionExists(input, answer.placement)) {
                result = true;
            }
        }
        return result;
    }

    /**
     * Validate if a sugestion exists.
     * @param input - Input that have sugestions to be verified.
     * @param placement - Value of answer to be verified.
     * @returns - True if sugestion exists, else false.
     */
    private static inputSugestionExists(input: Input, placement: number): boolean {
        let result: boolean = false;
        for (const sugestion of input.sugestions) {
            if (sugestion.placement === placement) {
                result = true;
            }
        }
        return result;
    }

    /**
     * Validate if a input has a minimum number of answers.
     * @param inputAnswers - Dictionary of InputAnswers to be verified.
     * @param id - Input to be searched.
     * @param argument - Max number of answers.
     * @returns - True if has minimum answers, else false.
     */
    private static validateMaxAnswers(inputAnswers: InputAnswerDict, id: number, argument: string): boolean {
        const max: number = parseInt(argument, 10);
        // Verify if argument is an integer
        if (!(isNaN(max))) {
            return (inputAnswers[id].length <= max);
        } else {
            return false;
        }
    }

    /**
     * Validate if exists a answer for a dependent input.
     * @param inputAnswers - Dictionary of InputAnswers to be verified.
     * @param argument - Placement of the dependent input.
     * @returns - True if the input was answered, else false.
     */
    private static validateDependency(inputAnswers: InputAnswer[], argument: string): boolean {
        let result: boolean = false;
        const placement: number = parseInt(argument, 10);
        if (!(isNaN(placement))) {
            for (const inputAnswer of inputAnswers) {
                if (inputAnswer.placement === placement) {
                    result = (inputAnswer.value === "true");
                }
            }
        }
        return result;
    }

    /**
     * Validate if answer has minimum number of chars.
     * @param input - Input to validate answer.
     * @param answer - Answer of input.
     * @returns - A string with all errors.
     */
    private static validateInput(input: Input, inputAnswers: InputAnswerDict): string {
        const errors: string[] = [];

        let inputMandatory: boolean = false;

        for (const val of input.validation) {
            if (val.type === ValidationType.MANDATORY) {
                inputMandatory = true;
                break;
            }
        }

        if ((inputAnswers[input.id] === undefined || inputAnswers[input.id][0].value === "") && !(inputMandatory)) {
            return;
        }

        for (const validation of input.validation) {

            switch (validation.type) {

                case ValidationType.REGEX:
                    for (const answer of inputAnswers[input.id]) {
                        if (!this.validateByRegex(answer.value, validation.arguments[0])) {
                            errors.push("RegEx do not match");
                        }
                    }
                    break;

                case ValidationType.MANDATORY:
                    for (const answer of inputAnswers[input.id]) {
                        if (!(this.validateMandatory(answer.value))) {
                            errors.push("Input answer is mandatory");
                        }
                    }
                    break;

                case ValidationType.MAXCHAR:
                    for (const answer of inputAnswers[input.id]) {
                        if (!(this.validateMaxChar(answer.value, validation.arguments[0]))) {
                            errors.push("Input answer must be lower than " + validation.arguments[0]);
                        }
                    }
                    break;

                case ValidationType.MINCHAR:
                    for (const answer of inputAnswers[input.id]) {
                        if (!(this.validateMinChar(answer.value, validation.arguments[0]))) {
                            errors.push("Input answer must be greater than " + validation.arguments[0]);
                        }
                    }
                    break;

                case ValidationType.TYPEOF:
                    for (const answer of inputAnswers[input.id]) {
                        if (!(this.validateTypeOf(answer.value, validation.arguments[0]))) {
                            errors.push("Input answer must be a " + validation.arguments[0]) + " type";
                        }
                    }
                    break;

                case ValidationType.SOMECHECKBOX:
                    if (!(this.validateSomeCheckbox(input, inputAnswers))) {
                        errors.push("Input answer must have a answer");
                    }
                    break;

                case ValidationType.MAXANSWERS:
                    if (!(this.validateMaxAnswers(inputAnswers, input.id, validation.arguments[0]))) {
                        errors.push("Number of input answers must be lower than " + validation.arguments[0]);
                    }
                    break;

                case ValidationType.DEPENDENCY:
                    const id: number = parseInt(validation.arguments[0], 10);
                    if (!(isNaN(id)) && !(this.validateDependency(inputAnswers[id], validation.arguments[1]))) {
                        errors.push("Must answer question with id " + validation.arguments[0] + " and placement " + validation.arguments[1]);
                    }
                    break;
            }
        }

        return errors.join(";");
    }

    /**
     * Validate if form answer is valid.
     * @param formAnswer - FormAnswer to be validated.
     */
    public static validateFormAnswer(formAnswer: FormAnswer): void {
        const errorsDict: ValidationDict = {};
        for (const input of formAnswer.form.inputs) {
            const error: string = this.validateInput(input, formAnswer.inputAnswers);
            if (error !== "" && error !== undefined) {
                errorsDict[input.id] = error;
            }
        }

        if (Object.keys(errorsDict).length > 0) {
            throw new ValidationError(errorsDict, "Validation Error");
        }
    }
}