/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.messaging;

// ACS Core classes
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.kernel.Party;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.Filter;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.PersistenceException;
import com.arsdigita.persistence.Session;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.util.UncheckedWrapperException;

// Java Core classes
import java.math.BigDecimal;

import org.apache.log4j.Logger;

/**
 * Extends Message in a way that allows messages to be organized into
 * discussion threads with a tree structure.  A typical discussion
 * might be organized as follows:
 *
 * <pre>
 *     msg-0
 *         msg-0.0
 *         msg-0.1
 *             msg-0.1.0
 *             msg-0.1.1
 *         msg-0.2
 *     msg-1
 *         msg-1.0
 *     msg-2
 * </pre>
 *
 * <p>where msg-0.0 and msg-0.1 are replies to msg-0, msg-0.1.0 is a
 * reply to msg-0.1, and so forth.  Messages at the first level
 * (msg-0, msg-1, and msg-2) are referred to as "root" message, and
 * higher-level messages contain a pointer to their common root.  If a
 * root message is deleted, all of its children are deleted.
 *
 * <p>A structure like the one shown above is created using the
 * reply() method, which returns a new ThreadedMessage correctly
 * initialized to represent a response to its parent.  For example,
 * you might generate a similar structure using:
 *
 * <pre>
 *     msg0 = new Message();       // root message
 *
 *     msg00  = msg0.reply();      // level 1 replies
 *     msg01  = msg0.reply();
 *
 *     msg010 = msg01.reply();     // level 2 replies (to msg01)
 *     msg011 = msg01.reply();
 * </pre>
 *
 * <p>Replying to a message always generates a new message one level
 * deeper in the tree.  Successive replies to the same message
 * generate the appropriate "next child" for that message.
 *
 * @author Ron Henderson
 * @version $Id:
 * //core-platform/dev/services/messaging/src/ThreadedMessage.java#3 $
 */

public class ThreadedMessage extends Message {

    /**
     * Base data object type.
     */

    public static final String BASE_DATA_OBJECT_TYPE =
        ThreadedMessage.class.getName();

    /**
     * Keys for persistent data.
     */

    private static final String ROOT_ID   = "root";
    private static final String ROOT      = "root";
    private static final String SORT_KEY  = "sortKey";

    private boolean m_wasNew = false;

    private MessageThread m_thread = null;
    private ThreadedMessage m_root = null;

    private static final Logger s_log =
        Logger.getLogger(ThreadedMessage.class);

    /**
     * Creates a new message with the sentDate initialized to the
     * current time, but leaves all other parameters null.
     */

    public ThreadedMessage() {
        super(BASE_DATA_OBJECT_TYPE);
    }

    /**
     * Creates a threaded message from a party with a given subject.
     *
     * @param f the party sending the message
     * @param s the subject of the message
     */

    public ThreadedMessage(Party f, String s) {
        this(f,s,null);
    }

    /**
     * Creates a threaded message from a party with a given subject and body.
     *
     * @param from the party sending the message
     * @param subject the subject of the message
     * @param body the plain-text body of the message
     */

    public ThreadedMessage(Party from, String subject, String body) {
        // Call the default constructor to initialize
        this();

        setFrom(from);
        setSubject(subject);
        setText(body);
    }

    /**
     * Creates a threaded message from its underlying data type.
     *
     * @param type the DataObject type.
     */

    public ThreadedMessage(String type) {
        super(type);
    }

    /**
     * Creates a threaded message from its underlying data object.
     *
     * @param dataObject the DataObject representing this message.
     */

    public ThreadedMessage(DataObject dataObject) {
        super(dataObject);
    }

    /**
     * Creates a threaded message by retrieving it from the database
     * using its id;
     *
     * @param key the id of the message.
     */

    public ThreadedMessage(BigDecimal id)
        throws DataObjectNotFoundException
    {
        super(new OID(BASE_DATA_OBJECT_TYPE, id));
    }

    /**
     * Creates a threaded message by retrieving it from the database
     * using its OID.
     *
     * @param oid the OID of the message
     */

    public ThreadedMessage(OID oid)
        throws DataObjectNotFoundException
    {
        super(oid);
    }

    public ThreadedMessage newInstance() {
        return new ThreadedMessage();
    }

    /**
     * Gets a new message suitable for a reply to this message, with
     * the given sender and message body.
     *
     * @param from the Party sending the reply
     * @param body the text/plain body of the reply
     */

    public ThreadedMessage replyTo(Party from, String body) {
        ThreadedMessage reply = replyTo();
        reply.setFrom(from);
        reply.setText(body);

        return reply;
    }

    /**
     * Gets a new message that is suitable for a reply to this
     * message. The message object returned will have its root and
     * sort key properties initialized so that it is a valid child of
     * this message in the message tree.
     *
     * <p>For example, if root = 14 and sortKey = 04a, the new message
     * will have root = 14 and sortKey = 04a000.
     *
     * <p>If this message already has many responses, the new sort key
     * will be computed based on the maximum value of the current
     * responses. Absolute uniqueness of sort keys is not guaranteed,
     * but conflicts are highly unlikely.
     *
     * @return a new child response to this message.
     */

    public ThreadedMessage replyTo() {
        return replyTo(newInstance());
    }

