/*
 * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2.0, as published by the
 * Free Software Foundation.
 *
 * This program is also distributed with certain software (including but not
 * limited to OpenSSL) that is licensed under separate terms, as designated in a
 * particular file or component or in included license documentation. The
 * authors of MySQL hereby grant you an additional permission to link the
 * program and your derivative works with the separately licensed software that
 * they have included with MySQL.
 *
 * Without limiting anything contained in the foregoing, this file, which is
 * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at
 * http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * 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 General Public License, version 2.0,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.cj.xdevapi;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import com.mysql.cj.x.protobuf.MysqlxDatatypes.Scalar;
import com.mysql.cj.x.protobuf.MysqlxExpr.Array;
import com.mysql.cj.x.protobuf.MysqlxExpr.ColumnIdentifier;
import com.mysql.cj.x.protobuf.MysqlxExpr.DocumentPathItem;
import com.mysql.cj.x.protobuf.MysqlxExpr.Expr;
import com.mysql.cj.x.protobuf.MysqlxExpr.FunctionCall;
import com.mysql.cj.x.protobuf.MysqlxExpr.Identifier;
import com.mysql.cj.x.protobuf.MysqlxExpr.Object;
import com.mysql.cj.x.protobuf.MysqlxExpr.Operator;

/**
 * Serializer utility for dealing with X Protocol expression trees.
 */
public class ExprUnparser {
    /**
     * List of operators which will be serialized as infix operators.
     */
    static Set<String> infixOperators = new HashSet<>();

    static {
        infixOperators.add("and");
        infixOperators.add("or");
    }

    // /**
    //  * Convert an "Any" (scalar) to a string.
    //  */
    // static String anyToString(Any e) {
    //     switch (e.getType()) {
    //         case SCALAR:
    //             return scalarToString(e.getScalar());
    //         default:
    //             throw new IllegalArgumentException("Unknown type tag: " + e.getType());
    //     }
    // }

    /**
     * Scalar to string.
     * 
     * @param e
     *            {@link Scalar}
     * @return scalar string
     */
    static String scalarToString(Scalar e) {
        switch (e.getType()) {
            case V_SINT:
                return "" + e.getVSignedInt();
            case V_OCTETS:
                return "\"" + escapeLiteral(e.getVOctets().getValue().toStringUtf8()) + "\"";
            case V_STRING:
                return "\"" + escapeLiteral(e.getVString().getValue().toStringUtf8()) + "\"";
            case V_DOUBLE:
                return "" + e.getVDouble();
            case V_BOOL:
                return e.getVBool() ? "TRUE" : "FALSE";
            case V_NULL:
                return "NULL";
            default:
                throw new IllegalArgumentException("Unknown type tag: " + e.getType());
        }
    }

    /**
     * JSON document path to string.
     * 
     * @param items
     *            list of {@link DocumentPathItem} objects
     * @return JSON document path string
     */
    static String documentPathToString(List<DocumentPathItem> items) {
        StringBuilder docPathString = new StringBuilder();
        for (DocumentPathItem item : items) {
            switch (item.getType()) {
                case MEMBER:
                    docPathString.append(".").append(quoteDocumentPathMember(item.getValue()));
                    break;
                case MEMBER_ASTERISK:
                    docPathString.append(".*");
                    break;
                case ARRAY_INDEX:
                    docPathString.append("[").append("" + Integer.toUnsignedLong(item.getIndex())).append("]");
                    break;
                case ARRAY_INDEX_ASTERISK:
                    docPathString.append("[*]");
                    break;
                case DOUBLE_ASTERISK:
                    docPathString.append("**");
                    break;
            }
        }
        return docPathString.toString();
    }

    /**
     * Column identifier (or JSON path) to string.
     * 
     * @param e
     *            {@link ColumnIdentifier}
     * @return Column identifier or JSON path string.
     */
    static String columnIdentifierToString(ColumnIdentifier e) {
        if (e.hasName()) {
            String s = quoteIdentifier(e.getName());
            if (e.hasTableName()) {
                s = quoteIdentifier(e.getTableName()) + "." + s;
            }
            if (e.hasSchemaName()) {
                s = quoteIdentifier(e.getSchemaName()) + "." + s;
            }
            if (e.getDocumentPathCount() > 0) {
                s = s + "->$" + documentPathToString(e.getDocumentPathList());
            }
            return s;
        }
        return "$" + documentPathToString(e.getDocumentPathList());
    }

    /**
     * Function call to string.
     * 
     * @param e
     *            {@link FunctionCall}
     * @return Function call string
     */
    static String functionCallToString(FunctionCall e) {
        Identifier i = e.getName();
        String s = quoteIdentifier(i.getName());
        if (i.hasSchemaName()) {
            s = quoteIdentifier(i.getSchemaName()) + "." + s;
        }
        s = s + "(";
        for (Expr p : e.getParamList()) {
            s += exprToString(p) + ", ";
        }
        s = s.replaceAll(", $", "");
        s += ")";
        return s;
    }

    static String arrayToString(Array e) {
        String s = "[";

        for (Expr v : e.getValueList()) {
            s += exprToString(v) + ", ";
        }
        s = s.replaceAll(", $", "");
        s += "]";

        return s;
    }

