/*
 *
 * 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.qpid.qmf2.common;

// Misc Imports
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

// Reuse this class as it provides a handy mechanism to parse an predicate String into a Map
import org.apache.qpid.messaging.util.AddressParser;

/**
 * A Query is a mechanism for interrogating the management database. A Query represents a selector which is sent to
 * an Agent. The Agent applies the Query against its management database, and returns those objects which meet the  
 * constraints described in the query.
 * <p>
 * A Query must specify the class of information it is selecting. This class of information is considered the target
 * of the query. Any data objects selected by the query will be of the type indicated by the target.
 * <p>
 * A Query may also specify a selector which is used as a filter against the set of all target instances. Only those 
 * instances accepted by the filter will be returned in response to the query.
 * <p>
 * N.B. There appear to be a number of differences in the description of the map encoding of a Query between the
 * QMF2 API specified at <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Proposal</a> and the
 * QMF2 protocol that is specified at <a href=https://cwiki.apache.org/qpid/qmf-map-message-protocol.html>QMF Map
 * Message Protocol</a> in particular the use of the underscore to specify key names e.g. "_what", "_where",
 * "_object_id", "_schema_id".
 * <p>
 * This implementation trusts the protocol specification more than the API specification as the underscores are more  
 * consistent with the rest of the protocol and the underscored variants are what have been observed when querying
 * the broker ManagementAgent.
 * <p>
 * A QmfQuery may be constructed as either an "ID" query (to query for a specific ObjectId or SchemaClassId) or a
 * "PREDICATE" query (to query based upon an expression). Note that QMF considers string arguments in boolean
 * expressions to be names of data values in the target object. When evaluating a predicate expression, QMF will fetch
 * the value of the named data item from each candidate target object. The value is then used in the boolean expression.
 * In other words, QMF considers string arguments to be variables in the expression. In order to indicate that a string 
 * should be treated as a literal instead, the string must be quoted using the "quote" expression.
 * <p>
 * <b>Examples</b>
 * <p>
 * Assume a QmfData type defines fields named "name", "address" and "town". The following predicate expression matches
 * any instance with a name field set to "tross", or any instance where the name field is "jross", the address field is
 * "1313 Spudboy Lane" and the town field is "Utopia":
 * <p>
 * <pre>
 * ["or" ["eq" "name" ["quote" "tross"]]
 *       ["and" ["eq" "name" ["quote" "jross"]]
 *              ["eq" "address" ["quote" "1313 Spudboy Lane"]]
 *              ["eq" ["quote" "Utopia"] "town"]
 *     ]
 * ]
 * </pre>
 * Assume a QmfData type with fields "name" and "age". A predicate to find all instances with name matching the regular 
 * expression "?ross" with an optional age field that is greater than the value 29 or less than 12 would be:
 * <pre>
 * ["and" ["re_match" "name" ["quote" "?ross"]]
 *        ["and" ["exists" "age"]
 *               ["or" ["gt" "age" 27] ["lt" "age" 12]]
 *        ]
 * ]
 * </pre>
 * <p>
 * The Expression structure is illustrated below in the context of its relationship with QmfQuery. 
 * <img src="doc-files/QmfQuery.png"/>
 *
 *
 * @author Fraser Adams
 */
public final class QmfQuery extends QmfData
{
    public static final QmfQuery ID = new QmfQuery();
    public static final QmfQuery PREDICATE = new QmfQuery();

    private QmfQueryTarget _target;
    private SchemaClassId  _classId;
    private String         _packageName;
    private String         _className;
    private ObjectId       _objectId;
    private List           _predicate;
    private Expression     _expression;

    /**
     * This Constructor is only used to construct the ID and PREDICATE objects
     */
    private QmfQuery()
    {
    }

    /**
     * Construct an QmfQuery with no Selector from a QmfQueryTarget
     * @param target the query target
     */
    public QmfQuery(final QmfQueryTarget target)
    {
        _target = target;
        setValue("_what", _target.toString());
    }

    /**
     * Construct an ID QmfQuery from a QmfQueryTarget and SchemaClassId
     * @param target the query target
     * @param classId the SchemaClassId to evaluate against
     */
    public QmfQuery(final QmfQueryTarget target, final SchemaClassId classId)
    {
        _target = target;
        _classId = classId;
        _packageName = _classId.getPackageName();
        _className = _classId.getClassName();
        setValue("_what", _target.toString());
        setValue("_schema_id", _classId.mapEncode());
    }

    /**
     * Construct an ID QmfQuery from a QmfQueryTarget and ObjectId
     * @param target the query target
     * @param objectId the ObjectId to evaluate against
     */
    public QmfQuery(final QmfQueryTarget target, final ObjectId objectId)
    {
        _target = target;
        _objectId = objectId;
        setValue("_what", _target.toString());
        setValue("_object_id", _objectId.mapEncode());
    }

