/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.apache.empire.struts2.actionsupport;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.Record;
import org.apache.empire.db.DBColumn;
import org.apache.empire.db.DBCommand;
import org.apache.empire.db.DBIndex;
import org.apache.empire.db.DBReader;
import org.apache.empire.db.DBRecord;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.db.DBTable;
import org.apache.empire.db.exceptions.InvalidKeyException;
import org.apache.empire.db.exceptions.NoPrimaryKeyException;
import org.apache.empire.db.exceptions.QueryNoResultException;
import org.apache.empire.db.expr.compare.DBCompareExpr;
import org.apache.empire.exceptions.EmpireException;
import org.apache.empire.exceptions.ItemNotFoundException;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.ObjectNotValidException;
import org.apache.empire.struts2.exceptions.InvalidFormDataException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * RecordActionSupport
 * <p>
 * This class provides functions for form data processing for a given Table or View (DBRowSet).<br>   
 * The record object provided with the constructor will be used to obtain further context specific metadata
 * such as field options (getFieldOptions) and field accessibility (isFieldReadOnly).<br>
 * The record object should initially be invalid and not attached to any Table or View (DBRowSet).
 * </p>
 * @author Rainer
 */
public class RecordActionSupport extends RecordFormActionSupport
{
    /*
     * Choices for record persistence
     * 
     * None = nothing is stored on session
     * Key = the record key is stored on the session. The record is reloaded if necessary
     * Data = the whole record is stored on the session.  
     */
    @Deprecated
    public static enum SessionPersistance
    {
        None,
        Key,
        Data;
        
        static SessionPersistence convert(SessionPersistance sp)
        {
            switch(sp)
            {
                case Key: return SessionPersistence.Key;
                case Data: return SessionPersistence.Data;
                default: return SessionPersistence.None; 
            }
        }
    }
    
    
    @SuppressWarnings("hiding")
    protected static Logger log = LoggerFactory.getLogger(RecordActionSupport.class);

    protected DBRowSet rowset;

    protected DBRecord record;
    
    private boolean loadBeforeDelete = false;
    
    public RecordActionSupport(ActionBase action, DBRowSet rowset, DBRecord record, SessionPersistence persistence, String propertyName)
    {
        super(action, persistence, propertyName);
        // Set Rowset and Record
        this.rowset = rowset;
        this.record = record;
    }

    public RecordActionSupport(ActionBase action, DBRowSet rowset, DBRecord record, SessionPersistence persistence)
    {
        this(action, rowset, record, persistence, action.getItemPropertyName());
    }

    @Deprecated
    public RecordActionSupport(ActionBase action, DBRowSet rowset, DBRecord record, SessionPersistance persistence, String propertyName)
    {
        this(action, rowset, record, SessionPersistance.convert(persistence), propertyName);
    }

    @Deprecated
    public RecordActionSupport(ActionBase action, DBRowSet rowset, DBRecord record, SessionPersistance persistence)
    {
        this(action, rowset, record, SessionPersistance.convert(persistence), action.getItemPropertyName());
    }
    
    @Override
    public DBRecord getRecord()
    {
        return record;
    }

    public DBRowSet getRowset()
    {
        return rowset;
    }

    public boolean isLoadBeforeDelete()
    {
        return loadBeforeDelete;
    }

    public void setLoadBeforeDelete(boolean loadBeforeDelete)
    {
        this.loadBeforeDelete = loadBeforeDelete;
    }
    
    @Deprecated
    public SessionPersistance getPersistance()
    {
        switch(getPersistence())
        {
            case Key: return SessionPersistance.Key;
            case Data: return SessionPersistance.Data;
            default: return SessionPersistance.None; 
        }
    }
    
    // ------- Methods -------
    
    /**
     * creates a new record.<BR>
     * Depending on the persistence setting the record key or the record data will be stored on the session.
     * @return true if the record was successfully created or false otherwise
     */
    public boolean createRecord()
    {
        try {
            // initNew
            record.create(rowset, action.getConnection());
            // Save Record Key Info
            persistOnSession();
            return true;
            
        } catch(Exception e) {
            // Action failed
            action.setActionError(e);
            return false;
        }
    }
    
