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

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.integrator.security.AccessRightsManager;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.integrator.security.NoSuchUserException;
import com.queplix.core.integrator.security.User;
import com.queplix.core.jxb.entity.Efield;
import com.queplix.core.jxb.entity.Entity;
import com.queplix.core.modules.config.ejb.CaptionManagerLocal;
import com.queplix.core.modules.config.utils.SysPropertyManager;
import com.queplix.core.modules.eql.EQLDRes;
import com.queplix.core.modules.eql.EQLERes;
import com.queplix.core.modules.eql.EQLResCell;
import com.queplix.core.modules.eql.EQLResRecord;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eql.error.EQLLockException;
import com.queplix.core.modules.eql.error.EQLLockExpiredException;
import com.queplix.core.utils.DateHelper;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.sql.SqlWrapper;
import com.queplix.core.utils.sql.error.SQLDuplicateKeyException;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Locking operations manager.
 *
 * @author [SVM] Maxim Suponya
 * @author [ALB] Baranov Andrey
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:25 $
 */

public class LockManagerEJB
        extends AbstractEQLSupportedEJB {

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

    // Null key value
    protected static final String NULL_VALUE = StringHelper.NULL_VALUE;

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

    public void ejbCreate() {
    }

    /**
     * Checks lock record
     *
     * @param eqlRes EQLERes response
     * @param ls     logon session
     * @throws EQLException
     */
    public void check(EQLERes eqlRes, LogonSession ls)
            throws EQLException {

        // Initialization
        SqlWrapper sqlW = getSqlWrapper();
        List<LockStructure> lockStructList = buildLockStructures(ls, eqlRes);
        if(lockStructList.isEmpty()) {
            return;
        }

        // Build SQL to check lock.
        StringBuffer sql = new StringBuffer("SELECT * FROM QX_LOCK WHERE ");
        sql.append(getSelectCondition(lockStructList));

        if(getLogger().isDebugEnabled()) {
            DEBUG("Try to check records:");
            DEBUG("	list: " + lockStructList);
            DEBUG("	sql: " + sql);
        }

        // Execute SQL.
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = sqlW.doConnection();
            ps = sqlW.doPreparedStatement(con, sql.toString());
            ResultSet rs = sqlW.executeQuery(ps);

            boolean found = false;
            while(rs.next()) {

                found = true;

                // retrive data
                LockStructure lockStruct = LockStructure.build(rs);

                // check
                boolean isOwnLock = lockStruct.isOwn(ls, null, null);
                if(!isOwnLock) {
                    // not own lock - throw exception
                    throw new EQLLockExpiredException();
                }
            }

            if(!found) {
                // no lock(s) found - throw exception
                throw new EQLLockExpiredException();
            }

        } catch (SQLException ex) {
            ERROR(ex);
            throw new GenericSystemException(ex);

        } finally {
            sqlW.closeConnection(con, ps);
        }
    }

    private void removeExpiredLocks() throws EQLException {
        SqlWrapper sqlW = getSqlWrapper();
        long lockTimeout = Long.parseLong(SysPropertyManager.getProperty(
                "LockTimeout"));

        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = sqlW.doConnection();
            ps = sqlW.doPreparedStatement(con,
                    "delete from qx_lock where created < ?");

            Date delta = new Date();
            delta.setTime(DateHelper.currentTimeMillis() - lockTimeout * 1000);

            sqlW.getTimestampParser().setValue(ps, 1, delta);

            sqlW.executeUpdate(ps);

        } catch (SQLException e) {
            ERROR(e);
            setRollbackOnly();
            throw new EQLException(
                    "Cannot unlock expired record(s). Status code: " + e
                            .getErrorCode());
        } finally {
            sqlW.closeConnection(con, ps);
        }
    }

    /**
     * Try to lock records with dataset records
     *
     * @param eqlRes        EQLERes response
     * @param focus         web focus id
     * @param focusInstance web focus instance number
     * @param ls            logon session
     * @throws EQLException
     */
    public void lock(EQLERes eqlRes, String focus, Long focusInstance,
                     LogonSession ls)
            throws EQLException {

        removeExpiredLocks();

        // Initialization
        SqlWrapper sqlW = getSqlWrapper();
        List<LockStructure> lockStructList = buildLockStructures(ls, eqlRes,
                null, focus, focusInstance);
        if(lockStructList.isEmpty()) {
            return;
        }
        List<LockStructure> recordsForUpdate = new ArrayList<LockStructure>();

        // Build SQL to check lock.
        StringBuffer sql = new StringBuffer("SELECT * FROM QX_LOCK WHERE ");
        sql.append(getSelectCondition(lockStructList));

        if(getLogger().isDebugEnabled()) {
            DEBUG("Try to update records:");
            DEBUG("	list: " + lockStructList);
            DEBUG("	sql: " + sql);
        }

        // Execute SQL.
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = sqlW.doConnection();
            ps = sqlW.doPreparedStatement(con, sql.toString());
            ResultSet rs = sqlW.executeQuery(ps);

            while(rs.next()) {

                // retrive data
                LockStructure lockStruct = LockStructure.build(rs);

                // check
                boolean isOwnLock = lockStruct.isOwn(ls, focus, focusInstance);
                if(getLogger().isDebugEnabled()) {
                    DEBUG("Check lock:");
                    DEBUG("	struct: " + lockStruct);
                    DEBUG("	own lock?: " + isOwnLock);
                }

                if(isOwnLock) {
                    // remember LockStructure for future update
                    recordsForUpdate.add(lockStruct);

                } else {
                    // throws exception - record locked
                    throw buildLockException(ls, lockStruct);
                }
            }

            rs.close();
            ps.close();

            // Insert new records.
            if(!lockStructList.isEmpty()) {
                sql = new StringBuffer("INSERT INTO QX_LOCK (");
                sql.append("PKEY,");
                sql.append("TABLE_NAME,");
                sql.append("RECORD_ID,");
                sql.append("RECORD_ID2,");
                sql.append("RECORD_ID3,");
                sql.append("RECORD_ID4,");
                sql.append("SESSION_ID,");
                sql.append("FOCUS_ID,");
                sql.append("FOCUS_INSTANCE,");
                sql.append("USER_ID,");
                sql.append("USER_TYPE_ID,");
                sql.append("CREATED");
                sql.append(") VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )");

                ps = sqlW.doPreparedStatement(con, sql.toString());

                for(int i = 0; i < lockStructList.size(); i++) {
                    LockStructure lockStruct = lockStructList.get(i);
                    if(recordsForUpdate.contains(lockStruct)) {
                        // will be updated later
                        continue;
                    }

                    if(getLogger().isDebugEnabled()) {
                        DEBUG("Insert record:");
                        DEBUG("	struct: " + lockStruct);
                    }

                    ps.setLong(1, getNextKeyLocal().next("QX_LOCK"));
                    sqlW.getStringParser().setValue(ps, 2,
                            lockStruct.tableName);
                    sqlW.getStringParser().setValue(ps, 3,
                            lockStruct.recordIds.get(0));
                    sqlW.getStringParser().setValue(ps, 4,
                            lockStruct.recordIds.get(1));
                    sqlW.getStringParser().setValue(ps, 5,
                            lockStruct.recordIds.get(2));
                    sqlW.getStringParser().setValue(ps, 6,
                            lockStruct.recordIds.get(3));
                    sqlW.getStringParser().setValue(ps, 7, lockStruct.session);
                    sqlW.getStringParser().setValue(ps, 8, lockStruct.focus);
                    sqlW.getLongParser().setValue(ps, 9,
                            lockStruct.focusInstance);
                    sqlW.getLongParser().setValue(ps, 10, lockStruct.user);
                    sqlW.getIntParser().setValue(ps, 11, lockStruct.userType);
                    sqlW.getTimestampParser().setValue(ps, 12,
                            DateHelper.getNowDate());

                    try {
                        sqlW.executeUpdate(ps);
                    } catch (SQLDuplicateKeyException ex) {
                        // Ops. somebody already locked this record.
                        throw buildLockException(ls, lockStruct);
                    }

                }

                ps.close();
            }

            // Update old records.
            if(!recordsForUpdate.isEmpty()) {

                sql = new StringBuffer("UPDATE QX_LOCK SET ");
                sql.append("SESSION_ID = ?,");
                sql.append("FOCUS_ID = ?,");
                sql.append("FOCUS_INSTANCE = ?, ");
                sql.append("CREATED = ? ");
                sql.append("WHERE PKEY IN (");

                for(int i = 0; i < recordsForUpdate.size(); i++) {
                    if(i > 0) {
                        sql.append(",");
                    }
                    LockStructure lockStruct = recordsForUpdate.get(i);
                    sql.append(lockStruct.pkey);
                }

                sql.append(")");

                if(getLogger().isDebugEnabled()) {
                    DEBUG("Update records:");
                    DEBUG("	list: " + recordsForUpdate);
                    DEBUG("	sql: " + sql);
                }

                ps = sqlW.doPreparedStatement(con, sql.toString());
                sqlW.getStringParser().setValue(ps, 1, ls.getSessionID());
                sqlW.getStringParser().setValue(ps, 2, focus);
                sqlW.getLongParser().setValue(ps, 3, focusInstance);
                sqlW.getTimestampParser().setValue(ps, 4,
                        DateHelper.getNowDate());
                sqlW.executeUpdate(ps);

                ps.close();
            }

        } catch (EQLLockException ex) {
            // rollback entire transaction
            setRollbackOnly();
            throw ex;

        } catch (SQLException ex) {
            // rollback entire transaction
            /** @todo fix it in future release */
            ERROR(ex);
            setRollbackOnly();
            throw new EQLException("Cannot lock record(s). Status code: " +
                    ex.getErrorCode() + ". Please, try again.");

        } finally {
            sqlW.closeConnection(con, ps);
        }
    }

    /**
     * Unlock record using session id from logon session
     *
     * @param eqlRes EQLERes response
     * @param ls     logon session
     * @throws EQLException
     */
    public void unlock(EQLERes eqlRes, LogonSession ls)
            throws EQLException {

        unlock(eqlRes, ls, null);
    }

    /**
     * Unlocks record using session id from logon session
     *
     * @param eqlRes EQLERes response
     * @param ls     logon session
     * @param record EQLResRecord record
     * @throws EQLException
     */
    public void unlock(EQLERes eqlRes, LogonSession ls, EQLResRecord record)
            throws EQLException {

        // Initialization
        SqlWrapper sqlW = getSqlWrapper();
        List<LockStructure> lockStructList = buildLockStructures(ls, eqlRes,
                record);
        if(lockStructList.isEmpty()) {
            return;
        }

        // Build SQL to delete lock.
        StringBuffer sql = new StringBuffer(
                "DELETE FROM QX_LOCK WHERE SESSION_ID = ?");
        String cond = getSelectCondition(lockStructList);
        if(cond != null) {
            sql.append(" AND ").append(cond);
        }

        // Execute SQL.
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = sqlW.doConnection();
            ps = sqlW.doPreparedStatement(con, sql.toString());
            ps.setString(1, ls.getSessionID());
            int records = sqlW.executeUpdate(ps);

            if(getLogger().isDebugEnabled()) {
                DEBUG("Remove " + records + " records.");
            }

        } catch (SQLException ex) {
            throwException("Cannot unlock record(s). Status code: " +
                    ex.getErrorCode() + ".", ex);

        } finally {
            sqlW.closeConnection(con, ps);
        }
    }

    /**
     * Unlock all records using sessoin id from logon session
     *
     * @param ls logon session
     */
    public void unlock(LogonSession ls) {

        // Initialization
        SqlWrapper sqlW = getSqlWrapper();

        // Build SQL to delete lock.
        StringBuffer sql = new StringBuffer(
                "DELETE FROM QX_LOCK WHERE SESSION_ID = ?");

        // Execute SQL.
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = sqlW.doConnection();
            ps = sqlW.doPreparedStatement(con, sql.toString());
            ps.setString(1, ls.getSessionID());
            sqlW.executeUpdate(ps);

        } catch (SQLException ex) {
            throwException("Cannot unlock record(s). Status code: " +
                    ex.getErrorCode() + ".", ex);

        } finally {
            sqlW.closeConnection(con, ps);
        }
    }

    /**
     * Update locks time
     *
     * @param eqlRes EQLERes response
     * @param ls     logon session
     */
    public void ping(EQLERes eqlRes, LogonSession ls) {
        /** @todo implement it */
    }

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

    //
    // Build select conditions.
    //

    private String getSelectCondition(List<LockStructure> lockStructList) {

        StringBuffer sql = new StringBuffer();
        for(int i = 0; i < lockStructList.size(); i++) {
            LockStructure lockStruct = lockStructList.get(i);
            String recordId1 = lockStruct.recordIds.get(0);
            String recordId2 = lockStruct.recordIds.get(1);
            String recordId3 = lockStruct.recordIds.get(2);
            String recordId4 = lockStruct.recordIds.get(3);

            if(i > 0) {
                sql.append(" OR ");
            }

            sql.append("(");
            sql.append("TABLE_NAME = '").append(lockStruct.tableName).append(
                    "'");
            sql.append(" AND ");
            sql.append("RECORD_ID = '").append(recordId1).append("'");
            if(!recordId2.equals(NULL_VALUE)) {
                sql.append(" AND ");
                sql.append("RECORD_ID2 = '").append(recordId2).append("'");
            }
            if(!recordId3.equals(NULL_VALUE)) {
                sql.append(" AND ");
                sql.append("RECORD_ID3 = '").append(recordId3).append("'");
            }
            if(!recordId4.equals(NULL_VALUE)) {
                sql.append(" AND ");
                sql.append("RECORD_ID4 = '").append(recordId4).append("'");
            }
            sql.append(")");
        }

        return sql.toString();
    }

    //
    // Build EQLLockException object by LockLocal
    //
    private EQLLockException buildLockException(LogonSession ls,
                                                LockStructure lockStruct) {

        // get user parameters
        String loginName = null;
        String userType = null;

        User user = null;
        if(lockStruct.user != null) {
            try {
                user = AccessRightsManager.getUser(lockStruct.user);
            } catch (NoSuchUserException ex) {
                WARN("User for lock not found: " + ex.getMessage());
            }
        }
        if(user != null) {
            loginName = user.getLoginName();
            userType = String.valueOf(user.getAuthenticationType());
        }

        // get focus parameters
        String focusName = null;
        if(lockStruct.focus != null) {
            CaptionManagerLocal local = getCaptionManagerLocal();
            focusName = local.getFocusCaption(ls.getUser().getLangID(),
                    lockStruct.focus);
        }

        // create exception
        EQLLockException ex =
                new EQLLockException(loginName,
                        userType,
                        focusName,
                        lockStruct.focusInstance);

        return ex;
    }

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

    //
    // Get list of LockStructure objects for entire EQLRes
    //

    private static List<LockStructure> buildLockStructures(LogonSession ls,
                                                           EQLERes eqlRes,
                                                           EQLResRecord record) {
        return buildLockStructures(ls, eqlRes, record, null, null);
    }


    private static List<LockStructure> buildLockStructures(LogonSession ls,
                                                           EQLERes eqlRes) {
        return buildLockStructures(ls, eqlRes, null);
    }

    //
    // Get list of LockStructure objects for entire EQLRes
    //
    private static List<LockStructure> buildLockStructures(LogonSession ls,
                                                           EQLERes eqlRes,
                                                           EQLResRecord record,
                                                           String focusId,
                                                           Long focusInstance) {

        List<LockStructure> ret = new ArrayList<LockStructure>();
        buildLockStructures(ls, eqlRes, record, focusId, focusInstance, ret);
        return ret;
    }

    //
    // Get list of LockStructure objects for entire EQLRes (special method)
    //
    private static void buildLockStructures(LogonSession ls,
                                            EQLERes eqlRes,
                                            EQLResRecord record,
                                            String focusId,
                                            Long focusInstance,
                                            List<LockStructure> ret) {

        // Build LockStructure object in cycle
        int records = 0;
        EQLResRecord[] recordsArray;
        if(record == null) {
            records = eqlRes.size();
            recordsArray = (EQLResRecord[]) eqlRes.getRecords().toArray(
                    new EQLResRecord[0]);
        } else {
            records = 1;
            recordsArray = new EQLResRecord[]{record};
        }

        for(int i = 0; i < records; i++) {
            EQLResRecord eqlResRecord = recordsArray[i];
            if(eqlResRecord.isNew()) {
                // Don't lock new record.
                continue;
            }
            if(eqlResRecord.doDelete()) {
                // Don't lock record for deletion.
                continue;
            }

            LockStructure lockStruct =
                    LockStructure.build(eqlRes,
                            eqlResRecord,
                            ls,
                            focusId,
                            focusInstance);

            ret.add(lockStruct);

            // Build LockStructure for datasets in cycle
            int datasets = eqlResRecord.getDResSize();
            for(int j = 0; j < datasets; j++) {
                EQLDRes dRes = eqlResRecord.getDRes(j);
                buildLockStructures(ls, dRes, null, focusId, focusInstance,
                        ret);
            }
        }
    }

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

    private static class LockStructure {
        private Long pkey;
        private String tableName;
        private List<String> recordIds = new ArrayList<String>(4);
        private String session;
        private String focus;
        private Long focusInstance;
        private Long user;
        private Integer userType;
        private Date created;

        // Constructor.
        public LockStructure(String tableName,
                             List<String> recordIds,
                             String session,
                             String focus,
                             Long focusInstance,
                             Long user,
                             Integer userType) {

            this.tableName = tableName;
            this.recordIds = recordIds;
            this.session = session;
            this.focus = focus;
            this.focusInstance = focusInstance;
            this.user = user;
            this.userType = userType;
        }

        //
        // LockStructure builder
        //
        static LockStructure build(ResultSet rs)
                throws SQLException {

            String tableName = rs.getString("TABLE_NAME");

            List<String> recordIds = new ArrayList<String>();
            recordIds.add(rs.getString("RECORD_ID"));
            recordIds.add(rs.getString("RECORD_ID2"));
            recordIds.add(rs.getString("RECORD_ID3"));
            recordIds.add(rs.getString("RECORD_ID4"));

            String session = rs.getString("SESSION_ID");
            String focus = rs.getString("FOCUS_ID");
            Long focusInstance = null;
            long l = rs.getLong("FOCUS_INSTANCE");
            if(!rs.wasNull()) {
                focusInstance = l;
            }
            Long user = null;
            l = rs.getLong("USER_ID");
            if(!rs.wasNull()) {
                user = l;
            }
            Integer userType = null;
            int i = rs.getInt("USER_TYPE_ID");
            if(!rs.wasNull()) {
                userType = i;
            }

            LockStructure lockStruct =
                    new LockStructure(tableName,
                            recordIds,
                            session,
                            focus,
                            focusInstance,
                            user,
                            userType);

            lockStruct.pkey = rs.getLong("PKEY");
            lockStruct.created = rs.getTimestamp("CREATED");

            return lockStruct;
        }

        //
        // LockStructure builder
        //
        static LockStructure build(EQLERes eqlRes,
                                   EQLResRecord eqlResRecord,
                                   LogonSession ls,
                                   String focus,
                                   Long focusInstance) {

            LockStructure lockStruct =
                    new LockStructure(getBaseEntity(eqlRes).getDbobject(),
                            getRecordIds(eqlResRecord),
                            ls.getSessionID(),
                            focus,
                            focusInstance,
                            ls.getUser().getUserID(),
                            ls.getUser().getAuthenticationType());

            return lockStruct;
        }

        //
        // Checks if lock own
        //
        public boolean isOwn(LogonSession ls, String __focus,
                             Long __focusInstance) {
            boolean isOwnLock =
                    ((this.session != null && this.session.equals(
                            ls.getSessionID())) ||
                            (this.session == null && ls.getSessionID() == null))
                            &&
                            ((__focus == null) || (this.focus != null && this
                                    .focus.equals(__focus))) &&
                            ((__focusInstance == null) || (
                                    this.focusInstance != null && this
                                            .focusInstance.equals(
                                            __focusInstance)));

            return isOwnLock;
        }

        //
        // Equals method.
        //
        public boolean equals(Object o) {
            if(o == null || !(o instanceof LockStructure)) {
                return false;
            }
            LockStructure lockStruct = (LockStructure) o;

            return
                    (lockStruct.tableName.equals(tableName)) &&
                            (lockStruct.recordIds.equals(recordIds));
        }

        //
        // Hash code method.
        //
        public int hashCode() {
            return tableName.hashCode() | recordIds.hashCode();
        }

        //
        // To string method.
        //
        public String toString() {
            return
                    " pkey = " + pkey +
                            ", tableName = " + tableName +
                            ", recordIds = " + recordIds +
                            ", session = " + session +
                            ", focus = " + focus +
                            ", focusInstance = " + focusInstance +
                            ", user = " + user +
                            ", userType = " + userType +
                            ", created = " + created;
        }

        //
        // Get base entity from EQL response
        //
        private static Entity getBaseEntity(EQLERes eqlRes) {
            Entity baseEntity = eqlRes.getEntity();
            if(baseEntity == null) {
                throw new IllegalStateException("Base entity not found");
            }

            return baseEntity;
        }

        //
        // Build array of 4 elements of record ids
        //
        private static List<String> getRecordIds(EQLResRecord eqlResRecord) {
            List<String> pkeyList = new ArrayList<String>();
            int pkeys = 0;
            for(int i = 0; i < eqlResRecord.size(); i++) {
                EQLResCell eqlResCell = eqlResRecord.getResCell(i);
                Efield field = eqlResCell.getReqField().getField();
                if(field.getPkey()) {
                    String value = eqlResCell.getEQLObject().toString();
                    pkeyList.add(pkeys, value == null ? NULL_VALUE:value);
                    pkeys++;
                }
            }
            for(int i = pkeys; i < 4; i++) {
                pkeyList.add(i, NULL_VALUE);
            }

            return pkeyList;
        }

    } // -- end inner class

}
