/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.apache.jackrabbit.oak.plugins.index.search.util;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.memory.EmptyPropertyState;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.query.QueryConstants;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A parser for function-based indexes. It converts the human-readable function
 * definition (XPath) to the internal Polish notation.
 */
public class FunctionIndexProcessor {
    
    private static final Logger LOG =
            LoggerFactory.getLogger(FunctionIndexProcessor.class);
    
    private String remaining;

    private static final PropertyState EMPTY_PROPERTY_STATE = EmptyPropertyState.emptyProperty("empty", Type.STRINGS);

    protected FunctionIndexProcessor(String function) {
        this.remaining = function;
    }
    
    /**
     * Get the list of properties used in the given function code.
     * 
     * @param functionCode the tokens, for example ["function", "lower", "@name"]
     * @return the list of properties, for example ["name"]
     */
    public static String[] getProperties(String[] functionCode) {
        ArrayList<String> properties = new ArrayList<>();
        for(String token : functionCode) {
            if (token.startsWith("@")) {
                String propertyName = token.substring(1);
                properties.add(propertyName);
            }
        }
        return properties.toArray(new String[0]);
    }

    /**
     * Try to calculate the value for the given function code.
     * 
     * @param path the path of the node
     * @param state the node state
     * @param functionCode the tokens, for example ["function", "lower", "@name"]
     * @return null, or the calculated value
     */
    public static PropertyState tryCalculateValue(String path, NodeState state, String[] functionCode) {
        Deque<PropertyState> stack = new ArrayDeque<>();
        for (int i = functionCode.length - 1; i > 0; i--) {
            String token = functionCode[i];
            PropertyState ps;
            if (token.startsWith("@")) {
                String propertyName = token.substring(1);
                ps = getProperty(path, state, propertyName);
            } else {
                ps = calculateFunction(token, stack);
            }
            if (ps == null) {
                ps = EMPTY_PROPERTY_STATE;
            }
            stack.push(ps);
        }

        PropertyState ret = stack.pop();
        return ret==EMPTY_PROPERTY_STATE ? null : ret;
    }
    
    /**
     * Split the polish notation into a tokens that can more easily be processed.
     *  
     *  @param functionDescription in polish notation, for example "function*lower*{@literal @}name"
     *  @return tokens, for example ["function", "lower", "{@literal @}name"]
     */
    public static String[] getFunctionCode(String functionDescription) {
        if (functionDescription == null) {
            return null;
        }
        return functionDescription.split("\\*");
    }
    
    private static PropertyState calculateFunction(String functionName,
                                                   Deque<PropertyState> stack) {
        PropertyState ps = stack.pop();
        if ("coalesce".equals(functionName)) {
            // coalesce (a, b) => (a != null ? a : b)
            // we pop stack again to consume the second parameter
            // also, if ps is EMPTY_PROPERTY_STATE, then newly popped value is to be used
            PropertyState ps2 = stack.pop();
            if (ps == EMPTY_PROPERTY_STATE) {
                ps = ps2;
            }
        }
        if (ps == EMPTY_PROPERTY_STATE) {
            return ps;
        }
        Type<?> type = null;
        ArrayList<Object> values = new ArrayList<>(ps.count());
        for (int i = 0; i < ps.count(); i++) {
            String s = ps.getValue(Type.STRING, i);
            Object x;
            if ("lower".equals(functionName)) {
                x = s.toLowerCase();
                type = Type.STRING;
            } else if ("coalesce".equals(functionName)) {
                x = s;
                type = Type.STRING;
            } else if ("upper".equals(functionName)) {
                x = s.toUpperCase();
                type = Type.STRING;
            } else if ("length".equals(functionName)) {
                x = (long) s.length();
                type = Type.LONG;
            } else {
                LOG.debug("Unknown function {}", functionName);
                return null;
            }
            values.add(x);
        }
        PropertyState result;
        if (values.size() == 1) {
            result = PropertyStates.createProperty("value", values.get(0), type);
        } else {
            type = type.getArrayType();
            result = PropertyStates.createProperty("value", values, type);
        }
        return result;
    }
    
    private static PropertyState getProperty(String path, NodeState state,
                                             String propertyName) {
        if (PathUtils.getDepth(propertyName) != 1) {
            for(String n : PathUtils.elements(PathUtils.getParentPath(propertyName))) {
                state = state.getChildNode(n);
                if (!state.exists()) {
                    return null;
                }
            }
            propertyName = PathUtils.getName(propertyName);
        }
        PropertyState ps;
        if (":localname".equals(propertyName)) {
            ps = PropertyStates.createProperty("value",
                    getLocalName(PathUtils.getName(path)), Type.STRING);
        } else if (":name".equals(propertyName)) {
            ps = PropertyStates.createProperty("value",
                    PathUtils.getName(path), Type.STRING);
        } else {
            ps = state.getProperty(propertyName);
        }
        if (ps == null || ps.count() == 0) {
            return null;
        }
        return ps;
    }
    
    private static String getLocalName(String name) {
        int colon = name.indexOf(':');
        // TODO LOCALNAME: evaluation of local name might not be correct
        return colon < 0 ? name : name.substring(colon + 1);
    }
    
    /**
     * Convert a function (in human-readable form) to the polish notation.
     * 
     * @param function the function, for example "lower([name])"
     * @return the polish notation, for example "function*lower*{@literal @}name"
     */
    public static String convertToPolishNotation(String function) {
        if (function == null) {
            return null;
        }
        FunctionIndexProcessor p = new FunctionIndexProcessor(function);
        return QueryConstants.FUNCTION_RESTRICTION_PREFIX + p.parse();
    }
    
    String parse() {
        if (match("fn:local-name()") || match("localname()")) {
            return "@:localname";
        }
        if (match("fn:name()") || match("name()")) {
            return "@:name";
        }
        if (match("fn:upper-case(") || match("upper(")) {
            return "upper*" + parse() + read(")");
        }
        if (match("fn:lower-case(") || match("lower(")) {
            return "lower*" + parse() + read(")");
        }
        if (match("fn:coalesce(") || match("coalesce(")) {
            return "coalesce*" + parse() + readCommaAndWhitespace() + parse() + read(")");
        }
        if (match("fn:string-length(") || match("length(")) {
            return "length*" + parse() + read(")");
        }

        // property name
        if (match("[")) {
            String prop = remaining;
            int indexOfComma = remaining.indexOf(",");
            if (indexOfComma > 0) {
                prop = remaining.substring(0, indexOfComma);
            }
            prop = prop.substring(0, prop.lastIndexOf(']'));
            remaining = remaining.substring(prop.length() + 1);
            return property(prop.replaceAll("]]", "]"));
        } else {
            String prop = remaining;
            int paren = remaining.indexOf(')');
            int comma = remaining.indexOf(',');
            int end = comma;
            if (paren >=0) {
                end = (end < 0) ? paren : Math.min(end, paren);
            }
            if (end >= 0) {
                prop = remaining.substring(0, end);
            }
            remaining = remaining.substring(prop.length());
            return property(prop.replaceAll("@", ""));
        }
    }
    
    String property(String p) {
        return "@" + p;
    }
    
    private String read(String string) {
        match(string);
        return "";
    }

    private String
    readCommaAndWhitespace() {
        while (match(" ")) {
        }
        match(",");
        while (match(" ")) {
        }
        return "*";
    }
    
    private boolean match(String string) {
        if (remaining.startsWith(string)) {
            remaining = remaining.substring(string.length());
            return true;
        }
        return false;
    }

}
