/*
 * NNTPFolder.java
 * Copyright (C) 2002 Chris Burdess <dog@gnu.org>
 * 
 * This file is part of GNU JavaMail, a library.
 * 
 * GNU JavaMail is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * GNU JavaMail 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 for more details.
 * 
 * You should have received a copy of the GNU 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
 * 
 * As a special exception, if you link this library with other files to
 * produce an executable, this library does not by itself cause the
 * resulting executable to be covered by the GNU General Public License.
 * This exception does not however invalidate any other reasons why the
 * executable file might be covered by the GNU General Public License.
 */

package gnu.mail.providers.nntp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.FolderNotFoundException;
import javax.mail.IllegalWriteException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
import javax.mail.MethodNotSupportedException;
import javax.mail.event.ConnectionEvent;

import gnu.inet.nntp.ArticleResponse;
import gnu.inet.nntp.GroupResponse;
import gnu.inet.nntp.HeaderEntry;
import gnu.inet.nntp.HeaderIterator;
import gnu.inet.nntp.NNTPConstants;
import gnu.inet.nntp.NNTPException;

/**
 * A JavaMail folder delegate for an NNTP newsgroup.
 *
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 * @version 2.0
 */
public final class NNTPFolder
  extends Folder
{

  String name;
  int first = -1;
  int last = -1;
  int count = -1;
  boolean open;

  Map articleCache; // cache of article-number to NNTPMessage
  
  NNTPFolder (NNTPStore store, String name)
    {
      super (store);
      this.name = name;
    }

  /**
   * Returns the name of the newsgroup, e.g. <code>alt.test</code>.
   */
  public String getName ()
    {
      return name;
    }

  /**
   * @see #getName
   */
  public String getFullName ()
    {
      return name;
    }

  /**
   * This implementation uses a flat namespace, so the parent of any
   * NNTPFolder is the NNTP root folder.
   */
  public Folder getParent ()
    throws MessagingException
    {
      NNTPStore ns = (NNTPStore) store;
      return ns.root;
    }
  
  /**
   * Returns the type of this folder.
   * This folder type only holds messages.
   */
  public int getType ()
    throws MessagingException
    {
      return HOLDS_MESSAGES;
    }

  public boolean isOpen ()
    {
      return open;
    }

  /**
   * This folder type is always read-only.
   */
  public int getMode ()
    {
      return READ_ONLY;
    }

  /**
   * Returns the flags supported by this folder.
   */
  public Flags getPermanentFlags ()
    {
      NNTPStore ns = (NNTPStore) store;
      return new Flags (ns.permanentFlags);
    }

  /**
   * This method has no particular meaning in NNTP.
   * However, we will use it to send a GROUP command and refresh our article
   * stats.
   */
  public void open (int mode)
    throws MessagingException
    {
      // NB this should probably throw an exception if READ_WRITE is
      // specified, but this tends to cause problems with existing clients.
      if (open)
        {
          throw new IllegalStateException ();
        }
      try
        {
          NNTPStore ns = (NNTPStore) store;
          synchronized (ns.connection)
            {
              GroupResponse response = ns.connection.group (name);
              count = response.count;
              first = response.first;
              last = response.last;
            }

          articleCache = new HashMap (1024); // TODO make configurable
          open = true;
          notifyConnectionListeners (ConnectionEvent.OPENED);
        }
      catch (NNTPException e)
        {
          if (e.getResponse ().getStatus () == NNTPConstants.NO_SUCH_GROUP)
            {
              throw new FolderNotFoundException (e.getMessage (), this);
            }
          else
            {
              throw new MessagingException (e.getMessage (), e);
            }
        }
      catch (IOException e)
        {
          throw new MessagingException (e.getMessage (), e);
        }
    }

  /**
   * This method has no particular meaning in NNTP.
   */
  public void close (boolean expunge)
    throws MessagingException
    {
      if (!open)
        {
          throw new IllegalStateException ();
        }
      
      articleCache = null;
      open = false;
      notifyConnectionListeners (ConnectionEvent.CLOSED);
    }
  
  /**
   * Indicates whether the newsgroup is present on the server.
   */
  public boolean exists ()
    throws MessagingException
    {
      try
        {
          NNTPStore ns = (NNTPStore) store;
          synchronized (ns.connection)
            {
              GroupResponse response = ns.connection.group (name);
              count = response.count;
              first = response.first;
              last = response.last;
            }
          return true;
        }
      catch (NNTPException e)
        {
          if (e.getResponse ().getStatus () == NNTPConstants.NO_SUCH_GROUP)
            {
              return false;
            }
          else
            {
              throw new MessagingException (e.getMessage (), e);
            }
        }
      catch (IOException e)
        {
          throw new MessagingException (e.getMessage (), e);
        }
    }

  /**
   * Indicates whether there are new articles in this newsgroup.
   */
  public boolean hasNewMessages ()
    throws MessagingException
    {
      try
        {
          NNTPStore ns = (NNTPStore) store;
          boolean hasNew = false;
          synchronized (ns.connection)
            {
              GroupResponse response = ns.connection.group (name);
              if (response.last > last)
                {
                  hasNew = true;
                }
              count = response.count;
              first = response.first;
              last = response.last;
            }
          return hasNew;
        }
      catch (NNTPException e)
        {
          if (e.getResponse ().getStatus () == NNTPConstants.NO_SUCH_GROUP)
            {
              throw new FolderNotFoundException (e.getMessage (), this);
            }
          else
            {
              throw new MessagingException (e.getMessage (), e);
            }
        }
      catch (IOException e)
        {
          throw new MessagingException (e.getMessage (), e);
        }
    }
  
  /**
   * Returns the number of articles in this newsgroup.
   */
  public int getMessageCount ()
    throws MessagingException
    {
      return count;
    }

  /**
   * Returns the article corresponding to the specified article
   * number.
   * @throws MessageRemovedException often ;-)
   */
  public Message getMessage (int msgnum)
    throws MessagingException
    {
      if (!open)
        {
          throw new IllegalStateException ();
        }
      
      // Cache lookup
      Integer key = new Integer (msgnum);
      NNTPMessage m = (NNTPMessage) articleCache.get (key);
      if (m!=null)
        {
          return m;
        }
      
      try
        {
          NNTPStore ns = (NNTPStore) store;
          synchronized (ns.connection)
            {
              // Ensure group selected
              GroupResponse gr = ns.connection.group (name);
              first = gr.first;
              last = gr.last;
              count = gr.count;
              // Get article
              m = getMessageImpl (msgnum - 1 + first);
              // Cache store
              articleCache.put (key, m);
              return m;
            }
        }
      catch (NNTPException e)
          {
            switch (e.getResponse ().getStatus ())
              {
              case NNTPConstants.NO_ARTICLE_SELECTED:
              case NNTPConstants.NO_SUCH_ARTICLE_NUMBER:
              case NNTPConstants.NO_SUCH_ARTICLE:
                throw new MessageRemovedException (e.getMessage ());
              default:
                throw new MessagingException (e.getMessage (), e);
              }
          }
        catch (IOException e)
          {
            throw new MessagingException (e.getMessage (), e);
          }
      }

  /*
   * Perform article STAT.
   * NB not synchronized against the connection!
   */
  NNTPMessage getMessageImpl (int msgnum)
    throws IOException
    {
      NNTPStore ns = (NNTPStore) store;
      // Issue STAT
      ArticleResponse response = ns.connection.stat (msgnum);
      String messageId = response.messageId;
      return new NNTPMessage (this, msgnum, messageId);
    }

  /**
   * Returns all articles in this group.
   * This tries XHDR first to retrieve Message-IDs for the articles.
   * If this fails we fall back to statting each article.
   */
  public Message[] getMessages ()
    throws MessagingException
    {
      NNTPStore ns = (NNTPStore) store;
      List acc = new LinkedList ();
      synchronized (ns.connection)
        {
          try
            {
              // Ensure group selected
              GroupResponse gr = ns.connection.group (name);
              first = gr.first;
              last = gr.last;
              count = gr.count;
              // Get Message-IDs for all article numbers
              StringBuffer rb = new StringBuffer ();
              rb.append (Integer.toString (first));
              rb.append ('-');
              rb.append (Integer.toString (last));
              HeaderIterator i = ns.connection.xhdr ("Message-ID",
                                                     rb.toString ());
              while (i.hasNext ())
                {
                  HeaderEntry entry = i.nextHeaderEntry ();
                  Integer key = new Integer (entry.getArticleId ());
                  // Cache lookup
                  NNTPMessage m = (NNTPMessage) articleCache.get (key);
                  if (m == null)
                    {
                      int msgnum = key.intValue ();
                      String messageId = entry.getHeader ();
                      m = new NNTPMessage (this, msgnum, messageId);
                      // Cache store
                      articleCache.put (key, m);
                    }
                  acc.add (m);
                }
            }
          catch (NNTPException e)
            {
              // Perhaps the server does not understand XHDR.
              // We'll do it the slow way.
              for (int i = first; i <= last; i++)
                {
                  Integer key = new Integer (i);
                  // Cache lookup
                  Message m = (NNTPMessage) articleCache.get (key);
                  if (m == null)
                    {
                      try 
                        {
                          m = getMessageImpl (i);
                          // Cache store
                          articleCache.put (key, m);
                          acc.add (m);
                        }
                      catch (NNTPException e2)
                        {
                          switch (e2.getResponse ().getStatus ())
                            {
                            case NNTPConstants.NO_ARTICLE_SELECTED:
                            case NNTPConstants.NO_SUCH_ARTICLE_NUMBER:
                            case NNTPConstants.NO_SUCH_ARTICLE:
                              break; // article does not exist, ignore
                            default:
                              throw new MessagingException (e2.getMessage (),
                                                            e2);
                            }
                        }
                      catch (IOException ie)
                        {
                          throw new MessagingException (ie.getMessage (), ie);
                        }
                    }
                }
            }
          catch (IOException e)
            {
              throw new MessagingException (e.getMessage (), e);
            }
        }
      int len = acc.size ();
      Message[] messages = new Message[len];
      acc.toArray (messages);
      return messages;
    }

  /**
   * Prefetch.
   */
  public void fetch (Message[] msgs, FetchProfile fp)
    throws MessagingException
    {
      boolean head = fp.contains (FetchProfile.Item.ENVELOPE);
      head = head || (fp.getHeaderNames ().length > 0);
      boolean body = fp.contains (FetchProfile.Item.CONTENT_INFO);
      int op = (head && body) ? 3 : head ? 2 : body ? 1 : 0;
      try
        {
          NNTPStore ns = (NNTPStore) store;
          for (int i = 0; i < msgs.length; i++)
            {
              Message msg = msgs[i];
              if (msg == null || !(msg instanceof NNTPMessage))
                {
                  continue;
                }
              NNTPMessage message = (NNTPMessage) msg;
              String messageId = message.getMessageId ();

              ArticleResponse response = null;
              synchronized (ns.connection)
                {
                  switch (op)
                    {
                    case 3: // head & body
                      response = ns.connection.article (messageId);
                      break;
                    case 2: // head
                      response = ns.connection.head (messageId);
                      break;
                    case 1: // body
                      response = ns.connection.body (messageId);
                      break;
                    }
                  ByteArrayOutputStream out = new ByteArrayOutputStream ();
                  byte[] buf = new byte[4096];
                  for (int len = response.in.read (buf);
                       len >- 1;
                       len = response.in.read (buf))
                    {
                      out.write (buf, 0, len);
                    }
                  switch (op)
                    {
                    case 3: // head & body
                      ByteArrayInputStream hbin =
                        new ByteArrayInputStream (out.toByteArray ());
                      message.updateHeaders (hbin);
                      int len = hbin.available ();
                      byte[] content = new byte[len];
                      hbin.read (content);
                      message.updateContent (content);
                      break;
                    case 2: // head
                      ByteArrayInputStream hin =
                        new ByteArrayInputStream (out.toByteArray ());
                      message.updateHeaders (hin);
                      break;
                    case 1: // body
                      message.updateContent (out.toByteArray ());
                      break;
                    }
                }
            }
        }
      catch (NNTPException e)
        {
          switch (e.getResponse ().getStatus ())
            {
            case NNTPConstants.NO_GROUP_SELECTED:
              throw new IllegalStateException (e.getMessage ());
            case NNTPConstants.NO_ARTICLE_SELECTED:
            case NNTPConstants.NO_SUCH_ARTICLE_NUMBER:
            case NNTPConstants.NO_SUCH_ARTICLE:
              throw new MessageRemovedException (e.getMessage ());
            default:
              throw new MessagingException (e.getMessage (), e);
            }
        }
      catch (IOException e)
        {
          throw new MessagingException (e.getMessage (), e);
        }
    }

  // -- Subscription --

  /**
   * Indicates if the newsgroup is subscribed.
   * This uses the newsrc mechanism associated with this folder's store.
   */
  public boolean isSubscribed ()
    {
      NNTPStore ns = (NNTPStore) store;
      return ns.newsrc.isSubscribed (name);
    }

  /**
   * Subscribes or unsubscribes to this newsgroup.
   * This uses the newsrc mechanism associated with this folder's store.
   */
  public void setSubscribed (boolean flag)
    throws MessagingException
    {
      NNTPStore ns = (NNTPStore) store;
      ns.newsrc.setSubscribed (name, flag);
    }

  boolean isSeen (int articleNumber)
    {
      NNTPStore ns = (NNTPStore) store;
      return ns.newsrc.isSeen (name, articleNumber);
    }

  void setSeen (int articleNumber, boolean flag)
    {
      NNTPStore ns = (NNTPStore) store;
      ns.newsrc.setSeen (name, articleNumber, flag);
    }

  // -- Stuff we can't do --

  /**
   * This folder type does not contain other folders.
   */
  public Folder getFolder (String name)
    throws MessagingException
    {
      throw new MethodNotSupportedException ();
    }

  /**
   * This folder type does not contain other folders.
   */
  public Folder[] list (String pattern)
    throws MessagingException
    {
      throw new MethodNotSupportedException ();
    }

  /**
   * This folder type does not contain other folders.
   */
  public Folder[] listSubscribed (String pattern)
    throws MessagingException
    {
      return list (pattern);
    }

  /**
   * If we move away from a flat namespace, this might be useful.
   */
  public char getSeparator ()
    throws MessagingException
    {
      return '.';
    }

  /**
   * NNTP servers are read-only.
   */
  public boolean create (int type)
    throws MessagingException
    {
      throw new MethodNotSupportedException ();
    }

  /**
   * NNTP servers are read-only.
   */
  public boolean delete (boolean recurse)
    throws MessagingException
    {
      throw new MethodNotSupportedException ();
    }

  /**
   * NNTP servers are read-only.
   */
  public boolean renameTo (Folder folder)
    throws MessagingException
    {
      throw new MethodNotSupportedException ();
    }

  /**
   * NNTP servers are read-only.
   */
  public void appendMessages (Message[] messages)
    throws MessagingException
    {
      throw new IllegalWriteException();
    }

  /**
   * NNTP servers are read-only.
   */
  public Message[] expunge ()
    throws MessagingException
    {
      throw new IllegalWriteException ();
    }

}
