/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (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.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * 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 com.queplix.core.modules.eql.parser.generic;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.jxb.entity.Dataschema;
import com.queplix.core.jxb.entity.Efield;
import com.queplix.core.jxb.entity.Entity;
import com.queplix.core.jxb.entity.Listref;
import com.queplix.core.jxb.entity.types.JoinSType;
import com.queplix.core.jxb.entity.types.SqlSType;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.EQLEReq;
import com.queplix.core.modules.eql.EQLNullObject;
import com.queplix.core.modules.eql.EQLNumberObject;
import com.queplix.core.modules.eql.EQLObject;
import com.queplix.core.modules.eql.EQLReq;
import com.queplix.core.modules.eql.EQLReqAggFunc;
import com.queplix.core.modules.eql.EQLReqEntity;
import com.queplix.core.modules.eql.EQLReqField;
import com.queplix.core.modules.eql.EQLReqFuncExecutable;
import com.queplix.core.modules.eql.EQLReqOp;
import com.queplix.core.modules.eql.EQLReqSelectAttr;
import com.queplix.core.modules.eql.EQLReqSubOp;
import com.queplix.core.modules.eql.EQLReqSubOpMember;
import com.queplix.core.modules.eql.EQLReqSubOpMemberEnum;
import com.queplix.core.modules.eql.EQLReqSubOpMemberField;
import com.queplix.core.modules.eql.EQLReqSubOpMemberFunc;
import com.queplix.core.modules.eql.EQLReqSubOpMemberUnknown;
import com.queplix.core.modules.eql.EQLReqSubOrder;
import com.queplix.core.modules.eql.EQLReqSubWhere;
import com.queplix.core.modules.eql.EQLReqSubWhereCond;
import com.queplix.core.modules.eql.EQLReqWhere;
import com.queplix.core.modules.eql.EQLSession;
import com.queplix.core.modules.eql.EQLStringObject;
import com.queplix.core.modules.eql.conds.EqCond;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eql.error.EQLFunctionParseException;
import com.queplix.core.modules.eql.error.EQLSystemException;
import com.queplix.core.modules.eql.ops.NoneOp;
import com.queplix.core.modules.eql.parser.EQLFuncManager;
import com.queplix.core.modules.eql.utils.EQLUtils;
import com.queplix.core.utils.log.AbstractLogger;

/**
 * <p>Special class mediator for EQL interpreter and EQL parser.
 * Stores EQLReq object.</p>
 * <p>Schema:</p>
 * <p>EQL interpreter <-> EQL mediator <-> EQL parser</p>
 *
 * @author [ALB] Baranov Andrey
 */

public class EQLIntMediator
        extends AbstractLogger {

    // ------------------------------------------------------- variables

    private AbstractEQLInterpreter interpreter;
    private EQLReq req;

    // ------------------------------------------------------- constructor

    /**
     * Constructor
     *
     * @param interpreter EQL generic interpreter
     */
    protected EQLIntMediator(AbstractEQLInterpreter interpreter) {
        this.interpreter = interpreter;
        this.req = new EQLReq();
    }

    // ------------------------------------------------------- public methods

    //
    // SERVICE METHODS
    //

    /**
     * Return current EQL session
     *
     * @return EQLSession object
     */
    public EQLSession getSession() {
        return interpreter.getSession();
    }

    /**
     * Get EQL request object
     *
     * @return EQLReq object
     */
    public EQLReq getEQLReq() {
        return req;
    }

    /**
     * Clone EQL mediator
     *
     * @return new mediator
     */
    public Object clone() {
        DEBUG("Clone mediator...");
        return ((AbstractEQLInterpreter) interpreter.clone()).getMainMediator();
    }

    //
    // Wrappers for EQLParserGenericImpl parser methods
    //

    public void parse(String eql)
            throws EQLException {
        AbstractEQLParser.getParser(this, eql).parse();
    }

    public void parseWhere(String eql)
            throws EQLException {
        AbstractEQLParser.getParser(this, eql).parseWhere();
    }

    public EQLReqOp parseSrc(String eql)
            throws EQLException {
        return AbstractEQLParser.getParser(this, eql).parseSrc();
    }

    public void parseOrder(String eql)
            throws EQLException {
        AbstractEQLParser.getParser(this, eql).parseOrder();
    }

    /**
     * Get EQL function by the <code>name</code>
     *
     * @param name function name
     * @return EQLReqSubOpMemberFunc object
     * @throws EQLException
     */
    public EQLReqSubOpMemberFunc getFunction(String name)
            throws EQLException {
        return EQLFuncManager.getFunction(name, interpreter);
    }

    //
    // BUILDER METHODS
    //

    /**
     * Add EQL request meta information
     *
     * @param name  parameter name
     * @param value parameter value
     */
    public void addMetaInfoAsNumber(String name, String value) {
        try {
            getEQLReq().getMetaData().setParam(name, EQLUtils.parseEQLNumber(
                    value));
        } catch (Exception ex) {
            throw new EQLSystemException(ex);
        }
    }

    /**
     * Add EQL request meta information
     *
     * @param name  parameter name
     * @param value parameter value
     * @throws EQLException
     */
    public void addMetaInfoAsBoolean(String name, Boolean value)
            throws EQLException {
        getEQLReq().getMetaData().setParam(name, value);
    }

    /**
     * Add EQL request meta information
     *
     * @param name  parameter name
     * @param value parameter value
     */
    public void addMetaInfoAsList(String name, String value) {
        try {
            getEQLReq().getMetaData().setParam(name, EQLUtils.parseEQLList(
                    value));
        } catch (Exception ex) {
            throw new EQLSystemException(ex);
        }
    }

    /**
     * Add all field from entity <code>entity</code> in select list
     *
     * @param entity toplevel entity
     * @throws EQLException
     */
    public void addSelectAllColumns(Entity entity)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSelectAllColumns(): " + entity.getName());
        }

        EQLReq __req = req;
        req = new EQLEReq(entity);
        req.copyOf(__req);
        __req = null;

        // add entity
        EQLReqEntity reqEntity = new EQLReqEntity(entity);
        interpreter.addEntity(reqEntity);

        // checking
        int size = entity.getEfieldCount();
        if(size == 0) {
            throw new GenericSystemException("Entity '" + entity.getName() +
                    "' doesn't contain any fields");
        }

        // add ALL in SELECT list
        for(int i = 0; i < size; i++) {
            Efield field = entity.getEfield(i);
            boolean isLazy = req.isLazy(field);
            addSelectColumn(new EQLReqField(reqEntity, field), isLazy);
        }

        // add entity WHERE conditions
        addEntityWhereCondition(reqEntity);

        // add entity ORDER BY condition as default
        String eqlOrderQuery = entity.getEqlOrder();
        if(eqlOrderQuery != null) {
            parseOrder(eqlOrderQuery);
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("Added all fields from entity: " + reqEntity);
        }
    }

    /**
     * Add field <code>reqField</code> in select list
     *
     * @param reqField EQLReqField object
     * @throws EQLException
     */
    public void addSelectColumn(EQLReqField reqField)
            throws EQLException {
        addSelectColumn(reqField, false);

        // add entity WHERE conditions
        addEntityWhereCondition(reqField.getReqEntity());

    }

    /**
     * Add field <code>reqField</code> in select list
     *
     * @param reqField  EQLReqField object
     * @param lazyField is field for lazy loading
     * @throws EQLException
     */
    public void addSelectColumn(EQLReqField reqField, boolean lazyField)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSelectColumn(): " + reqField + " lazyField?="
                    + lazyField);
        }

        if(!reqField.getField().getSelectable().booleanValue()) {
            // field isn't selectable
            return;
        }

        // add field

        /** @todo re-implement it */
        // NOTE: [ALB] Quick fix for lazy loading (add reference only for MEMO lazy fields).
        Efield field = reqField.getField();
        int sql_type = field.getSqltype().getType();
        boolean withRef;
        if(sql_type == SqlSType.MEMO_TYPE) {
            withRef = true;
        } else if(lazyField) {
            withRef = false;
        } else {
            withRef = true;
        }
        // <---

        EQLReqOp reqOp = buildSelectEQLReqOp(interpreter.addField(reqField,
                withRef));
        EQLReqSelectAttr reqSelectAttr = new EQLReqSelectAttr(reqField, reqOp);

        if(!lazyField && !interpreter.isCloned()) {
            // top level select query --
            // try to add list reference

            Listref listref = reqField.getField().getListref();
            if(listref != null) {
                // check list field JOIN type
                JoinSType joinType = listref.getJointype();
                switch(joinType.getType()) {
                    case JoinSType.USE_CACHE_TYPE: {
                        // don't add in query - take from cache later
                        break;
                    }

                    default: {
                        // join entities
                        EQLReqField listReqField = interpreter.addListField(
                                reqField);
                        reqSelectAttr.setListFieldSelectAttr(
                                new EQLReqSelectAttr(listReqField,
                                        buildSelectEQLReqOp(listReqField)));
                    }
                }
            }
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("Add field in select list: " + reqField);
        }

        // add in SELECT list
        req.getSelect().addAttr(reqSelectAttr);
    }

    /**
     * Add field <code>reqField</code> default value in select list
     *
     * @param reqField EQLReqField object
     * @throws EQLException
     */
    public void addSelectColumnAsDefault(EQLReqField reqField)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSelectColumnAsDefault(): " + reqField);
        }

        if(!reqField.getField().getSelectable().booleanValue()) {
            // field isn't selectable
            return;
        }

        EQLReqOp reqOp = buildDefaultSelectEQLReqOp(reqField);
        EQLReqSelectAttr reqSelectAttr = new EQLReqSelectAttr(reqField, reqOp);

        // try to add list reference
        addListFieldAsDefault(reqSelectAttr, reqField);

        // add in SELECT list
        req.getSelect().addAttr(reqSelectAttr);
    }

    /**
     * Add function <code>memberFunc</code> in select list
     *
     * @param memberFunc EQLReqSubOpMemberFunc object
     * @throws EQLException
     */
    public void addSelectColumnAsFunc(EQLReqSubOpMemberFunc memberFunc)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSelectColumnAsFunc(): " + memberFunc);
        }

        if(!(memberFunc instanceof EQLReqAggFunc)) {
            throw new EQLFunctionParseException(memberFunc);
        }

        EQLReqAggFunc aggFunc = (EQLReqAggFunc) memberFunc;

        // build new EQLReqOp object
        EQLReqOp reqOp = new EQLReqOp();
        reqOp.addSubOp(new NoneOp(aggFunc));

        // build new EQLReqSelectAttr object
        EQLReqSelectAttr reqSelectAttr =
                new EQLReqSelectAttr(aggFunc.getParameter().getReqField(),
                        reqOp);

        // add in SELECT list
        req.getSelect().addAttr(reqSelectAttr);
    }

    /**
     * Add entity WHERE condition
     *
     * @param reqEntity EQLReqEntity object
     * @throws EQLException
     */
    public void addEntityWhereCondition(EQLReqEntity reqEntity)
            throws EQLException {

        String eqlWhereQuery = reqEntity.getEntity().getEqlWhere();
        if(eqlWhereQuery != null) {
            parseWhere(eqlWhereQuery);
        }
    }

    /**
     * Create and add new where condition as subwhere object
     *
     * @param reqWhere     parent where object
     * @param where_op     where operation id
     * @param subWhereCond EQLReqSubWhereCond sub where condition
     * @throws EQLException
     */
    public void addSubWhereCondition(EQLReqWhere reqWhere,
                                     int where_op,
                                     EQLReqSubWhereCond subWhereCond)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSubWhereCondition(): " + where_op);
        }

        // add sub where condition
        EQLReqSubWhere reqSubWhere = new EQLReqSubWhere(where_op, subWhereCond);
        if(!reqWhere.contains(reqSubWhere)) {
            reqWhere.addSubWhere(reqSubWhere);
        }
    }

    /**
     * Create and add new where as subwhere object
     *
     * @param reqWhere parent where object
     * @param where_op where operation id
     * @return new child where object
     * @throws EQLException
     */
    public EQLReqWhere addSubWhere(EQLReqWhere reqWhere, int where_op)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSubWhere(): " + where_op);
        }

        // create new EQL req where
        EQLReqWhere newReqWhere = new EQLReqWhere();

        // add sub where condition
        EQLReqSubWhere reqSubWhere = new EQLReqSubWhere(where_op, newReqWhere);
        if(!reqWhere.contains(reqSubWhere)) {
            reqWhere.addSubWhere(reqSubWhere);
        }

        return newReqWhere;
    }

    /**
     * Create and add new where as subwhere object
     *
     * @param reqField  EQLReqField object
     * @param order_op  order operation id
     * @param mandatory mandatory order or not
     * @throws EQLException
     */
    public void addSubOrder(EQLReqField reqField, int order_op,
                            boolean mandatory)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSubOrder(): " + order_op);
        }

        if(reqField.getField().getListref() != null) {
            // add list reference
            reqField = interpreter.addListField(reqField);
        } else {
            // add field
            reqField = interpreter.addField(reqField);
        }

        // create bew req operation
        EQLReqOp reqOp = buildSelectEQLReqOp(reqField);

        // create and add new sub order
        EQLReqSubOrder subOrder = new EQLReqSubOrder(order_op, reqOp,
                mandatory);
        getEQLReq().getOrder().addSubOrder(subOrder);
    }

    /**
     * Add sub operation in operation object
     *
     * @param reqSubOp sub operation object object
     * @param reqOp    operation object
     * @throws EQLException
     */
    public void addSubOp(EQLReqSubOp reqSubOp, EQLReqOp reqOp)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addSubOp() " + reqSubOp.getClass());
        }

        // add sub operation
        reqOp.addSubOp(reqSubOp);
    }

    /**
     * Try to get EQLReqSubOpMember object from EQL prepared statement
     *
     * @return EQLReqSubOpMember object or build new
     * @throws EQLException
     */
    public EQLReqSubOpMember buildNextSubOperation()
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" nextSubOperation()");
        }

        EQLReqSubOpMember o = null;
        if(interpreter.getPreparedStatement() != null) {
            // get sub operation member from EQL prepared statement
            o = interpreter.getPreparedStatement().next();
        }

        return (o == null) ? new EQLReqSubOpMemberUnknown():o;
    }

    /**
     * Build new sub operation member
     *
     * @param value number
     * @return EQLObject object
     * @throws EQLException
     */
    public EQLObject buildSubOperationMemberNumber(String value)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberNumber(): " + value);
        }

        // build sub operation member number
        Number number = EQLUtils.parseEQLNumber(value);
        return new EQLNumberObject(number);
    }

    /**
     * Build new sub operation member
     *
     * @param value string
     * @return EQLObject object
     * @throws EQLException
     */
    public EQLObject buildSubOperationMemberString(String value)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberString(): " + value);
        }

        // build sub operation member string
        return new EQLStringObject(value);
    }

    /**
     * Build new sub operation member
     *
     * @return EQLObject object
     * @throws EQLException
     */
    public EQLObject buildSubOperationMemberNull()
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberNull()");
        }

        // build sub operation member null
        return EQLNullObject.getInstance();
    }

    /**
     * Build new sub operation member
     *
     * @param reqField req field object
     * @return EQLReqSubOpMember object
     * @throws EQLException
     */
    public EQLReqSubOpMember buildSubOperationMemberField(EQLReqField reqField)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberField(): " + reqField);
        }

        /** @todo check for virtual field */

        // add field
        reqField = interpreter.addField(reqField);

        // build sub operation member field
        return new EQLReqSubOpMemberField(reqField);
    }

    /**
     * Build new sub operation member
     *
     * @param reqField EQLReqField object
     * @return EQLReqSubOpMember object
     * @throws EQLException
     */
    public EQLReqSubOpMember buildSubOperationMemberListField(
            EQLReqField reqField)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberListField(): " + reqField);
        }

        /** @todo check for virtual field */

        // add list field
        EQLReqField listReqField = interpreter.addListField(reqField);

        return buildSelectEQLReqOp(listReqField);
    }

    /**
     * Build new sub operation member
     *
     * @param func EQLReqSubOpMemberFunc object
     * @return EQLReqSubOpMember object
     * @throws EQLException
     */
    public EQLReqSubOpMember buildSubOperationMemberFunc(
            EQLReqSubOpMemberFunc func)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" buildSubOperationMemberFunc(): " + func);
        }

        if(func instanceof EQLReqFuncExecutable) {
            // execute function
            return ((EQLReqFuncExecutable) func).execute();
        } else {
            return func;
        }
    }

    /**
     * Add new function parameter
     *
     * @param func      EQL function
     * @param funcParam function parameter
     * @throws EQLException
     */
    public void addFunctionParameter(EQLReqSubOpMemberFunc func,
                                     EQLReqOp funcParam)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addFunctionParameter(): " + func + "," + funcParam);
        }

        func.addParameter(funcParam);

        // register field in EQL interpreter
        int size = funcParam.size();
        for(int i = 0; i < size; i++) {
            EQLReqSubOp subOp = funcParam.getSubOp(i);
            EQLReqSubOpMember subOpMember = subOp.getMember();

            if(subOpMember instanceof EQLReqSubOpMemberField) {
                EQLReqSubOpMemberField memberField
                        = (EQLReqSubOpMemberField) subOpMember;
                EQLReqField reqField = memberField.getReqField();

                // add entity and entity constraint
                // and get new req field
                reqField = interpreter.addField(reqField);

                if(func instanceof EQLReqAggFunc) {
                    // for aggregate function add entity WHERE condition
                    addEntityWhereCondition(reqField.getReqEntity());
                }

                // substitute member field
                subOp.setMember(new EQLReqSubOpMemberField(reqField));
            }
        }
    }

    /**
     * Add new enumeration parameter
     *
     * @param en        EQL enumeration
     * @param enumParam enumeration parameter
     * @throws EQLException
     */
    public void addEnumParameter(EQLReqSubOpMemberEnum en, EQLObject enumParam)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG(" addEnumParameter(): " + en + "," + enumParam);
        }

        // currently supportes only primitive types in enumeration
        /** @todo make support of other EQL types */

        en.addParameter(enumParam);
    }

    // ------------------------------------------------------- private methods

    /**
     * Add list reference default value
     *
     * @param reqSelectAttr parent EQLReqSelectAttr object
     * @param reqField      EQLReqField object
     * @throws EQLException
     */
    private void addListFieldAsDefault(EQLReqSelectAttr reqSelectAttr,
                                       EQLReqField reqField)
            throws EQLException {

        Entity entity = reqField.getReqEntity().getEntity();
        Listref listRef = reqField.getField().getListref();
        if(listRef == null) {
            return;
        }

        // get listref entity and field
        Entity lrefEntity = interpreter.getSession().findEntity(
                listRef.getEntity());
        Efield lrefField = EntityHelper.getEfield(listRef.getEfield(),
                lrefEntity);

        // construct list ref EQLReqEntity and EQLReqField objects
        EQLReqEntity lrefReqEntity = new EQLReqEntity(lrefEntity, reqField);
        EQLReqField listReqField = new EQLReqField(lrefReqEntity, lrefField);

        //
        // Left operation doesn't contain field.
        // So we can't add JOIN - add WHERE clause instead!
        //
        Dataschema ds = listRef.getDataschema();
        String[] keys1 = ds.getTable(0).getKey();
        String[] keys2 = ds.getTable(1).getKey();
        int keySize = keys1.length;

        for(int i = 0; i < keySize; i++) {
            Efield efield1 = EntityHelper.getEfieldBySrc(keys1[i], entity);
            Efield efield2 = EntityHelper.getEfieldBySrc(keys2[i], lrefEntity);

            EQLReqField reqField1 = new EQLReqField(entity, efield1);
            EQLReqField reqField2 = new EQLReqField(lrefEntity, efield2);

            EQLReqOp reqOp = buildDefaultSelectEQLReqOp(reqField1);
            EqCond cond = new EqCond(reqOp, new EQLReqOp(reqField2));
            interpreter.addWhere(reqField2, cond);
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("Add def list field: " + listReqField);
        }

        // build list reference EQLReqSelectAttr object
        reqSelectAttr.setListFieldSelectAttr(
                new EQLReqSelectAttr(listReqField, buildSelectEQLReqOp(
                        listReqField))
        );
    }

    /**
     * Build EQLReqOp object for select attribute
     * Takes <code>eql-src</code> Efield attribute if exists
     *
     * @param reqField EQLReqField object
     * @return EQLReqOp object
     * @throws EQLException
     */
    private EQLReqOp buildSelectEQLReqOp(EQLReqField reqField)
            throws EQLException {

        Efield field = reqField.getField();

        // check: has field 'eql-src' attribute
        EQLReqOp reqOp;
        if(field.getEqlSrc() != null) {
            reqOp = parseSrc(field.getEqlSrc());
        } else {
            reqOp = new EQLReqOp(reqField);
        }

        return reqOp;
    }

    /**
     * Build EQLReqOp object for select attribute
     * Takes <code>eql-defsrc</code> Efield attribute
     *
     * @param reqField EQLReqField object
     * @return EQLReqOp object
     * @throws EQLException
     */
    private EQLReqOp buildDefaultSelectEQLReqOp(EQLReqField reqField)
            throws EQLException {

        Efield field = reqField.getField();
        String eqlDefSrc = field.getEqlDefsrc();

        // check: has field 'eql-defsrc' attribute
        EQLReqOp reqOp = null;
        if(eqlDefSrc != null) {
            reqOp = parseSrc(eqlDefSrc);
        } else {
            reqOp = new EQLReqOp();
            reqOp.addSubOp(new NoneOp(EQLNullObject.getInstance()));
        }

        return reqOp;
    }
}
