/*
 * 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.gui;

import java.util.*;
import javax.swing.table.DefaultTableModel;

import ipipan.poliqarp.logic.MatchList;
import ipipan.poliqarp.logic.Match;
import ipipan.poliqarp.logic.Query;
import ipipan.poliqarp.logic.Segment;
import ipipan.poliqarp.logic.results.ColumnType;

/**
 * A model for the table holding query results.
 * 
 * This class can be viewed as a fixed-size window onto a set of query results
 * (a <code>MatchList</code>). It remembers which part of the results are
 * displayed, can update this information, or scroll up and down. It also
 * keeps track of whether any result has a non-empty left match -- if there 
 * are no such results, the 'left match' column becomes concealed (and the
 * middle column is then named simply 'match' instead of 'right match').
 */
public class ResultModel extends DefaultTableModel
{
   private String[] columnNames;

   private Query query;
   private MatchList matches;
   private Configuration config;
   private int offset, end, limit, total;

   private List<ResultEventListener> listeners;
   private Application app;

   private boolean statModel;

   /**
    * Constructs a result model of the default size, 50.
    */
   public ResultModel(Application app)
   {
      this.app = app;
      columnNames = new String[4];
      for (int col = 0; col < 4; col++)
         columnNames[col] = "" + col;
      listeners = new ArrayList<ResultEventListener>();
      limit = 50;
      offset = end = total = 0;
      statModel = false;
   }

   /**
    * Updates the names of table columns.
    *
    * @param config the configuration that is used to retrieve
    *     language-specific texts from
    */
   public void setColumnNames(Configuration config)
   {
      if (statModel) {
         columnNames = matches.getCounter().getColumnNames();
      } else if (query == null) {
         columnNames = new String[4];
         for (int col = 0; col < 4; col++) {
            columnNames[col] = config.getComponentName("TEXT_COL" + col);
         }
      } else {
         columnNames = new String[query.getHeader().length];
         int i = 0;
         for (ColumnType type : query.getHeader())
            columnNames[i++] = config.getComponentName("TEXT_" + type.getName());
      }
      fireTableStructureChanged();
   }

   /**
    * Always returns <code><b>false</b></code>, since no cells in a result
    * table can be edited.
    */
   public boolean isCellEditable(int x, int y)
   {
      return false;
   }

   /**
    * Returns the number of columns in the modelled table.
    */
   public int getColumnCount()
   {
      return columnNames.length;
   }

   /**
    * Returns the number of results that the table displays.
    */
   public int getRowCount()
   {
      return end - offset;
   }

   /**
    * Returns the name of a column.
    *
    * @param col zero-based number of the column
    */
   public String getColumnName(int col)
   {
      return columnNames[col];
   }

   /**
    * Returns the value of a cell.
    *
    * @param row number of row
    * @param col number of column
    */
   public Object getValueAt(int row, int col)
   {
      if (statModel)
         return matches.getCounter().getCount(row+offset)[col];
      else
         return matches.getMatch(row+offset).getCell(col).toString(config);
   }

   /**
    * Sets the underlying match list for the model as well as the
    * configuration. The other is used to pass to the generated events.
    */
   public void setData(Query query, MatchList matches, Configuration config)
   {
      this.query = query;
      this.matches = matches;
      this.config = config;
      offset = end = total = 0;
      statModel = (matches != null && matches.getCounter() != null);
      setColumnNames(config);
   }

   /**
    * Returns the number (offset from the beginning of a match list) of the
    * first displayed result.
    */
   public int getStartPosition()
   {
      return offset;
   }

   /**
    * Returns the number (offset from the beginning of a match list) greater
    * by one than that of the last displayed result.
    */
   public int getEndPosition()
   {
      return end;
   }

   /**
    * Returns the total number of matches contained in the underlying match
    * list.
    */
   public int getTotal()
   {
      return total;
   }

   /**
    * Adds a result event listener for this model.
    */
   public void addResultEventListener(ResultEventListener l)
   {
      listeners.add(l);
   }

   /**
    * Sends a <code>ResultEvent</code> to all listeners registered for this
    * model.
    *
    * @param e the event to be posted
    */
   public void postEvent(ResultEvent e)
   {
      for (ResultEventListener listener : listeners)
         listener.resultChanged(e);
   }

   private void forceRefreshContents(int from, int to, boolean ins)
   {
      if (!statModel)
         matches.prefetchMatches(from, to);

      if (ins)
         fireTableRowsInserted(from - offset, to - offset);
      else
         fireTableDataChanged();
      postEvent(new ResultEvent(this, config, false));
   }

   /**
    * Updates the contents of this model, re-querying the match list
    * about its size and triggering appropriate events as necessary.
    */
   public void updateContents()
   {
      int newTotal = statModel ?
         matches.getCounter().count() :
         matches.count();
      int newEnd = offset + limit;

      if (newEnd > newTotal)
         newEnd = newTotal;
      if (newEnd != end) {
         total = newTotal;
         int start = end;
         end = newEnd;
         forceRefreshContents(start, end, true);
      } else if (newTotal != total) {
         total = newTotal;
         postEvent(new ResultEvent(this, config, false));
      }
   }

   public void clear()
   {
      offset = end = total = 0;
      fireTableDataChanged();
      postEvent(new ResultEvent(this, config, true));
   }

   /**
    * Scrolls the list 50 results up, refreshing the model.
    */
   public void scrollUp()
   {
      scroll(-limit);
   }

   /**
    * Scrolls the list 50 results down, refreshing the model.
    */
   public void scrollDown()
   {
      scroll(limit);
   }

   /**
    * Sets the content of a context area to the wide context of
    * a query result currently displayed in this model.
    *
    * @param area the context area to be updated
    * @param index index of the result
    */
   public void setAreaContent(ContextArea area, MetaDataTable table, int index)
   {
      int matchRef = statModel ?
         matches.getCounter().getMatchRef(offset+index) :
         offset + index;
      area.setContent(matches, matchRef);
      table.getModel().setSource(app.getCorpus(), matches.getMetaData(matchRef));
   }

   private void scroll(int delta)
   {
      offset += delta;
      if (offset < 0)
         offset = 0;
      if (offset >= total)
         offset = total - 1;
      end = offset + limit;
      if (end > total)
         end = total;
      forceRefreshContents(offset, end, false);
   }

   public void refreshResults()
   {
      if (matches != null) {
         matches.invalidate();
         forceRefreshContents(offset, end, false);
      }
   }

   public boolean isStatistical()
   {
      return statModel;
   }

   public void sortStat(int column, boolean ascending)
   {
      matches.getCounter().sort(column, ascending);
   }
}
