/*
 * 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.asterix.external.classad;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.asterix.external.classad.Value.NumberFactor;
import org.apache.asterix.external.classad.object.pool.ClassAdObjectPool;
import org.apache.asterix.om.base.AMutableDouble;
import org.apache.asterix.om.base.AMutableInt32;
import org.apache.asterix.om.base.AMutableInt64;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.hyracks.api.exceptions.HyracksDataException;

public class ClassAdUnParser {

    // table of string representation of operators
    public static final String[] opString = { "", " < ", " <= ", " != ", " == ", " >= ", " > ", " is ", " isnt ", " +",
            " -", " + ", " - ", " * ", " / ", " % ", " !", " || ", " && ", " ~", " | ", " ^ ", " & ", " << ", " >> ",
            " >>> ", " () ", " [] ", " ?: " };
    protected static char delimiter = '\"';

    protected final ClassAdObjectPool objectPool;

    /// Constructor
    public ClassAdUnParser(ClassAdObjectPool objectPool) {
        this.objectPool = objectPool;
    }

    // The default delimiter for strings is '\"'
    // This can be changed to '\'' to unparse quoted attributes, with this function
    public void setDelimiter(char delim) {
        delimiter = delim;
    }

    /**
     * Unparse a value
     *
     * @param buffer
     *            The string to unparse to
     * @param val
     *            The value to unparse
     * @throws HyracksDataException
     */
    public void unparse(AMutableCharArrayString buffer, Value val) throws HyracksDataException {
        switch (val.getType()) {
            case NULL_VALUE:
                buffer.appendString("(null-value)");
                break;

            case STRING_VALUE: {
                AMutableCharArrayString s = objectPool.strPool.get();
                val.isStringValue(s);
                buffer.appendChar('"');
                for (int i = 0; i < s.getLength(); i++) {
                    char ch = s.charAt(i);
                    if (ch == delimiter) {
                        if (delimiter == '\"') {
                            buffer.appendString("\\\"");
                            continue;
                        } else {
                            buffer.appendString("\\\'");
                            continue;
                        }
                    }
                    switch (ch) {
                        case '\b':
                            buffer.appendString("\\b");
                            continue;
                        case '\f':
                            buffer.appendString("\\f");
                            continue;
                        case '\n':
                            buffer.appendString("\\n");
                            continue;
                        case '\r':
                            buffer.appendString("\\r");
                            continue;
                        case '\t':
                            buffer.appendString("\\t");
                            continue;
                        case '\\':
                            buffer.appendString("\\\\");
                            continue;
                        case '\'':
                            buffer.appendString("\'");
                            continue;
                        case '\"':
                            buffer.appendString("\"");
                            continue;
                        default:
                            if (Character.isISOControl(ch)) {
                                // print octal representation
                                buffer.appendString(String.format("\\%03o", ch));
                                continue;
                            }
                            break;
                    }

                    buffer.appendChar(ch);
                }
                buffer.appendChar('"');
                return;
            }
            case INTEGER_VALUE: {
                AMutableInt64 i = objectPool.int64Pool.get();
                val.isIntegerValue(i);
                buffer.appendString(String.valueOf(i.getLongValue()));
                return;
            }
            case REAL_VALUE: {
                AMutableDouble real = objectPool.doublePool.get();
                val.isRealValue(real);
                if (real.getDoubleValue() == 0.0) {
                    // It might be positive or negative and it's
                    // hard to tell. printf is good at telling though.
                    // We also want to print it with as few
                    // digits as possible, which is why we don't use the
                    // case below.
                    buffer.appendString(String.valueOf(real.getDoubleValue()));
                } else if (Util.isNan(real.getDoubleValue())) {
                    buffer.appendString("real(\"NaN\")");
                } else if (Util.isInf(real.getDoubleValue()) == -1) {
                    buffer.appendString("real(\"-INF\")");
                } else if (Util.isInf(real.getDoubleValue()) == 1) {
                    buffer.appendString("real(\"INF\")");
                } else {
                    buffer.appendString(String.format("%1.15E", real.getDoubleValue()));
                }
                return;
            }
            case BOOLEAN_VALUE: {
                MutableBoolean b = objectPool.boolPool.get();
                val.isBooleanValue(b);
                buffer.appendString(b.booleanValue() ? "true" : "false");
                return;
            }
            case UNDEFINED_VALUE: {
                buffer.appendString("undefined");
                return;
            }
            case ERROR_VALUE: {
                buffer.appendString("error");
                return;
            }
            case ABSOLUTE_TIME_VALUE: {
                ClassAdTime asecs = objectPool.classAdTimePool.get();
                val.isAbsoluteTimeValue(asecs);

                buffer.appendString("absTime(\"");
                Util.absTimeToString(asecs, buffer);
                buffer.appendString("\")");
                return;
            }
            case RELATIVE_TIME_VALUE: {
                ClassAdTime rsecs = objectPool.classAdTimePool.get();
                val.isRelativeTimeValue(rsecs);
                buffer.appendString("relTime(\"");
                Util.relTimeToString(rsecs.getRelativeTime(), buffer);
                buffer.appendString("\")");

                return;
            }
            case CLASSAD_VALUE: {
                ClassAd ad = objectPool.classAdPool.get();
                Map<CaseInsensitiveString, ExprTree> attrs = objectPool.strToExprPool.get();
                val.isClassAdValue(ad);
                ad.getComponents(attrs, objectPool);
                unparseAux(buffer, attrs);
                return;
            }
            case SLIST_VALUE:
            case LIST_VALUE: {
                ExprList el = objectPool.exprListPool.get();
                val.isListValue(el);
                unparseAux(buffer, el);
                return;
            }
        }
    }

    /**
     * Unparse an expression
     *
     * @param buffer
     *            The string to unparse to
     * @param expr
     *            The expression to unparse
     * @throws HyracksDataException
     */
    public void unparse(AMutableCharArrayString buffer, ExprTree tree) throws HyracksDataException {
        if (tree == null) {
            buffer.appendString("<error:null expr>");
            return;
        }

        switch (tree.getKind()) {
            case LITERAL_NODE: { // value
                Value val = objectPool.valuePool.get();
                AMutableNumberFactor factor = objectPool.numFactorPool.get();
                ((Literal) tree.getTree()).getComponents(val, factor);
                unparseAux(buffer, val, factor.getFactor());
                return;
            }

            case ATTRREF_NODE: { // string
                ExprTreeHolder expr = objectPool.mutableExprPool.get(); //needs initialization
                AMutableCharArrayString ref = objectPool.strPool.get();
                MutableBoolean absolute = objectPool.boolPool.get();
                ((AttributeReference) tree.getTree()).getComponents(expr, ref, absolute);
                unparseAux(buffer, expr, ref, absolute.booleanValue());
                return;
            }

            case OP_NODE: { //string
                AMutableInt32 op = objectPool.int32Pool.get();
                ExprTreeHolder t1 = objectPool.mutableExprPool.get();
                ExprTreeHolder t2 = objectPool.mutableExprPool.get();
                ExprTreeHolder t3 = objectPool.mutableExprPool.get();
                ((Operation) tree.getTree()).getComponents(op, t1, t2, t3);
                unparseAux(buffer, op.getIntegerValue(), t1, t2, t3);
                return;
            }

            case FN_CALL_NODE: { // string
                AMutableCharArrayString fnName = objectPool.strPool.get();
                ExprList args = objectPool.exprListPool.get();
                ((FunctionCall) tree.getTree()).getComponents(fnName, args);
                unparseAux(buffer, fnName, args);
                return;
            }

            case CLASSAD_NODE: { // nested record
                Map<CaseInsensitiveString, ExprTree> attrs = objectPool.strToExprPool.get();
                ((ClassAd) tree.getTree()).getComponents(attrs, objectPool);
                unparseAux(buffer, attrs);
                return;
            }
            case EXPR_LIST_NODE: { // list
                ExprList exprs = objectPool.exprListPool.get();
                ((ExprList) tree.getTree()).getComponents(exprs);
                unparseAux(buffer, exprs);
                return;
            }

            default:
                // I really wonder whether we should except here, but I
                // don't want to do that without further consultation.
                // wenger 2003-12-11.
                buffer.setValue("");
                throw new HyracksDataException("unknown expression type");
        }
    }

    private void unparseAux(AMutableCharArrayString buffer, AMutableCharArrayString fnName, ExprList args)
            throws HyracksDataException {
        buffer.appendString(fnName);
        buffer.appendChar('(');
        for (ExprTree tree : args.getExprList()) {
            unparse(buffer, tree);
            buffer.appendChar(',');
        }
        if (args.size() > 0) {
            buffer.decrementLength();
        }
        buffer.appendChar(')');

    }

    public void unparseAux(AMutableCharArrayString buffer, final Value value, NumberFactor numFactor)
            throws HyracksDataException {
        unparse(buffer, value);
        if ((value.isIntegerValue() || value.isRealValue()) && numFactor != NumberFactor.NO_FACTOR) {
            buffer.appendChar((numFactor == NumberFactor.B_FACTOR) ? 'B'
                    : (numFactor == NumberFactor.K_FACTOR) ? 'K'
                            : (numFactor == NumberFactor.M_FACTOR) ? 'M'
                                    : (numFactor == NumberFactor.G_FACTOR) ? 'G'
                                            : (numFactor == NumberFactor.T_FACTOR) ? 'T' : '?');
            if (buffer.charAt(buffer.getLength() - 1) == '?') {
                buffer.reset();
                throw new HyracksDataException("bad number factor");
            }
        }
        return;
    }

    /**
     * @param buffer
     * @param tree
     * @param ref
     * @param absolute
     *            = false if ommitted
     * @throws HyracksDataException
     */
    public void unparseAux(AMutableCharArrayString buffer, final ExprTree tree, AMutableCharArrayString ref,
            boolean absolute) throws HyracksDataException {

        if (tree != null && tree.self() != null) {
            unparse(buffer, tree);
            buffer.appendChar('.');
            buffer.appendString(ref);
            return;
        }
        if (absolute) {
            buffer.appendChar('.');
        }
        unparseAux(buffer, ref);
    };

    public void unparseAux(AMutableCharArrayString buffer, final ExprTree tree, AMutableCharArrayString ref)
            throws HyracksDataException {
        unparseAux(buffer, tree, ref, false);
    };

    public void unparseAuxPairs(AMutableCharArrayString buffer, List<Entry<AMutableCharArrayString, ExprTree>> attrlist)
            throws HyracksDataException {
        String delim = "; "; // NAC
        buffer.appendString("[ ");
        for (Entry<AMutableCharArrayString, ExprTree> entry : attrlist) {
            unparseAux(buffer, entry.getKey());
            buffer.appendString(" = ");
            unparse(buffer, entry.getValue());
            buffer.appendString(delim);
        }
        //get rid of last delimiter
        buffer.setLength(buffer.getLength() - delim.length());
        buffer.appendString(" ]");
    }

    // to unparse attribute names (quoted & unquoted attributes)
    public void unparseAux(AMutableCharArrayString buffer, AMutableCharArrayString identifier)
            throws HyracksDataException {
        Value val = objectPool.valuePool.get();
        AMutableCharArrayString idstr = objectPool.strPool.get();

        val.setStringValue(identifier);
        setDelimiter('\''); // change the delimiter from string-literal mode to quoted attribute mode
        unparse(idstr, val);
        setDelimiter('\"'); // set delimiter back to default setting
        idstr.erase(0, 1);
        idstr.erase(idstr.length() - 1, 1);
        if (identifierNeedsQuoting(idstr)) {
            idstr.insert(0, "'");
            idstr.appendString("'");
        }
        buffer.appendString(idstr);
    }

    static boolean identifierNeedsQuoting(AMutableCharArrayString aString) {
        return false;
    }

    public void unparseAux(AMutableCharArrayString buffer, Value val, AMutableNumberFactor factor)
            throws HyracksDataException {
        unparse(buffer, val);
        if ((val.isIntegerValue() || val.isRealValue()) && factor.getFactor() != NumberFactor.NO_FACTOR) {
            buffer.appendString((factor.getFactor() == NumberFactor.B_FACTOR) ? "B"
                    : (factor.getFactor() == NumberFactor.K_FACTOR) ? "K"
                            : (factor.getFactor() == NumberFactor.M_FACTOR) ? "M"
                                    : (factor.getFactor() == NumberFactor.G_FACTOR) ? "G"
                                            : (factor.getFactor() == NumberFactor.T_FACTOR) ? "T"
                                                    : "<error:bad factor>");
        }
        return;
    }

    public void unparseAux(AMutableCharArrayString buffer, ExprTree expr, String attrName, boolean absolute)
            throws HyracksDataException {
        if (expr != null) {
            unparse(buffer, expr);
            buffer.appendString("." + attrName);
            return;
        }
        if (absolute) {
            buffer.appendChar('.');
        }
        unparseAux(buffer, attrName);
    }

    public void unparseAux(AMutableCharArrayString buffer, int op, ExprTreeHolder t1, ExprTreeHolder t2,
            ExprTreeHolder t3) throws HyracksDataException {
        // case 0: parentheses op
        if (op == Operation.OpKind_PARENTHESES_OP) {
            buffer.appendString("( ");
            unparse(buffer, t1);
            buffer.appendString(" )");
            return;
        }
        // case 1: check for unary ops
        if (op == Operation.OpKind_UNARY_PLUS_OP || op == Operation.OpKind_UNARY_MINUS_OP
                || op == Operation.OpKind_LOGICAL_NOT_OP || op == Operation.OpKind_BITWISE_NOT_OP) {
            buffer.appendString(opString[op]);
            unparse(buffer, t1);
            return;
        }
        // case 2: check for ternary op
        if (op == Operation.OpKind_TERNARY_OP) {
            unparse(buffer, t1);
            buffer.appendString(" ? ");
            unparse(buffer, t2);
            buffer.appendString(" : ");
            unparse(buffer, t3);
            return;
        }
        // case 3: check for subscript op
        if (op == Operation.OpKind_SUBSCRIPT_OP) {
            unparse(buffer, t1);
            buffer.appendChar('[');
            unparse(buffer, t2);
            buffer.appendChar(']');
            return;
        }

        // all others are binary ops
        unparse(buffer, t1);
        buffer.appendString(opString[op]);
        unparse(buffer, t2);
    }

    public void UnparseAux(AMutableCharArrayString buffer, String fnName, ExprList args) throws HyracksDataException {
        buffer.appendString(fnName + "(");
        for (ExprTree tree : args.getExprList()) {
            unparse(buffer, tree);
            buffer.appendChar(',');
        }
        buffer.setChar(buffer.getLength() - 1, ')');
    }

    public void unparseAux(AMutableCharArrayString buffer, Map<CaseInsensitiveString, ExprTree> attrs)
            throws HyracksDataException {

        String delim = "; "; // NAC

        buffer.appendString("[ ");

        for (Entry<CaseInsensitiveString, ExprTree> entry : attrs.entrySet()) {
            unparseAux(buffer, entry.getKey().get());
            buffer.appendString(" = ");
            unparse(buffer, entry.getValue());
            buffer.appendString(delim); // NAC
        }
        buffer.setLength(buffer.getLength() - delim.length());
        buffer.appendString(" ]");

    }

    public void unparseAux(AMutableCharArrayString buffer, ExprList exprs) throws HyracksDataException {

        buffer.appendString("{ ");
        for (ExprTree expr : exprs.getExprList()) {
            unparse(buffer, expr);
            buffer.appendChar(',');
        }
        buffer.decrementLength();
        buffer.appendString(" }");
    }

    /* To unparse the identifier strings
     * based on the character content,
     * it's unparsed either as a quoted attribute or non-quoted attribute
     */
    public void unparseAux(AMutableCharArrayString buffer, String identifier) throws HyracksDataException {
        Value val = objectPool.valuePool.get();
        AMutableCharArrayString idstr = objectPool.strPool.get();

        val.setStringValue(identifier);
        setDelimiter('\''); // change the delimiter from string-literal mode to quoted attribute mode
        unparse(idstr, val);
        setDelimiter('\"'); // set delimiter back to default setting
        idstr.erase(0, 1);
        idstr.erase(idstr.length() - 1, 1);
        if (identifierNeedsQuoting(idstr)) {
            idstr.prependChar('\'');
            idstr.appendChar('\'');
        }
        buffer.appendString(idstr);
    }
}