    public ThreadedMessage replyTo(ThreadedMessage reply) {        
        SortKey nextKey = getNextChild();        

        // Initialize the basic properties

        reply.getReplyInfo(this);

        // Initialize the root message
        if (getRoot() == null) {
            reply.setRootID(getID());
        } else {
            reply.setRootID(getRoot());
        }

        // Initialize the sort key
        reply.setSortKey(nextKey);
        reply.setRefersTo(getRefersTo());
        getThread().updateForNewMessage(reply);
        getThread().save();
        return reply;
    }

    /**
     * Gets the ID of the root message associated with this family of
     * messages.
     * @return the ID of the root message
     */

    public BigDecimal getRoot() {
        return (BigDecimal) get(ROOT_ID);
    }

    public ThreadedMessage getRootMsg() {
        if (m_root == null) {
            DataObject rootData = (DataObject) get(ROOT);
            if (rootData != null) {
                m_root = new ThreadedMessage(rootData);
            }
        }
        return m_root;
    }

    /**
     * @deprecated Use the replyTo() method instead of this method
     * @throws UnsupportedOperationException
     */

    public void setRoot(BigDecimal root) {
        throw new UnsupportedOperationException
            ("ThreadedMessage.setRoot() is no longer supported. " +
             "Use ThreadedMessage.replyTo() instead.");
    }

    private void setRootID(BigDecimal root) {
        set(ROOT_ID, root);
    }

    /**
     * Gets the value of the sort key (possibly null).
     * @return the sort key
     */

    public SortKey getSortKey() {
        String key = (String) get(SORT_KEY);
        return key != null ? new SortKey(key) : null;
    }

    /**
     * Sets the value of the sort key.
     * @param key is the sort key for this message
     */

    public void setSortKey(SortKey key) {
        set(SORT_KEY, key.toString());
    }

    /**
     * Gets the depth of the message within a tree of messages.
     */

    public int getDepth() {
        SortKey key = getSortKey();
        return  key == null ? 0 : key.getDepth();
    }

    /**
     * Gets the number of replies in this thread.  Note -- this is not
     * the number of replies below this message, but the total number
     * in the entire thread to which this message belongs.
     *
     * @deprecated use getThread().getNumReplies();
     */
    public long getNumReplies() {
        return getThread().getNumReplies();
    }

    private void setThread(MessageThread mt) {
        m_thread = mt;
    }

    /**
     * gets the MessageThread that this message belongs to
     */
    public MessageThread getThread() {
        if (m_thread == null) {
            BigDecimal rootID = getRoot();
            ThreadedMessage root = null;
            if (rootID == null) {
                root = this;
            } else {
                try {
                    root = new ThreadedMessage(rootID);
                } catch (DataObjectNotFoundException e) {
                    throw new UncheckedWrapperException(
                                               "Tried to retrieve the root message " +
                                               "(believed to have ID #" + rootID + ") of message #" +
                                               getID() + " and failed.  This is indicative of " +
                                               "something profoundly wrong with the state of " +
                                               "the database.", e);
                }
            }
            m_thread = MessageThread.getFromRootMessage(root);
        }
        return m_thread;
    }

    /**
     * Saves the message after verifying that the root and sort key
     * are valid.  Also creates a new MessageThread if this is a root message.
     */

    protected void beforeSave() {
        if (getRoot() == null && getSortKey() != null) {
            throw new PersistenceException
                ("ThreadedMessage: root message must have a null sort key");
        }

        if (getRoot() != null && getSortKey() == null) {
            throw new PersistenceException
                ("ThreadedMessage: non-root message must have a sort key");
        }

        m_wasNew = isNew();

        super.beforeSave();
    }

    protected void afterSave() {
        super.afterSave();

        if (getRoot() == null && m_wasNew) {
            setThread(new MessageThread(this));
            getThread().save();
            m_wasNew = false;
        }
    }

    /**
     * Gets the sort key corresponding to the next child of this
     * message.
     *
     * @return the sort key of the next child.
     */        
    private SortKey getNextChild() {
            //      Prepare a query for the next valid sort key

             Session session = SessionManager.getSession();
             DataQuery query = session.retrieveQuery
                 ("com.arsdigita.messaging.maxSortKey");

             // Limit the query to other messages in the appropriate
             // subtree. If this message has no root, then it's at level
             // zero and we search for other messages that have this one as
             // root.  Otherwise we search for messages with the same root
             // and sort keys that are children of this one.

             Filter f;

             SortKey parent = getSortKey();
             SortKey child;

             if (getRoot() == null) {
                 f = query.addFilter("root = :root");
                 f.set("root", getID());

                 f = query.addEqualsFilter("sortSize", "3");
             } else {
                 f = query.addFilter("root = :root");
                 f.set("root", getRoot());

                 f = query.addFilter("sortKey like :parent");
                 f.set("parent", parent + "%");
                             
                 f = query.addFilter("inReplyTo = :reply");
                 f.set("reply", getID());
            
                 f = query.addFilter("sortSize > :parentSize");
                 f.set("parentSize", new Integer(parent.length()));
             }

             if (query.next()) {
                 child = new SortKey((String)query.get("sortKey"));                 
                 child.next();
                 query.close();
             } else {
                 child = (parent == null) ? new SortKey() : parent.getChild();
             }

             return child;        
    }
}
