/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. licenses this file to you 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.
 */

package org.wso2.carbon.identity.base;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

/**
 * This class defines utility methods to be used with input validation
 */
public class IdentityValidationUtil {

    private static final IdentityValidatorConfig validatorConfig = new IdentityValidatorConfig();
    private static final String msgSection1 = "The provided input ";
    private static final String msgSection2 = "does not match any of the white list patterns [ %s ]";
    private static final String msgSection3 =
            "contains illegal characters matching one of the black list patterns [ %s ]";
    private static final String msgSection4 = " or ";
    private static final String PATTERN_NOT_REGISTERED = "No regex pattern registered for the provided key : %s";

    /**
     * Defines a predefined set of pattern list
     */
    public static enum ValidatorPattern {
        DIGITS_ONLY("^[0-9]+$"),
        ALPHABETIC_ONLY("^[a-zA-Z]+$"),
        ALPHANUMERICS_ONLY("^[a-zA-Z0-9]+$"),
        URL("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"),
        EMAIL("^\\s*?(.+)@(.+?)\\s*$"),
        WHITESPACE_EXISTS(".*\\s+.*"),
        URI_RESERVED_EXISTS(".*[:/\\?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=]+.*"),
        URI_UNSAFE_EXISTS(".*[<>%\\{\\}\\|\\^~\\[\\]`]+.*"),
        HTML_META_EXISTS(".*[&<>\"'/]+.*"),
        XML_META_EXISTS(".*[&<>\"']+.*"),
        REGEX_META_EXISTS(".*[\\\\\\^\\$\\.\\|\\?\\*\\+\\(\\)\\[\\{]+.*"),
        HTTP_URL("^(http:)([^/?#])?(:)?(([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"),
        HTTPS_URL("^(https:)([^/?#])?(:)?(([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"),
        FTP_URL("^(ftp:)([^/?#])?(:)?(([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"),
        REGISTRY_INVALID_CHARS_EXISTS("[~!@#;%^*()+={}|<>\\\\\"'/,]+");

        private String regex;

        ValidatorPattern(String regex) {
            this.regex = regex;
        }

        public String getRegex() {
            return regex;
        }
    }

    static {
        for (ValidatorPattern pattern : ValidatorPattern.values()) {
            validatorConfig.addPattern(pattern.name(), pattern.getRegex());
        }
    }

    /**
     * Validates the provided input against the given white list patterns
     *
     * @param input             input
     * @param whiteListPatterns a String array of white list pattern keys
     * @return true if matches with any of the white list patterns
     * @throws IdentityValidationException if a white list pattern key provided does not correspond to a registered
     *                                     regex.
     */
    public static boolean isValidOverWhiteListPatterns(String input, String... whiteListPatterns) {
        if (ArrayUtils.isEmpty(whiteListPatterns)) {
            throw new IllegalArgumentException("Should provide at least one white list pattern");
        }

        if (StringUtils.isEmpty(input)) {
            return true;
        }

        validatePatternKeys(whiteListPatterns);

        boolean isValid = false;
        for (String key : whiteListPatterns) {
            if (validatorConfig.getPattern(key) != null) {
                isValid = validatorConfig.getPattern(key).matcher(input).matches();
                if (isValid) {
                    break;
                }
            }
        }

        return isValid;
    }

    /**
     * Validates the provided input against the given black list patterns
     *
     * @param input             input
     * @param blackListPatterns a String array of black list pattern keys
     * @return true if does not match with any of the black list patterns
     * @throws IdentityValidationException if a black list pattern key provided does not correspond to a registered
     *                                     regex.
     */
    public static boolean isValidOverBlackListPatterns(String input, String... blackListPatterns) {
        if (ArrayUtils.isEmpty(blackListPatterns)) {
            throw new IllegalArgumentException("Should provide at least one black list pattern");
        }

        if (StringUtils.isEmpty(input)) {
            return true;
        }

        validatePatternKeys(blackListPatterns);

        boolean isValid = false;
        for (String key : blackListPatterns) {
            if (validatorConfig.getPattern(key) != null) {
                isValid = !validatorConfig.getPattern(key).matcher(input).matches();
                if (!isValid) {
                    break;
                }
            }
        }

        return isValid;
    }

