/*
 * 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.integrator.entity;

import com.queplix.core.client.app.vo.EntityData;
import com.queplix.core.client.app.vo.FieldData;
import com.queplix.core.client.app.vo.FieldMeta;
import com.queplix.core.client.app.vo.GridData;
import com.queplix.core.client.app.vo.RowData;
import com.queplix.core.integrator.ActionContext;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.integrator.security.User;
import com.queplix.core.modules.config.ejb.FocusConfigManagerLocal;
import com.queplix.core.modules.config.jxb.ExternalField;
import com.queplix.core.modules.config.jxb.ExternalSet;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class helps to build bindings among the forms.
 *
 * @author Sergey Kozmin
 * @since 24.11.2006, 12:42:33
 */
public class BoundFormsOperationsHelper {
    private static final AbstractLogger logger = Log.getLog(
            BoundFormsOperationsHelper.class);

    public static ResultFormStructure getClearBoundFormResult(String formName,
                                                              LogonSession ls,
                                                              ActionContext ctx)
            throws CouldntGetEJBException {
        ResultFormStructure fs = new ResultFormStructure();
        FocusConfigManagerLocal focusManager = ctx.getFocusConfigManager();
        User user = ls.getUser();

        try {
            List<ExternalFieldStructure> externalFieldStructures
                    = getBoundByExternalFields(formName, focusManager, ls, ctx);

            for(ExternalFieldStructure externalField : externalFieldStructures) {
                FieldData[] fieldDatas = new FieldData[]{
                        EntitySerializeHelper.createEmptyFieldData(
                                externalField.fieldMeta, user)
                };
                EntityData entityData = new EntityData(
                        EntityHelper.getFormEntityName(externalField.formName),
                        -1L, fieldDatas);
                fs.getFieldDatas().add(entityData);
            }
        } catch (Exception e) {
            logger.ERROR("Could not load clearing information. ", e);
        }
        return fs;
    }

    public static ResultFormStructure getBoundFormResult(String formName,
                                                         Long rowID,
                                                         LogonSession ls,
                                                         ActionContext ctx)
            throws CouldntGetEJBException {
        ResultFormStructure fs = new ResultFormStructure();
        FocusConfigManagerLocal focusManager = ctx.getFocusConfigManager();
        //temp object, used to break a linked to circle chain
        Set<String> passedEntities = new HashSet<String>();
        retrieveNextLevelBoundFormResult(formName,
                EntityOperationsHelper.getEntityNameFromFormID(formName), rowID,
                fs, passedEntities,
                focusManager, false, ls, ctx);
        return fs;
    }

    private static void retrieveNextLevelBoundFormResult(String formName,
                                                         String filteringEntityName,
                                                         Long filteringEntityID,
                                                         ResultFormStructure ret,
                                                         Set<String> passedEntities,
                                                         FocusConfigManagerLocal focusManager,
                                                         boolean addGridData,
                                                         LogonSession ls,
                                                         ActionContext ctx)
            throws CouldntGetEJBException {

        String requestedEntityName = EntityOperationsHelper
                .getEntityNameFromFormID(formName);
        if(!passedEntities.contains(requestedEntityName)) {
            passedEntities.add(requestedEntityName);
            try {
                IntegratedRecordSet result = EntityFacade.getEntityByIDRequest(
                        requestedEntityName, filteringEntityName,
                        filteringEntityID, ls, ctx);
                //fill main entity data
                if(result.getRowsCount() == 1) {
                    //get form result
                    Map<String, FieldData> dataMap = result.getRecordSet().get(
                            0).getFieldSet();
                    Collection<FieldData> datas = dataMap.values();
                    FieldData[] resultFields = datas.toArray(
                            new FieldData[datas.size()]);
                    String pkeyFieldName = result.getPkeyFieldName();
                    FieldData rowID = dataMap.get(pkeyFieldName);
                    Long pkey = Long.parseLong(
                            EntitySerializeHelper.getValueStringRepresentation(
                                    rowID, result.getRecordMeta().get(
                                    pkeyFieldName)));
                    ret.addEntityData(new EntityData(requestedEntityName, pkey,
                            resultFields));

                    if(addGridData) {//check, should we add data for grid. 
                        //retrieve data for grid
                        RowData[] records = EntityOperationsHelper
                                .retrieveGridRowsDataFromResult(result, ls,
                                        ctx);
                        ret.addGridData(new GridData(records,
                                requestedEntityName));
                    }

                    //go next external sets
                    List<String> boundForms = getBoundByExternalSets(formName,
                            focusManager);
                    for(String boundForm : boundForms) {
                        retrieveNextLevelBoundFormResult(boundForm,
                                requestedEntityName, pkey, ret, passedEntities,
                                focusManager, true, ls, ctx);
                    }
                    //parse external fields
                    List<ExternalFieldStructure> boundFields
                            = getBoundByExternalFields(formName, focusManager,
                            ls, ctx);
                    try {
                        retrieveExternalFieldValue(boundFields, result, ret,
                                requestedEntityName, ls, ctx);
                    } catch (Exception e) {
                        logger.ERROR(
                                "Could not load external fields data for entity ["
                                        + requestedEntityName + "], with id ["
                                        + filteringEntityID + "]", e);
                    }


                } else {//if there is no linked entities with this entity, fill response with empty data.
                    logger.INFO("There is no [" + requestedEntityName
                            + "], entities found linked with entity ["
                            + filteringEntityName + "], with id ["
                            + filteringEntityID
                            + "]. Fill external form with empty values. ");
                    Map<String, FieldMeta> fieldsMeta = result.getRecordMeta();
                    for(String key : fieldsMeta.keySet()) {
                        ret.addFieldData(new EntityData(requestedEntityName,
                                -1L,
                                new FieldData[]{
                                        EntitySerializeHelper.createEmptyFieldData(
                                                fieldsMeta.get(key),
                                                ls.getUser())
                                }));
                    }
                }
            } catch (Exception e) {
                logger.ERROR("Could not find entity data for entity ["
                        + requestedEntityName + "], with id ["
                        + filteringEntityID + "]", e);
            }
        }
    }

