/*
 * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */
package com.arsdigita.versioning;

import com.arsdigita.db.DbHelper;
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.SessionManager;
import com.redhat.persistence.AddEvent;
import com.redhat.persistence.CreateEvent;
import com.redhat.persistence.DeleteEvent;
import com.redhat.persistence.pdl.PDL;
import com.redhat.persistence.PropertyEvent;
import com.redhat.persistence.RemoveEvent;
import com.redhat.persistence.SetEvent;
import com.arsdigita.util.Assert;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;

// new versioning

/**
 * {@link com.arsdigita.persistence.DataObject Data object} modification.
 *
 * @author Joseph A. Bank  (jbank@alum.mit.edu)
 * @author Stanislav Freidin
 * @author Vadim Nasardinov (vadimn@redhat.com)
 * @version $Revision: #12 $ $Date: 2004/08/16 $
 */
final class DataObjectChange implements Constants {
    private final static Logger s_log = Logger.getLogger(DataObjectChange.class);

    private final static int MAX_GENERIC = 4000;

    private DataObject m_dobj;
    private boolean m_isRecoverable;
    private List m_pending;

    /**
     * oid. This constructor creates a new instance of a
     * <code>DataObjectChange</code> whose job it is to track changes to a data
     * object identified by <code>trackedObjectOID</code>.
     **/
    public DataObjectChange(OID trackedObjectOID) {
        Assert.exists(trackedObjectOID, OID.class);
        m_dobj = SessionManager.getSession().create(CHANGE_DATA_TYPE);
        set(ID, functions.nextSequenceValue());
        set(OBJ_ID, Adapter.serialize(trackedObjectOID));
        m_isRecoverable = ObjectTypeMetadata.getInstance().isRecoverable
            (trackedObjectOID.getObjectType().getQualifiedName());

        if ( m_isRecoverable ) {
            // m_pending is only needed for recoverable objects.
            m_pending = new LinkedList();
        }
    }

    public void setTxn(DataObject txn) {
        set(TXN, txn);
    }

    private void set(String property, Object value) {
        m_dobj.set(property, value);
    }

    private Object get(String property) {
        return m_dobj.get(property);
    }

    DataObject getDataObject() {
        return m_dobj;
    }

    void recordEvent(AddEvent ev) {
        recordAssocEvent(ev, EventType.ADD);
    }

    void recordEvent(RemoveEvent ev) {
        recordAssocEvent(ev, EventType.REMOVE);
    }

    private static boolean shouldSkip(String attrName) {
        // XXX: these hacks can go away after the QGen related metadata
        // upgrades

        // This is a workaround for synthetic attributes generated by
        // persistence to convert one-way composite associations into two-way
        // associations.  Such synthetic attribute names currently start with
        // the tilde character and look like "~vt4versioning$events$C2".
        if ( attrName.startsWith("~")) return true;

        // This is a work around for events for the autogenerated intermediate
        // object types
        if (attrName.indexOf(PDL.LINK) != -1) return true;

        return false;
    }

    private void recordAssocEvent(PropertyEvent ev, EventType eventType) {
        String attrName = ev.getProperty().getName();
        if (shouldSkip(attrName)) { return; }

        OID value = ((DataObject) ev.getArgument()).getOID();
        String strValue = Adapter.serialize(value);
        DataObject operation = newOperation(strValue);
        operation.set(EVENT_TYPE, eventType.getDataObject());
        operation.set(ATTRIBUTE, attrName);
        operation.set(VALUE, strValue);
        operation.set(JAVACLASS, Types.OID.getDataObject());
        add(OPERATIONS, operation);
    }

    private void add(String property, DataObject value) {
        ((DataAssociation) m_dobj.get(property)).add(value);
    }

    void recordEvent(SetEvent ev) {
        String attrName = ev.getProperty().getName();
        if (shouldSkip(attrName)) { return; }

        if ( ev.getProperty().getContainer().getKeyProperties().contains
             (ev.getProperty()) ) {

            // We will get this property with the Create or Delete event.
            return;
        }

        if ( m_isRecoverable ) {
            if ( ev.getArgument() != null ) {
                // This set event was definitely not generated as part of a
                // delete event.
                m_pending.clear();
                return;
            }
            // This set event *may* have been generated as part of a delete
            // event.
            m_pending.add(ev);
        } else {
            record(ev);
        }
    }

    void record(SetEvent ev) {
        Object prevValue = ev.getPreviousValue();
        if ( ev.getProperty().getType().isCompound() && prevValue != null ) {
            prevValue = ((DataObject) prevValue).getOID();
        }

        Types type = Types.getObjectType(prevValue);
        Object value = null;
        DataObject operation = null;
        if ( type == Types.BLOB ) {
            value = prevValue;
            operation = newBlobOperation();
        } else {
            value = Adapter.serialize(prevValue);
            operation = newOperation((String) value);
        }

        operation.set(EVENT_TYPE, EventType.SET.getDataObject());
        operation.set(ATTRIBUTE, ev.getProperty().getName());
        operation.set(VALUE, value);
        operation.set(JAVACLASS, type.getDataObject());
        add(OPERATIONS, operation);
    }

    void recordEvent(CreateEvent ev) {
        OID value = ((DataObject) ev.getObject()).getOID();
        String strValue = Adapter.serialize(value);

        DataObject operation = newOperation(strValue);
        operation.set(EVENT_TYPE, EventType.CREATE.getDataObject());
        operation.set(ATTRIBUTE, value.getObjectType().getQualifiedName());
        operation.set(VALUE, strValue);
        operation.set(JAVACLASS, Types.OID.getDataObject());
        add(OPERATIONS, operation);
    }

    void recordEvent(DeleteEvent ev) {
        if ( m_isRecoverable ) {
            for (Iterator ii=m_pending.iterator(); ii.hasNext(); ) {
                SetEvent sev = (SetEvent) ii.next();
                record(sev);
            }
            // There can't be any more events coming after the delete. We won't
            // be needing the m_pending list.
            m_pending = null;
        }
        DataObject deletedObj = (DataObject) ev.getObject();
        String strValue = Adapter.serialize(deletedObj.getOID());
        DataObject operation = newOperation(strValue);
        operation.set(EVENT_TYPE, EventType.DELETE.getDataObject());
        operation.set(ATTRIBUTE,
                      deletedObj.getOID().getObjectType().getQualifiedName());


        operation.set(VALUE, strValue);
        operation.set(JAVACLASS, Types.OID.getDataObject());
        add(OPERATIONS, operation);
    }

    private DataObject newOperation(String str) {
        if ( DbHelper.varcharLength(str) <= MAX_GENERIC) {
            return operation(GENERIC_OPERATION);
        } else {
            return operation(CLOB_OPERATION);
        }
    }

    private DataObject newBlobOperation() {
        return operation(BLOB_OPERATION);
    }

    private DataObject operation(OpType opType) {
        DataObject dobj = SessionManager.getSession().create(opType.datatype());
        dobj.set(ID, functions.nextSequenceValue());
        dobj.set(CHANGESET, m_dobj);
        dobj.set(SUBTYPE, opType.integerValue());
        return dobj;
    }
}