    /**
     * Construct a PREDICATE QmfQuery from a QmfQueryTarget and predicate String
     * @param target the query target
     * @param predicateString the predicate to evaluate against
     */
    public QmfQuery(final QmfQueryTarget target, final String predicateString) throws QmfException
    {
        _target = target;

        if (predicateString.charAt(0) == '[')
        {
            Map predicateMap = new AddressParser("{'_where': " + predicateString + "}").map();
            _predicate = (List)predicateMap.get("_where");
            _expression = Expression.createExpression(_predicate);
        }
        else
        {
            throw new QmfException("Invalid predicate format");
        }

        setValue("_what", _target.toString());
        setValue("_where", _predicate);
    }

    /**
     * Construct a QmfQuery from a Map encoding
     * @param m encoding the query
     */
    public QmfQuery(final Map m) throws QmfException
    {
        super(m);

        _target = QmfQueryTarget.valueOf(getStringValue("_what"));

        if (hasValue("_object_id"))
        {
            _objectId = getRefValue("_object_id");
        }

        if (hasValue("_schema_id"))
        {
            _classId = new SchemaClassId((Map)getValue("_schema_id"));
            _packageName = _classId.getPackageName();
            _className = _classId.getClassName();
        }

        if (hasValue("_where"))
        {
            _predicate = (List)getValue("_where");
            _expression = Expression.createExpression(_predicate);
        }
    }

    /**
     * Return target name.
     * @return target name.
     */
    public QmfQueryTarget getTarget()
    {
        return _target;
    }

    /**
     * Undefined by QMF2 API.
     * <p>
     * According to <a href=https://cwiki.apache.org/confluence/display/qpid/QMFv2+API+Proposal>QMF2 API Specification</a>
     * "The value of the <target name string> map entry is ignored for now, its use is TBD."
     * so this method returns a null Map.
     */
    public Map getTargetParam()
    {
        return null;
    }

    /**
     * Return QmfQuery.ID or QmfQuery.PREDICATE or null if there is no Selector
     * @return QmfQuery.ID or QmfQuery.PREDICATE or null if there is no Selector
     */
    public QmfQuery getSelector()
    {
        if (_predicate == null)
        {
            if (_objectId == null && _classId == null)
            {
                return null;
            }
            return ID;
        }
        return PREDICATE;
    }

    /**
     * Return predicate expression if selector type is QmfQuery.PREDICATE
     * @return predicate expression if selector type is QmfQuery.PREDICATE
     */
    public List getPredicate()
    {
        return _predicate;
    }

    /**
     * Return the SchemaClassId if selector type is QmfQuery.ID
     * @return the SchemaClassId if selector type is QmfQuery.ID
     */
    public SchemaClassId getSchemaClassId()
    {
        return _classId;
    }

    /**
     * Return the ObjectId if selector type is QmfQuery.ID
     * @return the ObjectId if selector type is QmfQuery.ID
     */
    public ObjectId getObjectId()
    {
        return _objectId;
    }

    /**
     * Evaluate query against a QmfData instance.
     * @return true if query matches the QmfData instance, else false.
     */
    public boolean evaluate(final QmfData data)
    {
        if (_predicate == null)
        {
            if (data instanceof QmfManaged)
            {
                QmfManaged managedData = (QmfManaged)data;
                // Evaluate an ID query on Managed Data
                if (_objectId != null && _objectId.equals(managedData.getObjectId()))
                {
                    return true;
                }
                else if (_classId != null)
                {
                    SchemaClassId dataClassId = managedData.getSchemaClassId();
                    String dataClassName = dataClassId.getClassName();
                    String dataPackageName = dataClassId.getPackageName();

                    // Wildcard the package name if it hasn't been specified when checking class name
                    if (_className.equals(dataClassName) &&
                        (_packageName.length() == 0 || _packageName.equals(dataPackageName)))
                    {
                        return true;
                    }

                    // Wildcard the class name if it hasn't been specified when checking package name
                    if (_packageName.equals(dataPackageName) &&
                        (_className.length() == 0 || _className.equals(dataClassName)))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        else
        {
            // Evaluate a PREDICATE query by evaluating against the expression created from the predicate
            if (_predicate.size() == 0)
            {
                return true;
            }

            return _expression.evaluate(data);
        }
    }

    /**
     * Helper/debug method to list the QMF Object properties and their type.
     */
    @Override
    public void listValues()
    {
        System.out.println("QmfQuery:");
        System.out.println("target: " + _target);
        if (_predicate != null)
        {
            System.out.println("selector: QmfQuery.PREDICATE");
            System.out.println("predicate: " + _predicate);
        }
        else if (_classId != null)
        {
            System.out.println("selector: QmfQuery.ID");
            _classId.listValues();
        }
        else if (_objectId != null)
        {
            System.out.println("selector: QmfQuery.ID");
            System.out.println(_objectId);
        }
    }
}

