/*
 * $Id: FileNewsrc.java,v 1.3 2004/06/09 18:24:40 dog Exp $
 * Copyright (C) 2002 The Free Software Foundation
 * 
 * This file is part of GNU inetlib, a library.
 * 
 * GNU inetlib 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 inetlib 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.inet.nntp;

import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * A .newsrc configuration on a filesystem.
 *
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 * @version $Revision: 1.3 $ $Date: 2004/06/09 18:24:40 $
 */
public class FileNewsrc implements Newsrc
{

  private static final String NEWSRC_ENCODING = "US-ASCII";

  protected File file;

  protected List subs = null;
  protected List groups = null;
  protected Map lines = null;
  protected boolean dirty;
  protected boolean debug;

  /**
   * Constructs a FileNewsrc for the specified url.
   */
  public FileNewsrc (File file, boolean debug)
    {
      this.file = file;
      this.debug = debug;
    }

  public void close ()
    {
      if (!dirty)
        {
          return;
        }
      save ();
    }

  /**
   * Load the file.
   */
  void load ()
    {
      long fs = file.length ();
      long max = (long) Integer.MAX_VALUE;
      int bs = (int) (fs > max ? max : fs);

      groups = new LinkedList ();
      lines = new HashMap (bs / 20);
      subs = new LinkedList ();

      // Load
      try
        {
          long t1 = System.currentTimeMillis ();
          //long rcount = 0;
          if (debug)
            {
              System.err.println ("DEBUG: nntp: newsrc loading " +
                                  file.getPath ());
            }

          FileInputStream fr = new FileInputStream (file);
          InputStreamReader ir = new InputStreamReader (fr, NEWSRC_ENCODING);
          BufferedReader reader = new BufferedReader (ir, bs);
          //long tt1 = System.currentTimeMillis ();
          String line = reader.readLine ();
          //long tt2 = System.currentTimeMillis ();
          //rcount += (tt2 - tt1);
          while (line != null)
            {
              int cp = line.indexOf (':');
              if (cp > -1)
                {
                  // Subscribed newsgroup
                  String name = line.substring (0, cp);
                  groups.add (name);
                  subs.add (name);
                  cp++;
                  if (cp < line.length ())
                    {
                      String tail = line.substring (cp).trim ();
                      if (tail.length() > 0)
                        {
                          lines.put (name, tail);
                        }
                    }
                }
              else
                {
                  int pp = line.indexOf ('!');
                  if (pp > -1)
                    {
                      // Unsubscribed newsgroup
                      String name = line.substring (0, pp);
                      groups.add (name);
                      pp++;
                      if (pp < line.length ())
                        {
                          String tail = line.substring (pp).trim ();
                          if (tail.length () > 0)
                            {
                              lines.put (name, tail);
                            }
                        }
                    }
                  // else ignore - comments etc will not be saved!
                }
              //tt1 = System.currentTimeMillis ();
              line = reader.readLine ();
              //tt2 = System.currentTimeMillis ();
              //rcount += (tt2 - tt1);
            }
          reader.close ();

          long t2 = System.currentTimeMillis ();

          //System.err.println ("rcount=" + rcount);
          //System.err.println ("total=" + (t2 - t1));

          if (debug)
            {
              System.err.println ("DEBUG: nntp: newsrc load: " +
                                  groups.size () + " groups in " +
                                  (t2 - t1) + "ms");
            }
        }
      catch (FileNotFoundException e)
        {
        }
      catch (IOException e)
        {
          System.err.println ("WARNING: nntp: unable to read newsrc file");
          if (debug)
            {
              e.printStackTrace (System.err);
            }
        }
      catch (SecurityException e)
        {
          System.err.println ("WARNING: nntp: " +
                              "no read permission on newsrc file");
        }
      dirty = false;
    }

