/*
 * This file is part of the Poliqarp suite.
 * 
 * Copyright (C) 2004-2009 by Instytut Podstaw Informatyki Polskiej
 * Akademii Nauk (IPI PAN; Institute of Computer Science, Polish
 * Academy of Sciences; cf. www.ipipan.waw.pl).  All rights reserved.
 * 
 * This file may be distributed and/or modified under the terms of the
 * GNU General Public License version 2 as published by the Free Software
 * Foundation and appearing in the file gpl.txt included in the packaging
 * of this file.  (See http://www.gnu.org/licenses/translations.html for
 * unofficial translations.)
 * 
 * A commercial license is available from IPI PAN (contact
 * Michal.Ciesiolka@ipipan.waw.pl or ipi@ipipan.waw.pl for more
 * information).  Licensees holding a valid commercial license from IPI
 * PAN may use this file in accordance with that license.
 * 
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE.
 */

package ipipan.poliqarp.logic;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;

import ipipan.poliqarp.connection.AsyncHandler;
import ipipan.poliqarp.connection.PoliqarpConnection;
import ipipan.poliqarp.connection.Message;
import ipipan.poliqarp.stat.MatchCounter;
import ipipan.poliqarp.stat.StatIterator;
import ipipan.poliqarp.stat.StatQueryException;
import ipipan.poliqarp.stat.Tagset;

/**
 * A set of results of a query, with on-demand retrieval.
 *
 * This class tries to model the server's notion of a match buffer.
 * It has fixed size (can fit a fixed number of matches, though this
 * can be changed in runtime), but can contain a variable amount of matches.
 * Matches are appended at the end of the list as they come up.
 *
 * The Java counterpart tries to be as little memory-exhaustive as possible,
 * so it retrieves only these matches that are actually displayed by the GUI.
 * Retrieved matches are stored in an internal cache to minimize IPC load.
 * In addition, it is possible to invalidate some or all results (e.g. if 
 * the formatting changes and other pieces of segments are to be retrieved
 * and shown).
 *
 * Note that the internal cache doesn't necessarily have to be the same size
 * as the server-side match buffer.
 */
public final class MatchList
{
   private PoliqarpConnection connection;
   private Match[] results;
   private int resultsCount;
   private static Map<PoliqarpConnection, MatchList> factoryData;
   private int cacheSize;
   private int cacheOffset;
   private MatchCounter counter;
   private Query query;

   static {
      factoryData = new HashMap<PoliqarpConnection, MatchList>();
   }
    
   /**
    * Constructs a match list for the given connection. This factory
    * is provided instead of an usual constructor because it eliminates
    * the need to allocate and construct different match lists for
    * different queries for the same connection.
    */
   public static MatchList getMatchList(PoliqarpConnection connection, Query query)
   {
      MatchList res = factoryData.get(connection);
      if (res == null) {
         res = new MatchList(connection);
         factoryData.put(connection, res);
      } else
         res.invalidate();
      res.query = query;
      return res;
   }

   /**
    * Makes the factory no longer maintain a match list for this connection.
    * This method should be called when a connection will use no more
    * queries.
    */
   public static void unregisterConnection(PoliqarpConnection connection)
   {
      factoryData.remove(connection);
   }

   private Message queryBuffer() 
   {
      try {
         connection.send("BUFFER-STATE");
         return connection.getMessage();
      } catch (IOException e) {
         return null;
      }
   }

   private int getBufferSize()
   {
      return queryBuffer().getOKIntInfo(0);
   }

   private int getBufferUsed()
   {
      return queryBuffer().getOKIntInfo(1);
   }  
   
   MatchList(PoliqarpConnection connection)
   {
      cacheSize = 2000;
      cacheOffset = 0;
      this.connection = connection;
      results = new Match[cacheSize];
      counter = null;
   }

   public void createCounter(String groupby, Tagset tagset) 
      throws StatQueryException {
      if(groupby != null) {
         counter = new MatchCounter(groupby, tagset);
         /*
         try {
            connection.getOptions().setOrthTransmission(false, true, true, false);
            connection.send("SET left-context-width 1");
            if(!connection.getMessage().isOK())
               throw new Exception();
            connection.send("SET right-context-width 1");
            if(!connection.getMessage().isOK())
               throw new Exception();
         } catch (Exception e) {
            e.printStackTrace();
         }
         */
      } else {
         counter = null;
      }
   }

   public void runCounter() {
      if(counter != null) {
         counter.count(this);
         // counter.print();
      }
   }

   public MatchCounter getCounter() {
      return counter;
   }
    
   void queryDone(int num)
   {
      resultsCount = num;
   }

   /**
    * Queries the buffer about number of used results and updates the
    * count accordingly.
    */
   public void updateCount()
   {
      resultsCount = getBufferUsed();
   }

