/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.cli.util;

import java.io.File;

import org.jboss.as.cli.parsing.UnresolvedExpressionException;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 *
 * @author Alexey Loubyansky
 */
public class CLIExpressionResolver {

    /** File separator value */
    private static final String FILE_SEPARATOR = File.separator;

    /** Path separator value */
    private static final String PATH_SEPARATOR = File.pathSeparator;

    /** File separator alias */
    private static final String FILE_SEPARATOR_ALIAS = "/";

    /** Path separator alias */
    private static final String PATH_SEPARATOR_ALIAS = ":";

    /** Environment variable prefix */
    private static final String ENV_PREFIX = "env.";

    private static final int INITIAL = 0;
    private static final int DOLLAR = 1;

    /**
     * Attempts to substitute all the found expressions in the input
     * with their corresponding resolved values.
     * If any of the found expressions failed to resolve the exception will be thrown.
     * If the input does not contain any expression, the input is returned as is.
     *
     * @param input  the input string
     * @return  the input with resolved expressions
     * @throws UnresolvedExpressionException  in case an expression could not be resolved
     */
    public static String resolve(String input) throws UnresolvedExpressionException {
        return resolve(input, true);
    }

    /**
     * Attempts to substitute all the found expressions in the input
     * with their corresponding resolved values.
     * If any of the found expressions failed to resolve or
     * if the input does not contain any expression, the input is returned as is.
     *
     * @param input  the input string
     * @return  the input with resolved expressions or the original input in case
     *          the input didn't contain any expressions or at least one of the
     *          expressions could not be resolved
     */
    public static String resolveOrOriginal(String input) {
        try {
            return resolve(input, true);
        } catch (UnresolvedExpressionException e) {
            return input;
        }
    }

    /**
     * Attempts to substitute all the found expressions in the input with their corresponding resolved values. If any of the
     * found expressions failed to resolve, try to resolve expressions as more as possible and return the resolved value. If the
     * input does not contain any expression, the input is returned as is. see https://issues.jboss.org/browse/WFCORE-1980,
     *
     * @param input the input string
     * @return the input with resolved expressions or the original input in case the input didn't contain any expressions
     */
    public static String resolveLax(String input) {
        try {
            return resolve(input, false);
        } catch (UnresolvedExpressionException e) {
            // XXX OK. It should not reach here as exceptionIfNotResolved is false.
            return input;
        }
    }

    private static String resolve(String input, boolean exceptionIfNotResolved)
            throws UnresolvedExpressionException {

        int state = INITIAL;
        int i = 0;
        while(i < input.length()) {
            final char c = input.charAt(i++);
            if(c == '$') {
                if(state == DOLLAR) {
                    state = INITIAL;
                    final StringBuilder buf = new StringBuilder(input.length() - 1);
                    buf.append(input.substring(0, --i));
                    if(i + 1 < input.length()) {
                        buf.append(input.substring(i + 1));
                    }
                    input = buf.toString();
                } else {
                    state = DOLLAR;
                }
            } else if(c == '{') {
                if(state == DOLLAR) {
                    state = INITIAL;
                    final String[] inputRef = new String[]{input};
                    if(i - 2 == resolveProperty2(inputRef, i - 2, exceptionIfNotResolved)) {
                        input = inputRef[0];
                        i -= 2;
                    }
                }
            }
        }
        return input;
    }

    /**
     *
     * @param location the index of '$' starting the system property
     * @param exceptionIfNotResolved whether to ignore unresolved expression or throw an exception
     * @return the index the parsing should continue from
     * @throws UnresolvedExpressionException in case the expression could not be resolved
     */
    public static String resolveProperty(String input, final int location, boolean exceptionIfNotResolved)
            throws UnresolvedExpressionException {
        final String[] inputRef = new String[]{input};
        if(location == resolveProperty2(inputRef, location, exceptionIfNotResolved)) {
            return inputRef[0];
        }
        return input;
    }