  /**
   * Save the file.
   */
  void save ()
    {
      try
        {
          long t1 = System.currentTimeMillis ();
          if (debug)
            {
              System.err.println ("DEBUG: nntp: newsrc saving " +
                                  file.getPath ());
            }

          int bs = (groups.size() * 20);    // guess an average line length
          FileOutputStream fw = new FileOutputStream (file);
          BufferedOutputStream writer = new BufferedOutputStream (fw, bs);
          for (Iterator i = groups.iterator (); i.hasNext ();)
            {
              String group = (String) i.next ();
              StringBuffer buffer = new StringBuffer (group);
              if (subs.contains (group))
                {
                  buffer.append (':');
                }
              else
                {
                  buffer.append ('!');
                }
              Object r = lines.get (group);
              if (r instanceof String)
                {
                  buffer.append ((String) r);
                }
              else
                {
                  RangeList ranges = (RangeList) r;
                  if (ranges != null)
                    {
                      buffer.append (ranges.toString ());
                    }
                }
              buffer.append ('\n');

              byte[] bytes = buffer.toString ().getBytes (NEWSRC_ENCODING);
              writer.write (bytes);
            }
          writer.flush ();
          writer.close ();

          long t2 = System.currentTimeMillis ();
          if (debug)
            {
              System.err.println("DEBUG: nntp: newsrc save: " +
                                 groups.size () + " groups in " +
                                 (t2 - t1) + "ms");
            }
        }
      catch (IOException e)
        {
          System.err.println ("WARNING: nntp: unable to save newsrc file");
          if (debug)
            {
              e.printStackTrace (System.err);
            }
        }
      dirty = false;
    }

  /**
   * Returns an iterator over the names of the currently subscribed
   * newsgroups.
   */
  public Iterator list ()
    {
      if (subs == null)
        {
          load ();
        }
      return subs.iterator ();
    }

  public boolean isSubscribed (String newsgroup)
    {
      if (subs == null)
        {
          load ();
        }
      return (subs.contains (newsgroup));
    }

  public void setSubscribed (String newsgroup, boolean flag)
    {
      if (subs == null)
        {
          load ();
        }
      boolean subscribed = subs.contains (newsgroup);
      if (flag && !subscribed)
        {
          subs.add (newsgroup);
          dirty = true;
        }
      else if (!flag && subscribed)
        {
          subs.remove (newsgroup);
          dirty = true;
        }
    }

  public boolean isSeen (String newsgroup, int article)
    {
      if (subs == null)
        {
          load ();
        }
      Object value = lines.get (newsgroup);
      if (value instanceof String)
        {
          value = new RangeList ((String) value);
        }
      RangeList ranges = (RangeList) value;
      if (ranges != null)
        {
          return ranges.isSeen (article);
        }
      return false;
    }

  public void setSeen (String newsgroup, int article, boolean flag)
    {
      if (subs == null)
        {
          load ();
        }
      Object value = lines.get (newsgroup);
      if (value instanceof String)
        {
          value = new RangeList ((String) value);
        }
      RangeList ranges = (RangeList) value;
      if (ranges == null)
        {
          ranges = new RangeList ();
          lines.put (newsgroup, ranges);
        }
      if (ranges.isSeen (article) != flag)
        {
          ranges.setSeen (article, flag);
          dirty = true;
        }
    }

  /**
   * A RangeList holds a series of ranges that are ordered and
   * non-overlapping.
   */
  static class RangeList
    {

      List seen;

      RangeList ()
        {
          seen = new ArrayList ();
        }

      RangeList (String line)
        {
          this ();
          try
            {
              // Parse the line at comma delimiters.
              int start = 0;
              int end = line.indexOf (',');
              while (end > start)
                {
                  String token = line.substring (start, end);
                  addToken (token);
                  start = end + 1;
                  end = line.indexOf (',', start);
                }
              addToken (line.substring (start));
            }
          catch (NumberFormatException e)
            {
              System.err.println ("ERROR: nntp: bad newsrc format: " + line);
            }
        }