   /**
    * Ensures that the given index belongs to the cached portion of this
    * match list. If it doesn't, makes it appear exactly in the middle of
    * the cache (or as near the middle as possible).
    */
   private void ensureContaining(int index)
   {
      if (index >= cacheOffset && index < cacheOffset + cacheSize)
         return;
      int newstart = index - cacheSize / 2;
      if (newstart < 0)
         newstart = 0;
      if (newstart < cacheOffset) {
         int i;
         for (i = cacheSize - 1; i >= cacheOffset - newstart; i--)
            results[i] = results[i - (cacheOffset - newstart)];
         while (i >= 0)
            results[i--] = null;
      } else {
         int j = 0;
         for (int i = newstart - cacheOffset; i < cacheSize; i++) {
            results[j] = results[i];
            j++;
         }
         while (j < cacheSize) 
            results[j++] = null;
      }
      cacheOffset = newstart;
   }

   public void prefetchMatches(int from, int to)
      throws IllegalArgumentException
   {
      ensureContaining(from);
      while (from < to) {
         while (from < to && results[from - cacheOffset] != null) 
            from++;
         int next = from;
         while (next < to && results[next - cacheOffset] == null)
            next++;
         try {
            connection.send("GET-RESULTS " + from + " " + (next - 1));
            if (connection.getMessage().isOK()) {
               for (int i = from; i < next; i++)
                  results[i - cacheOffset] = new Match(query, connection);
            }
         } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException();
         }
         from = next;
      }
   }

   /**
    * Returns the <code>index</code>th match on this list.
    *
    * @throws IllegalArgumentException if the parameter is not a valid index
    * of a match.
    */
   public Match getMatch(int index) throws IllegalArgumentException
   {
      if (index < 0 || index >= resultsCount)
         throw new IllegalArgumentException();
      ensureContaining(index);
      Match res = results[index - cacheOffset];
      if (res == null) {
         prefetchMatches(index, index + 1);
         res = results[index - cacheOffset];
      }
      return res;
   }

   /**
    * Returns the wide context for the <code>index</code>th match on this 
    * list.
    * 
    * @throws IllegalArgumentException if the parameter is not a valid index
    * of a match.
    */
   public WideContext getWideContext(int index) 
      throws IllegalArgumentException
   {
      WideContext result = null;
      if (index < 0 || index >= resultsCount)
         throw new IllegalArgumentException();
      try {
         result = new WideContext(connection, index);
      } catch (Exception e) {
         throw new IllegalArgumentException();
      }
      return result;
   }

   /**
    * Returns an iterator over all results of the underlying query.
    */
   public Iterator<Match> getIterator()
   {
      return counter == null ? 
         new MatchIterator(connection, query, resultsCount) :
         new StatIterator(counter);
   }
   
   /**
    * Returns the set of metadata for the <code>index</code>th match on this 
    * list.
    * 
    * @throws IllegalArgumentException if the parameter is not a valid index
    * of a match.
    */
   public MetaData getMetaData(int index) 
      throws IllegalArgumentException
   {
      MetaData result = null;
      if (index < 0 || index >= resultsCount)
         throw new IllegalArgumentException();
      try {
         connection.send("METADATA " + index);
         Message msg = connection.getMessage();
         if (msg.isOK()) {
            result = new MetaData(connection, msg.getOKIntInfo(0));
         }
      } catch (Exception e) {
         e.printStackTrace();
         throw new IllegalArgumentException();
      }
      return result;
   }
   
   /**
    * Returns the number of matches held by this list.
    */
   public int count()
   {
      return resultsCount;
   }

   /**
    * Invalidate all matches, which is tantamount to flushing the internal
    * cache.
    */
   public void invalidate() 
   {
      for (int i = 0; i < cacheSize; i++)
         results[i] = null;
   }

   /**
    * Sorts the results in this match buffer.
    */
   public void sortLetter(char spec, Runnable whenDone)
   {
      final Runnable r = whenDone;
      AsyncHandler handler = new AsyncHandler() {
         public void handle(String message)
         {
            if (message.equals("SORTED")) {
               connection.setAsyncHandler(null);
               javax.swing.SwingUtilities.invokeLater(r);
            }
         }
      };
      connection.setAsyncHandler(handler);
      try {
         synchronized (handler) {
            connection.send("SORT " + spec);
            Message reply = connection.getMessage();
            if (!reply.isOK()) {
               return;
            }
         }
      } catch (Exception e) {}
   }
      
   public void sort(int column, boolean alpha, boolean ascending,
      Runnable whenDone)
   {
      char spec = (char)((int)'A' + column + (alpha ? 0 : 4));
      if (!ascending)
         spec = Character.toLowerCase(spec);
      sortLetter(spec, whenDone);
   }
}