    private static int resolveProperty2(String[] inputRef, final int location, boolean exceptionIfNotResolved)
            throws UnresolvedExpressionException {

        // there must be $ at location and { after it
        int nestingLevel = 1;
        int state = INITIAL;
        final StringBuilder expression = new StringBuilder();
        int i = location + 2;
        while (i < inputRef[0].length()) {
            final char c = inputRef[0].charAt(i++);
            switch (c) {
                case '$':
                    if (state == DOLLAR) {
                        expression.append(c);
                        state = INITIAL;
                    } else {
                        state = DOLLAR;
                    }
                    break;
                case '{':
                    if (state == DOLLAR) {
                        state = INITIAL;
                        if(expression.length() > 0 && expression.charAt(expression.length() - 1) == ':') {
                            expression.append("${");
                            ++nestingLevel;
                        } else {
                            i = resolveProperty2(inputRef, i - 2, exceptionIfNotResolved);
                        }
                    } else {
                        expression.append(c);
                    }
                    break;
                case '}':
                    if (state == DOLLAR) {
                        expression.append('$');
                    }

                    if(--nestingLevel > 0) {
                        state = INITIAL;
                        expression.append(c);
                        continue;
                    }

                    final String propName = expression.toString();
                    final String resolved = resolveKey(propName);
                    if (resolved != null) {
                        final String input = inputRef[0];
                        final StringBuilder buf = new StringBuilder(input.length() - i + location + resolved.length());
                        buf.append(input.substring(0, location)).append(resolved);
                        if (i < input.length()) {
                            buf.append(input.substring(i));
                        }
                        inputRef[0] = buf.toString();
                        return location;
                    } else if (exceptionIfNotResolved) {
                        throw new UnresolvedExpressionException(inputRef[0].substring(location, i), "Unrecognized system property "
                                + propName);
                    }

                    return i;
                default:
                    if (state == DOLLAR) {
                        state = INITIAL;
                        expression.append('$');
                    }
                    expression.append(c);
            }
        }
        return i;
    }

    private static String resolveKey(String key) {
        String value = null;
        // check for alias
        if (FILE_SEPARATOR_ALIAS.equals(key)) {
            value = FILE_SEPARATOR;
        } else if (PATH_SEPARATOR_ALIAS.equals(key)) {
            value = PATH_SEPARATOR;
        } else {
            // check from the properties
            value = WildFlySecurityManager.getPropertyPrivileged(key, null);

            if(value == null && key.startsWith(ENV_PREFIX)) {
                value = System.getenv(key.substring(4));
            }

            if (value == null) {
                // Check for a default value ${key:default}
                int colon = key.indexOf(':');
                if (colon > 0) {
                    String realKey = key.substring(0, colon);
                    value = WildFlySecurityManager.getPropertyPrivileged(realKey, null);

                    if(value == null && realKey.startsWith(ENV_PREFIX)) {
                        value = System.getenv(realKey.substring(4));
                    }

                    if (value == null) {
                        // Check for a composite key, "key1,key2"
                        value = resolveCompositeKey(realKey);

                        // Not a composite key either, use the
                        // specified default
                        if (value == null)
                            value = key.substring(colon + 1);
                    }
                } else {
                    // No default, check for a composite key,
                    // "key1,key2"
                    value = resolveCompositeKey(key);
                }
            }
        }
        return value;
    }
    /**
     * Try to resolve a "key" from the provided properties by checking if it is actually a "key1,key2", in which case try first
     * "key1", then "key2". If all fails, return null.
     *
     * It also accepts "key1," and ",key2".
     *
     * @param key the key to resolve
     * @param props the properties to use
     * @return the resolved key or null
     */
    private static String resolveCompositeKey(String key) {
        String value = null;

        // Look for the comma
        int comma = key.indexOf(',');
        if (comma > -1) {
            // If we have a first part, try resolve it
            if (comma > 0) {
                // Check the first part
                String key1 = key.substring(0, comma);
                value = WildFlySecurityManager.getPropertyPrivileged(key1, null);
                if (value == null && key1.startsWith(ENV_PREFIX)) {
                    value = System.getenv(key1.substring(4));
                }

            }
            // Check the second part, if there is one and first lookup failed
            if (value == null && comma < key.length() - 1) {
                String key2 = key.substring(comma + 1);
                value = WildFlySecurityManager.getPropertyPrivileged(key2, null);
                if (value == null && key2.startsWith(ENV_PREFIX)) {
                    value = System.getenv(key2.substring(4));
                }
            }
        }
        // Return whatever we've found or null
        return value;
    }
}