    private static void retrieveExternalFieldValue(
            List<ExternalFieldStructure> boundFields,
            IntegratedRecordSet baseResultSet,
            ResultFormStructure ret,
            String requestedEntityName,
            LogonSession ls,
            ActionContext ctx)
            throws EQLException {
        //go next external fields
        for(ExternalFieldStructure field : boundFields) {

            FieldMeta sourceFieldMeta = baseResultSet.getRecordMeta().get(
                    field.sourceFieldName);
            FieldData sourceFieldData = baseResultSet.getRecordSet().get(0)
                    .getFieldSet().get(field.sourceFieldName);

            Long sourceRowId = EntitySerializeHelper.retrieveRowId(
                    sourceFieldMeta, sourceFieldData);

            if(sourceRowId
                    != null) {//if source field was filled out, otherwise do nothing
                //make request to retrieve entity filter name
                IntegratedRecordSet recordSet = EntityFacade
                        .getEntityByIDRequest(field.referencedEntityName,
                                sourceRowId, ls, ctx);
                if(recordSet.getRowsCount() > 0) {
                    //parse the entity to get listref field value
                    FieldMeta listRefFieldMeta = recordSet.getRecordMeta().get(
                            recordSet.getListRefFieldName());
                    Map<String, FieldData> referencedDataMap = recordSet
                            .getRecordSet().get(0).getFieldSet();
                    FieldData listRefFieldData = referencedDataMap.get(
                            recordSet.getListRefFieldName());
                    String listRefEntityRepresentation = EntitySerializeHelper
                            .getStringRepresentationForGrid(listRefFieldData,
                                    listRefFieldMeta, ls, ctx);

                    //create field data for external field according to it's type
                    FieldData fieldData = EntitySerializeHelper
                            .createExternalFieldDataForPkey(field.fieldMeta,
                                    sourceRowId, listRefEntityRepresentation);

                    ret.addFieldData(new EntityData(
                            EntityOperationsHelper.getEntityNameFromFormID(
                                    field.formName), -1L,
                            new FieldData[]{fieldData}));
                } else {
                    logger.ERROR("Could not find entity ["
                            + field.referencedEntityName +
                            "] with id [" + sourceRowId
                            + "]. Please, check that entity [" +
                            field.referencedEntityName
                            + "] and [" + requestedEntityName +
                            "] are mapped to the same DB table. ");
                }
            }
        }
    }

    private static List<String> getBoundByExternalSets(String formName,
                                                       FocusConfigManagerLocal focusManager) {
        List<String> list = new LinkedList<String>();
        List<ExternalSet> externalSets = focusManager
                .getUnmodifiableExternalSet(formName);
        if(externalSets != null) {
            for(ExternalSet externalForm : externalSets) {
                list.add(externalForm.getName());
            }
        } else {
            logger.WARN("Referenced form [" + formName
                    + "], doesn't exists, or cannot be found. ");
        }
        return list;
    }