    /**
     * sets all required foreign keys for this record.<BR>
     * The foreign key values must be supplied with the request.
     */
    public void initReferenceColumns()
    {
        // set Reference Values (if provided)
        Map<DBColumn, DBColumn> refs = rowset.getColumnReferences();
        if (refs!=null)
        {   // Get Parent Columns from Request (if provided)
            for (DBColumn column : refs.keySet())
            {   // Parent Column
                String name  = column.getName();
                String value = action.getRequestParam(name);
                if (value!=null)
                {
                    if (StringUtils.isNotEmpty(value))
                        record.setValue(column, value);
                }
                else if (column.isRequired())
                {   // Reference column not provided
                    log.warn("Value for reference column has not been provided!");
                    throw new ItemNotFoundException(name);
                }
            }
        }
    }
    
    /**
     * loads the record identified by the supplied key from the database<BR>
     */
    public void loadRecord(Object[] recKey)
    {
        // Check Key
        if (recKey==null || recKey.length==0)
        {   // Invalid Record key
            throw new InvalidKeyException(rowset, recKey);
        }
        // Record laden
        record.read(rowset, recKey, action.getConnection());
        // Save Record Key Info
        persistOnSession();
    }
    
    /**
     * loads the record either from the supplied item key on the request or from the session.<BR>
     * @return true if the record has been successfully reloaded or false otherwise
     */
    public boolean loadRecord()
    {   // Load 
        try {
            Object[] key = getActionParamKey();
            if (key==null && (persistence==SessionPersistence.Data))
            {   // reload session record
                reloadRecord();
                return true;
            }
            // Load Record
            loadRecord(key);
            return true;

        } catch(Exception e) {
            // Action failed
            action.setActionError(e);
            return false;
        }
    }
    
    /**
     * reloads the current record from the session.<BR>
     * If persistence is set to Key then the key is obtained from the session and the record 
     * is reloaded from the database.
     */
    public void reloadRecord()
    {   // Load 
        switch(persistence)
        {
            // Key persistence
            case Key:
            {   // Load from session key
                String stKey = StringUtils.toString(action.getActionObject(getRecordPropertyName()));
                Object[] key = action.getRecordKeyFromString(stKey);
                loadRecord(key);
            }
            // Data persistence
            case Data:
            {   // get record object from session
                Record rec = getRecordFromSession();
                if (rec!=null && (rec instanceof DBRecord))
                {   // Check rowset
                    if (((DBRecord)rec).getRowSet()!=rowset)
                        throw new ObjectNotValidException(rec);
                    // Record restored
                    this.record = (DBRecord)rec;
                }
                // Record not found
                throw new ItemNotFoundException(rowset.getName());
            }
            // Other
            default:
                throw new NotSupportedException(this, "reloadRecord[] " + String.valueOf(persistence));
        }
    }
    
    /**
     * deletes the record identified by the supplied key from the database.
     * @param recKey the record key
     * @param newRec flag indicating whether it is a new unsaved record.
     * @return true if the record has been successfully deleted
     */
    public boolean deleteRecord(Object[] recKey, boolean newRec)
    {
        try {
            // Check Key
            if (recKey==null || recKey.length==0)
            {   // Invalid Record Key
                throw new InvalidKeyException(rowset, recKey);
            }
            if (newRec)
            { 	// Record has not been saved yet!
                record.close();
                return true;
            }
            // Delete Record
            if (loadBeforeDelete)
            {   // load record and delete afterwards
                record.read(rowset, recKey, action.getConnection());
                record.delete(action.getConnection());
            }
            else
            {   // rowset error
                rowset.deleteRecord(recKey, action.getConnection());
            }
            // Success
            removeFromSession();
            return true;

        } catch(Exception e) {
            // Action failed
            action.setActionError(e);
            return false;
        }
    }

    /**
     * deletes the current record database.
     * @return true if the record has been successfully deleted
     */
    public final boolean deleteRecord()
    {
        // Get Record Key
        Object[] recKey = getActionParamKey();
        boolean  newRec = getActionParamNewFlag();
        return deleteRecord(recKey, newRec);
    }
    