      /*
       * Used during initial parse.
       */
      private void addToken (String token) throws NumberFormatException
        {
          int hp = token.indexOf ('-');
          if (hp > -1)
            {
              // Range
              String fs = token.substring (0, hp);
              String ts = token.substring (hp + 1);
              int from = Integer.parseInt (fs);
              int to = Integer.parseInt (ts);
              if (from > -1 && to > -1)
                {
                  insert (from, to);
                }
            }
          else
            {
              // Single number
              int number = Integer.parseInt (token);
              if (number > -1)
                {
                  insert (number);
                }
            }
        }

      /**
       * Indicates whether the specified article is seen.
       */
      public boolean isSeen (int num)
        {
          int len = seen.size ();
          Range[] r = new Range[len];
          seen.toArray (r);
          for (int i = 0; i < len; i++)
            {
              if (r[i].contains (num))
                {
                  return true;
                }
            }
          return false;
        }

      /**
       * Sets whether the specified article is seen.
       */
      public void setSeen (int num, boolean flag)
        {
          if (flag)
            {
              insert (num);
            }
          else
            {
              remove (num);
            }
        }

      /*
       * Find the index within seen to insert the specified article.
       * The range object at the returned index may already contain num.
       */
      int indexOf (int num)
        {
          int len = seen.size ();
          Range[] r = new Range[len];
          seen.toArray (r);
          for (int i = 0; i < len; i++)
            {
              if (r[i].contains (num))
                {
                  return i;
                }
              if (r[i].from > num)
                {
                  return i;
                }
              if (r[i].to == num - 1)
                {
                  return i;
                }
            }
          return len;
        }

      void insert (int start, int end)
        {
          Range range = new Range (start, end);
          int i1 = indexOf (range.from);
          // range is at end
          if (i1 == seen.size ())
            {
              seen.add (range);
              return;
            }
          Range r1 = (Range) seen.get (i1);
          // range is before r1
          if (range.to < r1.from)
            {
              seen.add (i1, range);
              return;
            }
          // range is a subset of r1
          if (r1.from <= range.from && r1.to >= range.to)
            {
              return;
            }
          // range is a superset of r1
          int i2 = indexOf (range.to);
          Range r2 = (Range) seen.get (i2);
          System.err.println ("r2 " + r2 + " i2 " + i2);
          // remove all ranges between
          for (int i = i2; i >= i1; i--)
            {
              seen.remove (i);
            }
          // merge
          int f = (range.from < r1.from) ? range.from : r1.from;
          int t = (range.to > r2.to) ? range.to : r2.to;
          range = new Range (f, t);
          seen.add (i1, range);
        }

      void insert (int num)
        {
          insert (num, num);
        }

      void remove (int num)
        {
          int i = indexOf (num);
          Range r = (Range) seen.get (i);
          seen.remove (i);
          // num == r
          if ((r.from == r.to) && (r.to == num))
            {
              return;
            }
          // split r
          if (r.to > num)
            {
              Range r2 = new Range (num + 1, r.to);
              seen.add (i, r2);
            }
          if (r.from < num)
            {
              Range r2 = new Range (r.from, num - 1);
              seen.add (i, r2);
            }
        }

      public String toString ()
        {
          StringBuffer buf = new StringBuffer ();
          int len = seen.size ();
          for (int i = 0; i < len; i++)
            {
              Range range = (Range) seen.get (i);
              if (i > 0)
                {
                  buf.append (',');
                }
              buf.append (range.toString ());
            }
          return buf.toString ();
        }

    }

  /**
   * A range is either a single integer or a range between two integers.
   */
  static class Range
    {
      int from;
      int to;

      public Range (int i)
        {
          from = to = i;
        }

      public Range (int f, int t)
        {
          if (f > t)
            {
              from = t;
              to = f;
            }
          else
            {
              from = f;
              to = t;
            }
        }

      public boolean contains (int num)
        {
          return (num >= from && num <= to);
        }

      public String toString ()
        {
          if (from != to)
            {
              return new StringBuffer ()
                .append (from)
                .append ('-')
                .append (to)
                .toString ();
            }
          else
            {
              return Integer.toString (from);
            }
        }

    }

}
