/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.descriptors.changetracking;

import java.beans.*;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.changetracking.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.mappings.foundation.*;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.exceptions.ValidationException;

/**
 * <p>
 * <b>Purpose</b>: Define a listener for attribute change tracking.
 * <p>
 * <b>Description</b>: Listener is notified on a PropertyChangeEvent from the object it belongs to.
 * <p>
 * <b>Responsibilities</b>: Set the flag to true and build ObjectChangeSet that includes the
 * ChangeRecords for the changed attributes.
 */
public class AttributeChangeListener extends ObjectChangeListener {
    protected transient ClassDescriptor descriptor;
    protected transient UnitOfWorkImpl uow;
    protected org.eclipse.persistence.internal.sessions.ObjectChangeSet objectChangeSet;
    protected Object owner;

    /**
     * INTERNAL:
     * Create a AttributeChangeListener with a descriptor and unit of work
     */
    public AttributeChangeListener(ClassDescriptor descriptor, UnitOfWorkImpl uow, Object owner) {
        super();
        this.descriptor = descriptor;
        this.uow = uow;
        this.owner = owner;
    }

    /**
     * INTERNAL:
     * Return the object change set associated with this listener
     */
    public org.eclipse.persistence.internal.sessions.ObjectChangeSet getObjectChangeSet() {
        return objectChangeSet;
    }

    /**
     * INTERNAL:
     * Return the object change set associated with this listener
     */
    public void setObjectChangeSet(org.eclipse.persistence.internal.sessions.ObjectChangeSet changeSet) {
        this.objectChangeSet = changeSet;
    }

    /**
     * INTERNAL:
     * Return the descriptor associated with this listener
     */
    public ClassDescriptor getDescriptor() {
        return descriptor;
    }

    /**
     * INTERNAL:
     * Set the descriptor associated with this listener
     */
    public void setDescriptor(ClassDescriptor descriptor) {
        this.descriptor = descriptor;
    }

    /**
     * INTERNAL:
     * Return the unit of work associated with this listener
     */
    public UnitOfWorkImpl getUnitOfWork() {
        return uow;
    }

    /**
     * INTERNAL:
     * Set the unit of work associated with this listener
     */
    public void setUnitOfWork(UnitOfWorkImpl uow) {
        this.uow = uow;
    }

    /**
     * PUBLIC:
     * This method creates the object change set if necessary.  It also creates/updates
     * the change record based on the new value.  Object should check the if newValue and
     * oldValue are identical.  If they are identical, do not create PropertyChangeEvent
     * and call this method.
     */
    public void propertyChange(PropertyChangeEvent evt) {
        if (this.ignoreEvents){
            return;
        }
        internalPropertyChange(evt);
    }

    /**
     * INTERNAL:
     * This method marks the object as changed.  This method is only
     * called by EclipseLink
     */
    public void internalPropertyChange(PropertyChangeEvent evt) {
        if (evt.getNewValue() == evt.getOldValue()) {
            return;
        }

        DatabaseMapping mapping = descriptor.getObjectBuilder().getMappingForAttributeName(evt.getPropertyName());
        //Bug#4127952 Throw an exception indicating there is no mapping for the property name.
        if (mapping == null) {
            throw ValidationException.wrongPropertyNameInChangeEvent(owner.getClass(), evt.getPropertyName());
        }
        if (mapping instanceof AbstractDirectMapping || mapping instanceof AbstractTransformationMapping) {
            //If both newValue and oldValue are null, or newValue is not null and newValue equals oldValue, don't build ChangeRecord
            if (((evt.getNewValue() == null) && (evt.getOldValue() == null)) || ((evt.getNewValue() != null) && (evt.getNewValue()).equals(evt.getOldValue()))) {
                return;
            }
        }

        super.internalPropertyChange(evt);

        if (uow.getUnitOfWorkChangeSet() == null) {
            uow.setUnitOfWorkChangeSet(new UnitOfWorkChangeSet(uow));
        }
        if (objectChangeSet == null) {//only null if new or if in a new UOW
            //add to tracker list to prevent GC of clone if using weak references
            //put it in here so that it only occurs on the 1st change for a particular UOW
            uow.addToChangeTrackedHardList(owner);
            objectChangeSet = getDescriptor().getObjectBuilder().createObjectChangeSet(owner, (UnitOfWorkChangeSet) uow.getUnitOfWorkChangeSet(), false, uow);
        }

        if (evt.getClass().equals(ClassConstants.PropertyChangeEvent_Class)) {
            mapping.updateChangeRecord(evt.getSource(), evt.getNewValue(), evt.getOldValue(), objectChangeSet, getUnitOfWork());
        } else if (evt.getClass().equals(ClassConstants.CollectionChangeEvent_Class) || (evt.getClass().equals(ClassConstants.MapChangeEvent_Class))) {
            mapping.updateCollectionChangeRecord((CollectionChangeEvent)evt, objectChangeSet, getUnitOfWork());
        } else {
            throw ValidationException.wrongChangeEvent(evt.getClass());
        }
    }

    /**
     * INTERNAL:
     * Clear the changes in this listener
     */
    public void clearChanges(boolean forRefresh) {
        super.clearChanges(forRefresh);
        if (forRefresh && this.objectChangeSet != null){
            this.objectChangeSet.clear(true);
        }
        this.objectChangeSet = null;
    }

    public String toString() {
        return getClass().getSimpleName() + "(" + getObjectChangeSet() + ")";
    }
}