    /**
     * This function load all form date from the request into the record
     * for each record column the following steps are taken
     * 1. Detects the control type of the column
     * 2. Let's the corresponding InputControl read, parse and validate the value from the request
     * 3. If a field error occurred the error is stored on the action using action.setFieldError(...)
     * 4. Stores either the parsed or - in case of an error - the request value in the record.  
     * 
     * This procedure does not stop if a field error occurs.
     * Use Action.hasActionError() or Action.getFieldErrors()
     * to determine whether a field error has occurred.
     * 
     * @return true if the record could be loaded and the form data has been filled in
     */
    public boolean loadFormData(Object[] recKey, boolean insert)
    {
        try {
            // Check Key
            if (recKey==null || recKey.length==0)
            {   // Invalid Record key
                throw new InvalidKeyException(rowset, recKey);
            }
            // Prepare Update
            Connection conn = action.getConnection();
            initUpdateRecord(recKey, insert, conn);
            // Set Update Fields
            setUpdateFields(record);
            // Done
            persistOnSession();
            return true;

        } catch(Exception e) {
            // Action failed
            action.setActionError(e);
            return false;
        }
    }

    @Override
    public boolean loadFormData()
    {
        // Get Record Key
        Object[] recKey = getActionParamKey();
        boolean  insert = getActionParamNewFlag();
        return loadFormData(recKey, insert);
    }
    
    /**
     * Updates the record by calling onUpdateRecord and updates the currentKey
     * The update will not be committed, hence the caller must commit or rollback
     * the operation
     * 
     * @return true if the update was successful otherwise false
     */
    public boolean saveChanges()
    {
        try {
            // Record is not valid
            if (record.isValid()==false)
            {   
                log.error("Cannot save changes: record ist not valid");
                throw new ObjectNotValidException(record);
            }
            // Update Record
            updateRecord(action.getConnection());
            // Save Record Key Info
            persistOnSession();
            return true;

        } catch(Exception e) {
            // Action failed
            action.setActionError(e);
            return false;
        }
    }
    
    /**
     * Closes the record and releases any allocated session objects
     */
    public void closeRecord()
    {
        record.close();
        removeFromSession();
    }
    
    public DBRecord detachRecord()
    {
        DBRecord rec = record;
        record = null;
        return rec;
    }

    // --------------------------- useful helpers --------------------------------
    
    public final List<DBIndex> findChangedIndexes()
    {   // Must be a table to do this
        if ((rowset instanceof DBTable)==false)
            return null;
        // Check Record
        if (record.isValid()==false || record.isModified()==false)
            return null;
        // See which Index has changed
        DBTable table = (DBTable)rowset;
        List<DBIndex> avail = table.getIndexes();
        if (avail==null)
            return null;
        List<DBIndex> changed = null;
        for (DBIndex idx : avail)
        {   // Iterate through all indexes
            DBColumn[] idxColumns = idx.getColumns();
            for (int i=0; i<idxColumns.length; i++)
            {   // Check if column has changed
                if (record.wasModified(idxColumns[i]))
                {   // Yes, column has changed
                    if (changed == null)
                        changed = new ArrayList<DBIndex>();
                    changed.add(idx);
                    break;
                }
            }
        }
        return changed;
    }
    