    /**
     * Retrieve all information about external fields for the given form #formName.
     *
     * @param formName     form to be processed
     * @param focusManager focus manager
     * @param ls           logon session
     * @param ctx          servlet contectx
     * @return list of the {@link ExternalFieldStructure} elements
     * @throws CouldntGetEJBException thrown if ejb could not be got
     * @throws IncorrectEntityDescriptionException
     *                                thrown if incorrect entity description is found
     */
    private static List<ExternalFieldStructure> getBoundByExternalFields(
            String formName, FocusConfigManagerLocal focusManager,
            LogonSession ls, ActionContext ctx)
            throws CouldntGetEJBException, IncorrectEntityDescriptionException {

        List<ExternalFieldStructure> list
                = new LinkedList<ExternalFieldStructure>();
        List<ExternalField> externalFields = focusManager
                .getUnmodifiableExternalFields(formName);
        if(externalFields != null) {
            //iterate it
            for(ExternalField externalField : externalFields) {
                //retrieve entity name, where this external field is referencing
                String entityName = EntityOperationsHelper
                        .getEntityNameFromFormID(externalField.getForm());
                //retrieve metadata for this entity
                Map<String, FieldMeta> meta = EntityViewHelper.getMetaForEntity(
                        entityName, EntityViewHelper.FieldsModificator.FORM,
                        false, ls, ctx);
                //retrieve entity name, that is shown in efield as referencing entity.
                String referencedEntityName = EntityViewHelper
                        .getListRefEntityName(entityName,
                                externalField.getName(), ctx);
                if(referencedEntityName == null) {//incorrect entity description
                    throw new IncorrectEntityDescriptionException(entityName,
                            "There is no such field ["
                                    + externalField.getName()
                                    + "] defined in entity [" + entityName
                                    + "].");
                } else {//fill up structure element
                    //get list field name for the referenced entity
                    String listrefFieldName = EntityViewHelper
                            .getListFieldNameForEntity(referencedEntityName,
                                    ctx);
                    //field meta for external field in external form. 
                    FieldMeta fieldMeta = meta.get(externalField.getName());

                    String sourceField = externalField.getSourceField();
                    if(StringHelper.isEmpty(sourceField)) {
                        String thisEntityName = EntityOperationsHelper.
                                getEntityNameFromFormID(formName);

                        sourceField = EntityViewHelper.getPkeyID(thisEntityName,
                                ctx);
                    }

                    if(fieldMeta != null) {
                        ExternalFieldStructure structure
                                = new ExternalFieldStructure(
                                externalField.getForm(),
                                externalField.getName(), fieldMeta,
                                referencedEntityName, listrefFieldName,
                                sourceField);
                        list.add(structure);
                    } else {
                        logger.ERROR("Field [" + externalField.getName()
                                + "], wasn't found in entity ["
                                + entityName +
                                "], external field property won't be included to resultset. ");
                    }
                }
            }
        } else {
            logger.WARN("Referenced form [" + formName
                    + "], doesn't exists, or cannot be found. ");
        }
        return list;
    }

    public static class ResultFormStructure {
        /**
         * external sets data
         */
        private List<EntityData> entityDatas;
        /**
         * external fields data
         */
        private List<EntityData> fieldDatas;
        /**
         * grid data objects
         */
        private List<GridData> gridData;

        public ResultFormStructure(List<EntityData> entityDatas,
                                   List<EntityData> fieldDatas,
                                   List<GridData> gridData) {
            if(entityDatas != null) {
                this.entityDatas = entityDatas;
            } else {
                this.entityDatas = new LinkedList<EntityData>();
            }
            if(fieldDatas != null) {
                this.fieldDatas = fieldDatas;
            } else {
                this.fieldDatas = new LinkedList<EntityData>();
            }
            if(gridData != null) {
                this.gridData = gridData;
            } else {
                this.gridData
                        = new ArrayList<GridData>();//will be transferred to GWT hence should be ArrayList, not LinkedList
            }
        }

        public ResultFormStructure() {
            this(null, null, null);
        }

        public List<EntityData> getEntityDatas() {
            return entityDatas;
        }

        public List<EntityData> getFieldDatas() {
            return fieldDatas;
        }

        public List<GridData> getGridData() {
            return gridData;
        }

        public void addEntityData(EntityData data) {
            entityDatas.add(data);
        }

        public void addFieldData(EntityData data) {
            fieldDatas.add(data);
        }

        public void addGridData(GridData data) {
            gridData.add(data);
        }
    }

    private static class ExternalFieldStructure {
        /**
         * external form name
         */
        public String formName;
        /**
         * field name in external form
         */
        public String fieldName;
        /**
         * field meta in external form
         */
        public FieldMeta fieldMeta;
        /**
         * The field, that is shown as external-field actually can be one of the
         * following: EntityReference, ListBox. All such fields should have
         * <listref entity="users.security"/> value. "users.security" is this
         * value here.
         */
        public String referencedEntityName;
        /**
         * @see #referencedEntityName descr. #listrefFieldName is the field
         *      that is shown as listref="field_name"
         */
        public String listrefFieldName;
        /**
         * Field name from where we should get pkey. By default it's pkey.
         */
        public String sourceFieldName;

        public ExternalFieldStructure(String formName, String fieldName,
                                      FieldMeta fieldMeta,
                                      String referencedEntityName,
                                      String listrefFieldName,
                                      String sourceFieldName) {
            this.formName = formName;
            this.fieldName = fieldName;
            this.fieldMeta = fieldMeta;
            this.referencedEntityName = referencedEntityName;
            this.listrefFieldName = listrefFieldName;
            this.sourceFieldName = sourceFieldName;
        }
    }
}