/*
 * 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.jxb.entity.ChainTable;
import com.queplix.core.jxb.entity.Chainref;
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.Ref;
import com.queplix.core.jxb.entity.types.RefSType;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.EQLReq;
import com.queplix.core.modules.eql.EQLReqEntity;
import com.queplix.core.modules.eql.EQLReqField;
import com.queplix.core.modules.eql.EQLReqJoin;
import com.queplix.core.modules.eql.EQLReqSubWhere;
import com.queplix.core.modules.eql.EQLReqSubWhereCond;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eql.error.EQLSystemException;

import java.util.ArrayList;
import java.util.List;

/**
 * <p>EQL interpreter generic implementation</p>
 *
 * @author Baranov Andrey [ALB]
 * @version $Revision: 1.2 $ $Date: 2006/06/05 12:50:42 $
 */

public class EQLInterpreterGenericImpl
        extends AbstractEQLInterpreter {

    // ------------------------------------------------------- Variables

    // indicates whether current interpreter clone or not
    private boolean isClonned;

    // reference on parent mediator for clone
    private EQLIntMediator parentMediator;

    // ------------------------------------------------------- Cloneable methods

    /* (non-Javadoc)
     * @see AbstractEQLInterpreter
     */

    public Object clone() {
        EQLInterpreterGenericImpl _new = new EQLInterpreterGenericImpl();
        _new.setSession(getSession());
        _new.isClonned = true;
        _new.parentMediator = getMainMediator();

        if(getPreparedStatement() != null) {
            _new.setPreparedStatement(getPreparedStatement());
        }

        return _new;
    }

    /* (non-Javadoc)
     * @see AbstractEQLInterpreter
     */
    public boolean isCloned() {
        return isClonned;
    }

    // ------------------------------------------------------- EQLRequestBuilder methods

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */

    public void addEntity(EQLReqEntity reqEntity)
            throws EQLException {

        // fast checking: if entity alredy added - skip procedure
        if(getMainMediator().getEQLReq().getFrom().contains(reqEntity)) {
            return;
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("[#] Add entity: " + reqEntity);
        }

        // chain current entity with others
        addChainTab(reqEntity, null);

        // add current entity in FROM list
        addEntityInFromList(reqEntity, true);
    }

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */
    public EQLReqField addField(EQLReqField reqField)
            throws EQLException {
        return addField(reqField, true);
    }

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */
    public EQLReqField addField(EQLReqField reqField, boolean withRef)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG("[#] Add field: " + reqField + ", withRef?=" + withRef);
        }

        EQLReqEntity reqEntity = reqField.getReqEntity();
        Efield field = reqField.getField();

        // add entity
        addEntity(reqEntity);

        Ref ref = field.getRef();
        if(withRef && ref != null) {
            Entity refEntity = session.findEntity(ref.getEntity());
            Efield refField = EntityHelper.getEfield(ref.getEfield(),
                    refEntity);

            EQLReqEntity refReqEntity = new EQLReqEntity(refEntity, reqField);
            EQLReqField refReqField = new EQLReqField(refReqEntity, refField);

            // chain Ref entity - use original field
            addChainTab(refReqEntity, reqField);

            // [!!!] return reference, not original field
            return refReqField;

        } else {
            return reqField;
        }
    }

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */
    public EQLReqField addListField(EQLReqField reqField)
            throws EQLException {

        if(getLogger().isDebugEnabled()) {
            DEBUG("[#] Add list field: " + reqField);

        }
        Listref listRef = reqField.getField().getListref();
        if(listRef == null) {
            throw new EQLSystemException(
                    "Field '" + reqField.getField().getId() +
                            "' does not have list reference");
        }

        // first - add reqField itself
        // [!!!] it can be added as Ref, so remember it
        EQLReqField newReqField = addField(reqField);

        boolean join = reqField.getField().getJoin().booleanValue();
        if(join) {
            // get listref entity and field
            Entity lrefEntity = session.findEntity(listRef.getEntity());
            Efield lrefField = EntityHelper.getEfield(listRef.getEfield(),
                    lrefEntity);

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

            // add dataschema - use original field
            addDataschema(newReqField.getReqEntity(), lrefReqEntity,
                    listRef.getDataschema(), reqField);

            return listReqField;
        } else {
            return newReqField;
        }

    }

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */
    public boolean addJoin(List reqFields1, List reqFields2,
                           EQLReqField leftReqField)
            throws EQLException {

        EQLReq req = getMainMediator().getEQLReq();

        // try to set join type using 'required' attribute from left field(s)
        int joinType;
        boolean required = true;
        if(leftReqField != null) {
            // .. left field is present - take 'required attribute from there
            required = leftReqField.getField().getRequired().booleanValue();

        } else {
            // .. try to check all left fields and find out not required field
            int size = reqFields1.size();
            for(int i = 0; i < size; i++) {
                EQLReqField reqField1 = (EQLReqField) reqFields1.get(i);
                if(!reqField1.getField().getRequired().booleanValue()) {
                    required = false;
                    break;
                }
            }
        }
        if(required) {
            joinType = EQLReqJoin.INNER_JOIN;
        } else {
            joinType = EQLReqJoin.OUTER_JOIN;
        }

        // create new EQLReqJoin
        EQLReqJoin reqJoin = new EQLReqJoin(reqFields1, reqFields2, joinType);
        EQLReqEntity leftReqEntity = reqJoin.getLeftEntity();
        EQLReqEntity rightReqEntity = reqJoin.getRightEntity();

        if(getLogger().isDebugEnabled()) {
            DEBUG("Try to add JOIN");
            DEBUG("   right entity: " + leftReqEntity);
            DEBUG("   left entity: " + rightReqEntity);
            DEBUG("   join type: " + joinType);
        }

        if(req.getFrom().contains(reqJoin)) {
            return false;
        }

        // add right entity in FROM list
        // NOTE: [ALB] Quick fix - add right entity CONSTRAINT.
        String eqlConstraintQuery = rightReqEntity.getEntity()
                .getEqlConstraint();
        if(eqlConstraintQuery != null) {
            EQLInterpreterGenericImpl _interpreter
                    = (EQLInterpreterGenericImpl) clone();
            _interpreter.addEntityInFromList(rightReqEntity, true);
            reqJoin.setReqWhere(
                    _interpreter.getMainMediator().getEQLReq().getWhere());
        }

        // add right entity in FROM list without CONSTRAINT
        addEntityInFromList(rightReqEntity, false);

        // add  EQLReqJoin in JOIN list
        req.getFrom().addJoinEntity(reqJoin);

        return true;
    }

    /* (non-Javadoc)
     * @see EQLRequestBuilder
     */
    public boolean addWhere(EQLReqField reqField, EQLReqSubWhereCond cond)
            throws EQLException {

        EQLIntMediator mediator = getMainMediator();
        EQLReq req = mediator.getEQLReq();

        if(getLogger().isDebugEnabled()) {
            DEBUG("Try to add WHERE");
            DEBUG("   field: " + reqField);
            DEBUG("   right member: " + cond.getRightMember());
            DEBUG("   left member: " + cond.getLeftMember());
        }

        // create new EQLReqSubWhere object
        EQLReqSubWhere reqSubWhere = new EQLReqSubWhere(EQLReqSubWhere.NONE_OP,
                cond);
        if(req.getWhere().contains(reqSubWhere)) {
            return false;
        }

        // add entity in FROM list
        addEntityInFromList(reqField.getReqEntity(), true);

        // add EQLReqSubWhere in WHERE list
        req.getWhere().addSubWhere(reqSubWhere);

        return true;
    }

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

    /**
     * Attempt to find suited dataschemas for given entity and
     * other entities using <code>Chain</code> objects
     *
     * @param rightReqEntity right (slave) entity structure
     * @param leftField      indicates field we need to add join (optional)
     * @throws EQLException if can't join entities
     */
    private void addChainTab(EQLReqEntity rightReqEntity, EQLReqField leftField)
            throws EQLException {

        // list of already added entities
        EQLReq req = getMainMediator().getEQLReq();
        ArrayList entityFromList = req.getFrom().getFromEntities();
        int count = entityFromList.size();

        // empty list
        boolean find = false;
        if(count == 0) {
            find = true;
        }

        // main cycle
        int i = 0;
        while(!find && i < count) {
            EQLReqEntity leftReqEntity = (EQLReqEntity) entityFromList.get(i++);

            if(getLogger().isDebugEnabled()) {
                DEBUG("Try to chain left entity [" + leftReqEntity
                        + "] and right entity [" + rightReqEntity + "]");

            }
            find = addChainTab(leftReqEntity, rightReqEntity, leftField);
        }

        if(!find) {
            throw new EQLException("Can't join entity '"
                    + rightReqEntity.getEntity().getName() +
                    "' with:\n" + entityFromList);
        }
    }

    /**
     * Add Dataschema objects in list from ChainTable
     *
     * @param leftReqEntity  left (master) entity structure
     * @param rightReqEntity right (slave) entity structure
     * @param leftReqField   for what field we're adding chain (optional)
     * @return true if added
     * @throws EQLException
     */
    private boolean addChainTab(EQLReqEntity leftReqEntity,
                                EQLReqEntity rightReqEntity,
                                EQLReqField leftReqField)
            throws EQLException {

        Entity leftEntity = leftReqEntity.getEntity();
        Entity rightEntity = rightReqEntity.getEntity();
        if(leftReqEntity.equals(rightReqEntity)) {
            return true;
        }

        String leftEntityName = leftEntity.getName();
        String rightEntityName = rightEntity.getName();

        ChainTable ctab = EntityHelper.getChainTable(rightEntityName,
                leftEntity);
        if(ctab == null) {
            WARN("There are no any chains between entity '" + leftEntityName
                    + "' and entity '" + rightEntityName + "'");
            return false;
        }

        Chainref chainRef = ctab.getChainref();
        Dataschema ds = ctab.getDataschema();

        if(chainRef != null) {
            // we have another chain
            String chainEntityName = chainRef.getEntity();
            Entity chainEntity = session.findEntity(chainEntityName);
            EQLReqEntity chainReqEntity = new EQLReqEntity(chainEntity);

            if(getLogger().isDebugEnabled()) {
                DEBUG("Find external chain using chain entity: "
                        + chainEntityName);
            }

            // find new chain between e1 and chain entity
            boolean ret1 = addChainTab(leftReqEntity, chainReqEntity,
                    leftReqField);
            if(getLogger().isDebugEnabled()) {
                DEBUG("   1 between '" + leftEntityName + "' and '"
                        + chainEntityName + "' ret=" + ret1);
            }
            if(!ret1) {
                return false;
            }

            // find new chain between chain entity and e2
            boolean ret2 = addChainTab(chainReqEntity, rightReqEntity,
                    leftReqField);
            if(getLogger().isDebugEnabled()) {
                DEBUG("   2 between '" + chainEntityName + "' and '"
                        + rightEntityName + "' ret=" + ret2);
            }
            if(!ret2) {
                return false;
            }

        } else if(ds != null) {
            // add Dataschema object
            addDataschema(leftReqEntity, rightReqEntity, ds, leftReqField);

        } else {
            // add right entity in FROM list only
            addEntityInFromList(rightReqEntity, true);
        }

        return true;
    }

    //
    // Add dataschema
    //
    private void addDataschema(EQLReqEntity reqEntity1,
                               EQLReqEntity reqEntity2,
                               Dataschema ds,
                               EQLReqField leftReqField)
            throws EQLException {

        String entityName1 = reqEntity1.getEntity().getName();
        String entityName2 = reqEntity2.getEntity().getName();
        String[] keys1 = ds.getTable(0).getKey();
        String[] keys2 = ds.getTable(1).getKey();
        int keySize = keys1.length;

        ArrayList reqFields1 = new ArrayList(keySize);
        ArrayList reqFields2 = new ArrayList(keySize);

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

            reqFields1.add(new EQLReqField(reqEntity1, efield1));
            reqFields2.add(new EQLReqField(reqEntity2, efield2));
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("Try to add dataschema:");
            DEBUG("   name: " + ds.getName());
            DEBUG("   ref type=" + ds.getReftype());
            DEBUG("   left entity=" + entityName1);
            DEBUG("   right entity=" + entityName2);
        }

        // check if dataset reference type = '1:n'
        if(ds.getReftype().getType() == RefSType.VALUE_1_TYPE) {
            // add DISTINCT
            getMainMediator().getEQLReq().getSelect().setIsDistinct(true);
        }

        // add JOIN clause
        addJoin(reqFields1, reqFields2, leftReqField);
    }

    //
    // Add entity in EQL from list
    //
    private void addEntityInFromList(EQLReqEntity reqEntity,
                                     boolean addConstraint)
            throws EQLException {

        EQLIntMediator mediator = getMainMediator();

        EQLReq req = mediator.getEQLReq();
        if(req.getFrom().contains(reqEntity)) {
            return;
        }

        if(isClonned) {
            // for clonned EQL interpreter check reqEntity in parent EQLReq object
            // if found -- don't add
            // [!!!] check only one nested level
            if(parentMediator.getEQLReq().getFrom().contains(reqEntity)) {
                if(getLogger().isDebugEnabled()) {
                    DEBUG("Entity '" + reqEntity
                            + "' found in parent mediator. Skip...");
                }
                return;
            }
        }

        // add entity
        req.getFrom().addFromEntity(reqEntity);

        if(addConstraint) {
            // add entity CONSTRAINT
            String eqlConstraintQuery = reqEntity.getEntity()
                    .getEqlConstraint();
            if(eqlConstraintQuery != null) {
                mediator.parseWhere(eqlConstraintQuery);
            }
        }
    }
}
