/*
 * 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.eqlext.utils;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.integrator.security.AccessRightsManager;
import com.queplix.core.jxb.entity.Efield;
import com.queplix.core.jxb.entity.Entity;
import com.queplix.core.modules.config.ejb.EntityViewConfigManagerLocal;
import com.queplix.core.modules.config.ejb.EntityViewConfigManagerLocalHome;
import com.queplix.core.modules.config.error.UnknownEfieldException;
import com.queplix.core.modules.config.error.UnknownEntityException;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.CompoundKey;
import com.queplix.core.modules.eql.ejb.HistoryLocal;
import com.queplix.core.modules.eql.ejb.HistoryLocalHome;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eql.error.UserQueryParseException;
import com.queplix.core.modules.eql.history.HistoryConstants;
import com.queplix.core.modules.eqlext.actions.GRAction;
import com.queplix.core.modules.eqlext.ejb.GetRecordsLocal;
import com.queplix.core.modules.eqlext.ejb.GetRecordsLocalHome;
import com.queplix.core.modules.eqlext.error.FormTransformException;
import com.queplix.core.modules.eqlext.jxb.gr.ResField;
import com.queplix.core.modules.eqlext.jxb.gr.ResRecord;
import com.queplix.core.modules.eqlext.jxb.gr.Ress;
import com.queplix.core.utils.JNDINames;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.cache.CacheObjectManager;
import com.queplix.core.utils.log.AbstractLogger;
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Helper to work with forms (templates)
 *
 * @author [ALB] Baranov Andrey
 * @author [SVS] Sorokin Sergey
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:42 $
 */