    /**
     * Create a string from a list of (already stringified) parameters. Surround by parens and separate by commas.
     * 
     * @param params
     *            list of param strings
     * @return param list string
     */
    static String paramListToString(List<String> params) {
        String s = "(";
        boolean first = true;
        for (String param : params) {
            if (!first) {
                s += ", ";
            }
            first = false;
            s += param;
        }
        return s + ")";
    }

    /**
     * Convert an operator to a string. Includes special cases for chosen infix operators (AND, OR) and special forms such as LIKE and BETWEEN.
     * 
     * @param e
     *            {@link Operator}
     * @return Operator string
     */
    static String operatorToString(Operator e) {
        String name = e.getName();
        List<String> params = new ArrayList<>();
        for (Expr p : e.getParamList()) {
            params.add(exprToString(p));
        }
        if ("between".equals(name) || "not_between".equals(name)) {
            name = name.replaceAll("not_between", "not between");
            return String.format("(%s %s %s AND %s)", params.get(0), name, params.get(1), params.get(2));
        } else if ("in".equals(name) || "not_in".equals(name)) {
            name = name.replaceAll("not_in", "not in");
            return String.format("%s %s%s", params.get(0), name, paramListToString(params.subList(1, params.size())));
        } else if ("like".equals(name) || "not_like".equals(name)) {
            name = name.replaceAll("not_like", "not like");
            String s = String.format("%s %s %s", params.get(0), name, params.get(1));
            if (params.size() == 3) {
                s += " ESCAPE " + params.get(2);
            }
            return s;
        } else if ("overlaps".equals(name) || "not_overlaps".equals(name)) {
            name = name.replaceAll("not_overlaps", "not overlaps");
            return String.format("%s %s %s", params.get(0), name, params.get(1));
        } else if ("regexp".equals(name) || "not_regexp".equals("name")) {
            name = name.replaceAll("not_regexp", "not regexp");
            return String.format("(%s %s %s)", params.get(0), name, params.get(1));
        } else if ("cast".equals(name)) {
            return String.format("cast(%s AS %s)", params.get(0), params.get(1).replaceAll("\"", ""));
        } else if ((name.length() < 3 || infixOperators.contains(name)) && params.size() == 2) {
            return String.format("(%s %s %s)", params.get(0), name, params.get(1));
        } else if ("sign_minus".equals(name)) {
            name = name.replaceAll("sign_minus", "-");
            return String.format("%s%s", name, params.get(0));
        } else if ("sign_plus".equals(name)) {
            name = name.replaceAll("sign_plus", "+");
            return String.format("%s%s", name, params.get(0));
        } else if (params.size() == 1) {
            return String.format("%s%s", name, params.get(0));
        } else if (params.size() == 0) {
            return name;
        } else {
            return name + paramListToString(params);
        }
    }

    static String objectToString(Object o) {
        String fields = o.getFldList().stream().map(
                f -> new StringBuilder().append("'").append(quoteJsonKey(f.getKey())).append("'").append(":").append(exprToString(f.getValue())).toString())
                .collect(Collectors.joining(", "));
        return new StringBuilder("{").append(fields).append("}").toString();
    }

    /**
     * Escape a string literal.
     * 
     * @param s
     *            literal
     * @return escaped literal
     */
    public static String escapeLiteral(String s) {
        return s.replaceAll("\"", "\"\"");
    }

    /**
     * Quote a named identifier.
     * 
     * @param ident
     *            identifier
     * @return quoted identifier
     */
    public static String quoteIdentifier(String ident) {
        // TODO: make sure this is correct
        if (ident.contains("`") || ident.contains("\"") || ident.contains("'") || ident.contains("$") || ident.contains(".") || ident.contains("-")) {
            return "`" + ident.replaceAll("`", "``") + "`";
        }
        return ident;
    }

    /**
     * Quote a JSON document field key.
     * 
     * @param key
     *            key
     * @return quoted key
     */
    public static String quoteJsonKey(String key) {
        return key.replaceAll("'", "\\\\'");
    }

    /**
     * Quote a JSON document path member.
     * 
     * @param member
     *            path member
     * @return quoted path member
     */
    public static String quoteDocumentPathMember(String member) {
        if (!member.matches("[a-zA-Z0-9_]*")) {
            return "\"" + member.replaceAll("\"", "\\\\\"") + "\"";
        }
        return member;
    }

    /**
     * Serialize an expression to a string.
     * 
     * @param e
     *            {@link Expr}
     * @return string expression
     */
    public static String exprToString(Expr e) {
        switch (e.getType()) {
            case LITERAL:
                return scalarToString(e.getLiteral());
            case IDENT:
                return columnIdentifierToString(e.getIdentifier());
            case FUNC_CALL:
                return functionCallToString(e.getFunctionCall());
            case OPERATOR:
                return operatorToString(e.getOperator());
            case PLACEHOLDER:
                return ":" + Integer.toUnsignedLong(e.getPosition());
            case ARRAY:
                return arrayToString(e.getArray());
            case OBJECT:
                return objectToString(e.getObject());
            default:
                throw new IllegalArgumentException("Unknown type tag: " + e.getType());
        }
    }
}