    public final Object[] findAnyConflictRecord()
    {
        // Get list of changed indexes
        List<DBIndex> changed = findChangedIndexes();
        if (changed==null)
            return null; // No Conflicts
        // Iterate through all changed indexes
        DBColumn[] keyColumns = rowset.getKeyColumns();
        for (DBIndex idx : changed)
        {
            // Select all key columns
            DBCommand cmd = rowset.getDatabase().createCommand();
            cmd.select(keyColumns);
            // add constraints
            boolean allNull = true;
            DBColumn[] idxColumns = idx.getColumns();
            for (int i=0; i<idxColumns.length; i++)
            {   // Check if column has changed
                Object value = record.getValue(idxColumns[i]);
                cmd.where(idxColumns[i].is(value));
                if (value!=null)
                    allNull = false;
            }
            // Check whether all constraints are null
            if (allNull)
                continue; 
            // Exclude current record
            if (record.isNew()==false)
            {   // add restriction
                if (keyColumns.length>1)
                {   // Multiple key columns
                    Object value = record.getValue(keyColumns[0]);
                    DBCompareExpr notExpr = keyColumns[0].is(value);
                    for (int i=1; i<keyColumns.length; i++)
                    {   // Check if column has changed
                        cmd.where(keyColumns[i].is(value));
                    }
                    cmd.where(notExpr.not());
                }
                else
                {   // Single key column
                    Object value = record.getValue(keyColumns[0]);
                    cmd.where(keyColumns[0].isNot(value));
                }
            }
            // Query now
            DBReader reader = new DBReader();
            try { 
                reader.getRecordData(cmd, action.getConnection());
                // We have found a record
                Object[] key = new Object[keyColumns.length];
                for (int i=0; i<keyColumns.length; i++)
                {   // Check if column has changed
                    key[i] = reader.getValue(i);
                }
                return key;
            } catch(QueryNoResultException e) {
                // ignore
            } finally {
                reader.close();
            }
        }
        // No, no conflicts
        return null;
    }
    
    // --------------------------- protected --------------------------------

    /**
     * overridable: onUpdateRecord
     */
    protected void updateRecord(Connection conn)
    {
        // Modified?
        if (!record.isModified())
            return;
        // Missing defaults?
        record.fillMissingDefaults(null);
        // Update Record
        record.update(conn);
    }

    // --------------------------- overrides --------------------------------

    @Override
    protected boolean setRecordFieldValue(int i, Object value, boolean verify)
    {
        if (verify)
        {   // Set Value with checking
            try {
                record.setValue(i, value);
                return true;
            } catch(EmpireException e) {
                log.info("setRecordFieldValue failed. Message is {}.", e.getMessage());
                return false;
            }
        }
        else
        {   // No Checking
        	boolean validate = record.isValidateFieldValues();
        	try {
	        	record.setValidateFieldValues(false);
	            record.setValue(i, value);
        	} finally {
	        	record.setValidateFieldValues(validate);
        	}
            return true;
        }
    }
    
    // --------------------------- private --------------------------------
    
    /**
     * this function prepared (initialize) a record to insert or update them to
     * the database
     * 
     * @param rowset
     *            the current DBRowSet object
     * @param request
     *            the current request object
     * @param rec
     *            the DBRecord object, consist all fields and the field
     *            properties
     * @param recKey
     *            the primary key(s)
     * @param insert
     *            true if insert sql statement
     */
    private void initUpdateRecord(Object[] keyValues, boolean insert, Connection conn)
    { // Get the record key
        DBColumn[] keyColumns = rowset.getKeyColumns();
        if (keyColumns == null || keyColumns.length < 1)
            throw new NoPrimaryKeyException(rowset);
        if (keyValues == null || keyValues.length != keyColumns.length)
            throw new InvalidKeyException(rowset, keyValues);
        // Get Persistent record
        if (persistence==SessionPersistence.Data)
        {   // Get the record from the session
            Record rec = getRecordFromSession();
            if (rec==null || (rec instanceof DBRecord)==false)
            {   // Record restored
                throw new InvalidFormDataException();
            }
            // Record not found
            record = (DBRecord)rec;
        }
        // Check Record State
        if (record.isValid())
        {   // Is this the record we require?
            Object[] currentKey = record.getKeyValues();
            if (compareKey(currentKey, keyValues)==false)
            {   // Keys don't match
                throw new InvalidFormDataException();
            }
            // We have a valid record
            return;
        }
        // Insert
        if (insert)
        { // Initialize the Record
            record.init(rowset, keyValues, insert);
            // Add the record
            // rec.state = DBRecord.REC_NEW;
            log.debug("Record '" + rowset.getName() + "' prepared for Insert!");
        } else
        { // Read the record from the db
            record.read(rowset, keyValues, conn);
            // Record has been reloaded
            log.debug("Record '" + rowset.getName() + "' prepared for Update!");
        }
    }
    
}