public class FormsManager
        extends AbstractLogger {

    // ------------------------------------------------------- constants

    // Empty value.
    private static final String EMPTY_VALUE = "";

    // Fake compound key.
    private static final String DUMMY_PKEY = "-99999";
    private static final CompoundKey dummyCompoundPkey = new CompoundKey();

    static {
        dummyCompoundPkey.addKey(DUMMY_PKEY);
    }

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

    private String baseEntityName;
    private CompoundKey key;
    private boolean isHtml;

    private final Map entityDataMap = new HashMap();

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

    /**
     * Constructor
     */
    public FormsManager() {
        this(null);
    }

    /**
     * Constructor
     *
     * @param baseEntityName base entity name
     */
    public FormsManager(String baseEntityName) {
        this(baseEntityName, dummyCompoundPkey);
    }

    /**
     * Constructor
     *
     * @param baseEntityName base entity name
     * @param key            compound key
     */
    public FormsManager(String baseEntityName, CompoundKey key) {
        this.baseEntityName = baseEntityName;
        this.key = key;
    }

    /**
     * Translate form (template)
     *
     * @param form Form text
     * @return Translated form
     * @throws FormTransformException
     * @todo add strict parameter
     */
    public String translate(String form)
            throws FormTransformException {

        // init TagIterator object
        TagIterator it = new TagIterator(form);

        // set IsHtml flag
        this.isHtml = it.isHtml;

        // parse form in cycle
        int i = 0;
        while(it.hasNext()) {
            String body = it.getNextBody();

            // set base entity if empty
            if(i == 0 && baseEntityName == null) {
                this.baseEntityName = it.getEntityName();
            }

            if(getLogger().isDebugEnabled()) {
                DEBUG("#" + i);
                DEBUG("=== HTML?:" + isHtml);
                DEBUG("=== Base entity name : " + baseEntityName);
                DEBUG("=== Entity name : " + it.getEntityName());
                DEBUG("=== Body:" + body);
            }

            // translate next body
            String translatedBody;
            try {
                translatedBody = translateBody(it.getEntityName(), body, true);
            } catch (UnknownEntityException ex) {
                ERROR(ex);
                throw new FormTransformException(
                        "Unsupported entity tag <" + it.getEntityName() + ">.");
            }

            // add translated body in form
            if(translatedBody != null) {
                if(getLogger().isDebugEnabled()) {
                    DEBUG("=== Translated: " + translatedBody);
                }
                it.replaceBody(translatedBody);
            } else {
                it.skipBody();
            }

            i++;
        }

        return it.getForm();
    }

    /**
     * Validate form
     *
     * @param form Form string
     * @throws FormTransformException
     */
    public static void validate(String form)
            throws FormTransformException {

        TagIterator bodies = new TagIterator(form);
        while(bodies.hasNext()) {
            bodies.getNextBody();
        }
    }

    /**
     * Translate form (template) body only
     *
     * @param body   given template
     * @param strict strict checking or not
     * @return result of translation
     * @throws FormTransformException
     * @throws UnknownEntityException
     */
    public String translateBody(String body, boolean strict)
            throws UnknownEntityException, FormTransformException {

        // set IsHtml flag
        this.isHtml = StringHelper.isHTML(body);

        return translateBody(baseEntityName, body, strict);
    }

    /**
     * Return true if the last form in translation was in a HTML format
     *
     * @return true of false
     */
    public boolean isLastFormInHTMLFormat() {
        return isHtml;
    }

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

    /**
     * Translate form (template) body only
     *
     * @param entityName entity
     * @param body       given template
     * @param strict     strict checking or not
     * @return result of translation
     * @throws FormTransformException
     * @throws UnknownEntityException
     */
    private String translateBody(String entityName, String body, boolean strict)
            throws UnknownEntityException, FormTransformException {

        Entity entity = getEntity(entityName);
        if(StringHelper.isEmpty(body)) {
            return body;
        }

        try {
            RE re;
            if(isHtml) {
                // HTML
                re = new RE("&lt;([^ \t\n&;]*)&gt;", RE.MATCH_CASEINDEPENDENT);
            } else {
                // plain text
                re = new RE("<([^ \t\n><]*)>", RE.MATCH_CASEINDEPENDENT);
            }

            int curPos = 0;
            while(re.match(body, curPos)) {
                int startPos = re.getParenStart(0);
                int endPos = re.getParenEnd(0);

                int startFieldPos = re.getParenStart(1);
                int endFieldPos = re.getParenEnd(1);

                String fieldName = body.substring(startFieldPos, endFieldPos);

                if(getLogger().isDebugEnabled()) {
                    DEBUG("Find field:");
                    DEBUG("	entity '" + entityName + "'");
                    DEBUG("	field '" + fieldName + "'");
                }

                Efield field;
                try {
                    field = getEfield(entity, fieldName);
                } catch (UnknownEfieldException ex) {
                    if(strict) {
                        ERROR(ex);
                        throw new FormTransformException(
                                "Unsupported field tag <" +
                                        fieldName + "> inside entity tag <"
                                        + entityName + ">.");
                    } else {
                        WARN(ex.getMessage());
                        curPos = endFieldPos;
                        continue;
                    }
                }

                String value;
                try {
                    value = getFieldData(entity, field);
                } catch (EQLException ex) {
                    if(strict) {
                        ERROR(ex);
                        throw new FormTransformException(
                                "Cannot replace field tag <" +
                                        fieldName + "> inside entity tag <"
                                        + entityName + ">.");
                    } else {
                        WARN(ex.getMessage());
                        curPos = endFieldPos;
                        continue;
                    }
                }

                String head = body.substring(0, startPos);
                String tail = body.substring(endPos);
                body = head + value + tail;
                curPos = startPos + ((value == null) ? 0:value.length());
            }

        } catch (RESyntaxException ex) {
            ERROR(ex);
            throw new GenericSystemException(
                    "RegExp exception: " + ex.getMessage(), ex);
        }

        if(getLogger().isDebugEnabled()) {
            DEBUG("Final body '" + body + "'");
        }

        return body;
    }

    //
    // Retrive field value from cache
    // If no data found return EMPTY_VALUE
    //
    private String getFieldData(Entity entity, Efield field)
            throws EQLException {

        String fieldName = field.getName();

        // get cache object
        EntityData entityData = getEntityData(entity);
        if(entityData == null) {
            throw new NullPointerException(
                    "Cannot find EntityData for entity '" +
                            entity.getName() + "'");
        }

        ResField resField = entityData.getResField(fieldName);
        if(resField == null) {
            throw new NullPointerException("Cannot find ResField for field '" +
                    field.getId() + "'");
        }

        // get value
        String value = null;

        if(fieldName.equals(HistoryConstants.HISTORY_FIELD)) {
            // field is history
            if(isHtml) {
                value = getHistoryLocal().toHTML(resField.getResFieldValue());
            } else {
                value = getHistoryLocal().toText(resField.getResFieldValue());
            }

        } else if(field.getListref() != null) {
            // field has listfield
            value = resField.getResFieldText();

        } else {
            // odinary field
            value = resField.getResFieldValue();
        }

        if(StringHelper.isEmpty(value)) {
            value = EMPTY_VALUE;
        } else if(isHtml && !StringHelper.isHTML(value)) {
            value = StringHelper.escape(value);
        } else if(!isHtml && StringHelper.isHTML(value)) {
            value = StringHelper.html2text(value);
        }

        return value;
    }

    //
    // Load data for entity, cach it and return EntityData object
    //
    private EntityData getEntityData(Entity entity)
            throws EQLException {

        String entityName = entity.getName();

        // try to find in cache
        EntityData entityData = (EntityData) entityDataMap.get(entityName);

        if(entityData == null) {
            // load data throw GetRecords
            Properties prop = new Properties();
            prop.setProperty(GRAction.IGNORE_SEND_ON_REQ_PARAM, "true");

            Ress ress = null;
            try {
                ress = getGetRecordsLocal().process(
                        entityName,
                        baseEntityName,
                        new CompoundKey[]{key},
                        prop,
                        AccessRightsManager.getSystemLogonSession()
                ).getRess();

            } catch (UserQueryParseException ex) {
                ERROR(ex);
                throw ex;
            }

            // ...and cache result
            entityData = new EntityData(ress);
            entityDataMap.put(entityName, entityData);
        }

        return entityData;
    }

    //
    // Get entity by the name
    //
    private Entity getEntity(String entityName)
            throws UnknownEntityException {

        try {
            return getEntityViewConfigManagerLocal().getEntityViewConfig(
                    entityName);
        } catch (Exception ex) {
            if(ex instanceof UnknownEntityException) {
                throw (UnknownEntityException) ex;
            } else {
                throw new UnknownEntityException(entityName);
            }
        }
    }

    //
    // Get field by the name
    //
    private Efield getEfield(Entity entity, String fieldName)
            throws UnknownEfieldException {

        try {
            return EntityHelper.getEfield(fieldName, entity);
        } catch (UnknownEfieldException ex) {
            if(ex instanceof UnknownEfieldException) {
                throw (UnknownEfieldException) ex;
            } else {
                throw new UnknownEfieldException(entity.getName(), fieldName);
            }
        }
    }

    //
    // Get EntityViewConfigManager local interface
    //
    private EntityViewConfigManagerLocal getEntityViewConfigManagerLocal() {
        return (EntityViewConfigManagerLocal) new CacheObjectManager().
                getLocalObject(JNDINames.EntityViewConfigManager,
                        EntityViewConfigManagerLocalHome.class);
    }

    //
    // Get GetRecords local interface
    //
    private GetRecordsLocal getGetRecordsLocal() {
        return (GetRecordsLocal) new CacheObjectManager().
                getLocalObject(JNDINames.GetRecords, GetRecordsLocalHome.class);
    }

    //
    // Get History local interface
    //
    private HistoryLocal getHistoryLocal() {
        return (HistoryLocal) new CacheObjectManager().
                getLocalObject(JNDINames.History, HistoryLocalHome.class);
    }

    // ------------------------------------------------------- inner class

    //
    // Contains cached entity data
    //

    private static class EntityData
            implements java.io.Serializable {

        private Map resFieldMap = new HashMap();

        // constructor
        public EntityData(Ress ress) {
            ResRecord resRecord = ress.getRes().getResRecord(0);
            if(resRecord != null) {
                // indexing ResField objects
                ResField[] resFields = resRecord.getResField();
                for(int i = 0; i < resFields.length; i++) {
                    resFieldMap.put(resFields[i].getName(), resFields[i]);
                }
            }
        }

        // get ResField object by the name
        public ResField getResField(String fieldName) {
            return (ResField) resFieldMap.get(fieldName);
        }

    } //-- end of inner class

    //
    // Body Tag Iterator
    //

    public static class TagIterator {

        String form;
        final boolean isHtml;

        String entityName;
        int curPos = 0;
        int pos1 = 0;
        int pos2 = 0;

        final RE re1;

        private TagIterator(String form)
                throws FormTransformException {

            this.form = form;
            this.isHtml = StringHelper.isHTML(form);

            //
            // Build pattern substituion.
            //

            try {
                if(isHtml) {
                    // HTML
                    re1 = new RE("&lt;([^ \t\n&;]*)&gt;",
                            RE.MATCH_CASEINDEPENDENT);
                } else {
                    // plain text
                    re1 = new RE("<([^ \t\n><]*)>", RE.MATCH_CASEINDEPENDENT);
                }
            } catch (RESyntaxException ex) {
                throw new GenericSystemException(
                        "RegExp exception: " + ex.getMessage(), ex);
            }
        }

        boolean hasNext() {
            if(form == null || form.length() <= curPos) {
                return false;
            } else {
                return re1.match(form, curPos);
            }
        }

        String getNextBody()
                throws FormTransformException {

            try {
                int startPos1 = re1.getParenStart(0);
                int endPos1 = re1.getParenEnd(0);

                int startOpenBracket = re1.getParenStart(1);
                int endOpenBracket = re1.getParenEnd(1);

                // find open element - retrive entity name
                entityName = form.substring(startOpenBracket, endOpenBracket);

                RE re2;
                if(isHtml) {
                    re2 = new RE("&lt;/" + entityName + "&gt;",
                            RE.MATCH_CASEINDEPENDENT);
                } else {
                    re2 = new RE("</" + entityName + ">",
                            RE.MATCH_CASEINDEPENDENT);
                }

                if(!re2.match(form, endPos1)) {
                    // can't find close element - throw exception
                    throw new FormTransformException(
                            "Cannot find closed </" + entityName + "> tag.");
                }

                // we find closed element - get this block
                int startPos2 = re2.getParenStart(0);
                int endPos2 = re2.getParenEnd(0);
                pos1 = startPos1;
                pos2 = endPos2;
                return form.substring(endPos1, startPos2);

            } catch (RESyntaxException ex) {
                throw new GenericSystemException(
                        "RegExp exception: " + ex.getMessage(), ex);
            }
        }

        // replace block
        void replaceBody(String translatedBody) {
            String head = form.substring(0, pos1);
            String tail = form.substring(pos2);
            form = head + translatedBody + tail;
            curPos = pos1 + translatedBody.length();
        }

        void skipBody() {
            curPos = pos2;
        }

        String getEntityName() {
            return entityName;
        }

        String getForm() {
            return form;
        }

    } //-- end of inner class
}
