/*
 * RHQ Management Platform
 * Copyright (C) 2005-2011 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program 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 General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.rhq.core.domain.configuration;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.jetbrains.annotations.NotNull;

/**
 * Holds an indexed list of child {@link Property properties}. This can hold any number of properties, including
 * additional lists and maps of properties (which means you can have N-levels of hierarchical data).
 *
 * <p>This list will store the properties in the order they are {@link #add(Property) added}.</p>
 *
 * <p>Caution must be used when accessing this object. This class is not thread safe and, for entity persistence, the
 * child properties must have their {@link Property#getParentList()} field set. This is done for you when using the
 * {@link #add(Property)} method.</p>
 *
 * @author Jason Dobies
 * @author Greg Hinkle
 */
@DiscriminatorValue("list")
@Entity
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement
public class PropertyList extends Property {
    private static final long serialVersionUID = 1L;

    // CascadeType.REMOVE has been omitted, the cascade delete has been moved to the data model for performance 
    @Cascade( { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DELETE_ORPHAN })
    @OneToMany(mappedBy = "parentList", targetEntity = Property.class, fetch = FetchType.EAGER)
    // Order by primary key which will also put the list elements in chronological order.
    // Note, if we decide at some point to add support in the GUI for reordering list elements, we'll
    // need to add a new ORDER column and order by that.
    @OrderBy
    private List<Property> list = new ArrayList<Property>();

    @Transient
    String memberPropertyName;

    /* no-arg constructor required by EJB spec and Externalizable (Externalizable also requires it to be public) */
    public PropertyList() {
    }

    /**
     * Creates a new, empty {@link PropertyList} object that is associated with the given name.
     *
     * @param name the name of the list itself
     */
    public PropertyList(@NotNull String name) {
        setName(name);
    }

    protected PropertyList(PropertyList original, boolean keepId) {
        super(original, keepId);
    }

    /**
     * Creates a new {@link PropertyList} object that is associated with the given name and has the given properties as
     * its initial list of child properties. All properties found in <code>startingList</code> will have their
     * {@link Property#setParentList(PropertyList) parent list} set to this newly constructed list.
     *
     * @param name         the name of the list itself
     * @param startingList a list of properties to be immediately added to this list
     */
    public PropertyList(@NotNull String name, @NotNull Property... startingList) {
        this(name);
        for (Property property : startingList) {
            add(property);
        }
    }

    /**
     * Returns the children of this list.
     *
     * <p><b>Warning:</b> Caution should be used when accessing the returned list. Please see
     * {@link PropertyList the javadoc for this class} for more information.</p>
     *
     * @return the list of child properties
     */
    @NotNull
    public List<Property> getList() {
        if (this.list == null) {
            this.list = new ArrayList<Property>();
        }

        return this.list;
    }

    /**
     * Sets the list of child properties directly to the given <code>list</code> reference. This means the actual <code>
     * list</code> object is stored internally in this object. Changes made to <code>list</code> will be reflected back
     * into this object.
     *
     * <p><b>Warning:</b> Caution should be used when setting this object's internal list. Please see
     * {@link PropertyList the javadoc for this class} for more information.</p>
     *
     * @param list the new list used internally by this object
     */
    public void setList(List<Property> list) {
        if (list != null) {
            for (Property property : list) {
                add(property);
            }
        }
    }

    /**
     * Adds a child property to the end of this list. This method also sets the
     * {@link Property#setParentList(PropertyList) parent list} for the child property to make persistence work.
     *
     * @param property the property to add to this list
     */
    public void add(@NotNull Property property) {
        if (this.memberPropertyName == null) {
            this.memberPropertyName = property.getName();
        }

        if (!property.getName().equals(this.memberPropertyName)) {
            throw new IllegalStateException("All properties in a PropertyList (id=[" + getId() + "], name=["
                + getName() + "]) must have the same name: [" + property.getName() + "] != [" + this.memberPropertyName
                + "]");
        }

        getList().add(property);
        property.setParentList(this);
    }

    /**
     * NOTE: An PropertyList containing a null list is considered equal to a PropertyList containing an empty list.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if ((obj == null) || !(obj instanceof PropertyList)) {
            return false;
        }

        if (!super.equals(obj)) {
            return false; // superclass checks equality of the name fields
        }

        PropertyList that = (PropertyList) obj;
        if ((this.list == null) || this.list.isEmpty()) {
            // NOTE: Use that.getList(), rather than that.list, in case 'that' is a JPA/Hibernate proxy, to
            //       force loading of the List.
            return (that.getList() == null) || that.getList().isEmpty();
        }

        return this.list.containsAll(that.getList()) && that.getList().containsAll(this.list);
    }

    @Override
    public int hashCode() {
        int result = super.hashCode(); // superclass hashCode is derived from the name field
        result = (31 * result) + (((this.list != null) && !this.list.isEmpty()) ? this.list.hashCode() : 0);
        return result;
    }

    public PropertyList deepCopy(boolean keepId) {
        PropertyList copy = new PropertyList(this, keepId);

        for (Property property : list) {
            copy.add(property.deepCopy(false));
        }

        return copy;
    }

    @Override
    protected void appendToStringInternals(StringBuilder str) {
        super.appendToStringInternals(str);
        str.append(", list=").append(getList());
    }

    /**
     * This listener runs after jaxb unmarshalling and reconnects children properties to their parent list (as we don't
     * send them avoiding cyclic references).
     */
    public void afterUnmarshal(Object u, Object parent) {
        for (Property p : this.list) {
            p.setParentList(this);
        }
    }
}