    /**
     * Validates the provided input against the given white list and black list patterns.
     * Precedence was give to the white list patterns. Thus, if the input is both white listed and blacklisted it
     * will be considered as valid.
     *
     * @param input             input
     * @param whiteListPatterns a String array of white list pattern keys
     * @param blackListPatterns a String array of black list pattern keys
     * @return isWhiteListed || isNotBlackListed
     */
    public static boolean isValid(String input, String[] whiteListPatterns, String[] blackListPatterns) {
        if (ArrayUtils.isEmpty(whiteListPatterns) || ArrayUtils.isEmpty(blackListPatterns)) {
            throw new IllegalArgumentException("Should provide at least one white list pattern and black list pattern");
        }

        return isValidOverWhiteListPatterns(input, whiteListPatterns) ||
                isValidOverBlackListPatterns(input, blackListPatterns);

    }

    /**
     * Returns the input if valid over the given white list patterns else throws an IdentityValidationException
     *
     * @param input             input
     * @param whiteListPatterns a String array of white list pattern keys
     * @return input if valid over the given white list patterns else throws an IdentityValidationException
     */
    public static String getValidInputOverWhiteListPatterns(String input, String... whiteListPatterns)
            throws IdentityValidationException {

        if (StringUtils.isEmpty(input) || isValidOverWhiteListPatterns(input, whiteListPatterns)) {
            return input;
        }

        throw new IdentityValidationException(
                msgSection1 + String.format(msgSection2, getPatternString(whiteListPatterns)));
    }

    /**
     * Returns the input if valid over the given black list patterns else throws an IdentityValidationException
     *
     * @param input             input
     * @param blackListPatterns a String array of black list pattern keys
     * @return input if valid over the given black list patterns else throws an IdentityValidationException
     */
    public static String getValidInputOverBlackListPatterns(String input, String... blackListPatterns)
            throws IdentityValidationException {

        if (StringUtils.isEmpty(input) || isValidOverBlackListPatterns(input, blackListPatterns)) {
            return input;
        }

        throw new IdentityValidationException(
                msgSection1 + String.format(msgSection3, getPatternString(blackListPatterns)));
    }

    /**
     * Returns the input if valid over the given white list and black list patterns else throws an
     * IdentityValidationException
     *
     * @param input             input
     * @param whiteListPatterns a String array of white list pattern keys
     * @param blackListPatterns a String array of black list pattern keys
     * @return input if valid over the given white list and black list patterns else throws an
     * IdentityValidationException
     */
    public static String getValidInput(String input, String[] whiteListPatterns, String[] blackListPatterns)
            throws IdentityValidationException {

        if (StringUtils.isEmpty(input) || isValid(input, whiteListPatterns, blackListPatterns)) {
            return input;
        }

        StringBuilder message = new StringBuilder();
        message.append(msgSection1);
        message.append(String.format(msgSection2, getPatternString(whiteListPatterns)));
        message.append(msgSection4);
        message.append(String.format(msgSection3, getPatternString(blackListPatterns)));

        throw new IdentityValidationException(message.toString());
    }

    /**
     * Adds a validation pattern and stores it against the provided key.
     * Throws an IllegalArgumentException if pattern key or pattern is empty, or if a pattern exists for the given key
     *
     * @param key   pattern key
     * @param regex pattern regex
     */
    public static void addPattern(String key, String regex) {
        validatorConfig.addPattern(key, regex);
    }

    /**
     * Removes a validation pattern
     *
     * @param key pattern key
     */
    public static void removePattern(String key) {
        validatorConfig.removePattern(key);
    }

    /**
     * Checks if a pattern exists for the provided key
     *
     * @param key pattern key
     * @return true if pattern exists or false if pattern does not exist
     */
    public static boolean patternExists(String key) {
        return validatorConfig.patternExists(key);
    }

    private static String getPatternString(String[] patterns) {
        StringBuilder patternString = new StringBuilder();
        for (int i = 0; i < patterns.length; i++) {
            patternString.append(validatorConfig.getPattern(patterns[i]).pattern());
            if ((patterns.length - 1) != i) {
                patternString.append(", ");
            }
        }

        return patternString.toString();
    }

    /**
     * Check if all provided patterns keys have a corresponding regex registered.
     *
     * @param patterns array of pattern keys to be checked
     */
    private static void validatePatternKeys(String[] patterns) {
        for (String key : patterns) {
            if (!patternExists(key)) {
                throw new IllegalArgumentException(String.format(PATTERN_NOT_REGISTERED, key));
            }
        }
    }
}
