package com.k_int.sru.search_widget;

import java.awt.BorderLayout;
import java.awt.Component;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.k_int.sru.search_widget.bean.QueryStringBean;
import com.k_int.sru.search_widget.bean.ServiceBean;
import com.k_int.sru.search_widget.dto.SRWResultDTO;
import com.k_int.sru.search_widget.dto.SRWResultPackageDTO;
import com.k_int.sru.search_widget.ui.WidgetTabbedPane;
import com.k_int.sru.search_widget.ui.panel.ActionPanel;
import com.k_int.sru.search_widget.ui.panel.FullResultPanelBuilder;
import com.k_int.sru.search_widget.ui.panel.ResultPanel;
import com.k_int.sru.search_widget.ui.panel.StatusPanel;
import com.k_int.sru.search_widget.ui.table.ResultTableModel;
import com.k_int.sru.search_widget.util.Searcher;
import com.k_int.sru.search_widget.util.ServiceLoader;
import com.k_int.sru.search_widget.util.WidgetListener;


public class SRUSearchWidgetImpl extends JPanel implements SRUSearchWidget, Observer {
  private static final long   serialVersionUID = 1L;
  private static final Log    log              = LogFactory.getLog(SRUSearchWidgetImpl.class);
  private List<SRWResultDTO>  results          = new ArrayList<SRWResultDTO>();
  private WidgetListener      listener         = null;
  private Searcher            searcher;
  private Thread              search_thread;  
  private WidgetTabbedPane    tabbed_pane;
  private WidgetConfig        config;
  private StatusPanel         status_pane;
  private static List<String> VALID_CQL_OPS = Arrays.asList(new String[]{"and", "or", "not"});
  
  public SRUSearchWidgetImpl() {
    this(new WidgetConfig());
  }
  
  
  public SRUSearchWidgetImpl(WidgetConfig config) {
    this.config = config;

    log.info("Constructing SRUSearchWidgetImpl");

    status_pane = new StatusPanel();
    listener    = new WidgetListener(this);
    tabbed_pane = new WidgetTabbedPane(this);
    
    setLayout(new BorderLayout());
    add(tabbed_pane, BorderLayout.CENTER);
    add(status_pane, BorderLayout.SOUTH); 
        
    loadServices();
    setPreferredSize(config.getPreferredSize());
  }
  

  public void loadServices() {
    if (config.getServices()!=null) {
      ServiceLoader service_loader = new ServiceLoader(this, null);
      new Thread(service_loader).start();
    }
  }
  
  
  public void loadService(ServiceBean bean) {
    if (bean!=null) {
      ServiceLoader service_loader = new ServiceLoader(this, bean);
      new Thread(service_loader).start();
    }
  }
  
  
  
  /**
   * Loads a set of search results into the result tab and enables it for interaction.
   */
  public void setResult(SRWResultPackageDTO result, boolean cached) {
    if (result.getResults().size()>0) {
      tabbed_pane.setEnabledAt(tabbed_pane.getResultsTabIndex(), true);
      if (!cached) {
        addResults(result.getResults());
      }
      tabbed_pane.getResultsPanel().setResult(result);
    } else  {
      tabbed_pane.setEnabledAt(tabbed_pane.getResultsTabIndex(), false);
      log.info("no results returned");
    }
  }
  

  /**
   * Adds the new list to the map of cached search results.  
   * 
   * @param new_results The {@link List} of {@link SRWResultDTO} to be added to the cache.
   */
  private void addResults(List<SRWResultDTO> new_results) {
    results.addAll(new_results);
  }
  
  
  /**
   * Clears results from results tab. 
   */
  public void clearResults() {
    results = new ArrayList<SRWResultDTO>();
    tabbed_pane.getResultsPanel();
  }
  
  
  /**
   * Displays the given result in a new tab on the {@link JTabbedPane} or selects the already created tab if it exists.
   * 
   * @param result The {@link SRWResultDTO} to display.
   */
  public void addResultTab(SRWResultDTO result) {
    String title = result.getRecordPosition()+": "+result.getTitles().get(0);
    if (title.length()>15) {
      title = title.substring(0, 15);
    }
    int idx = tabbed_pane.findTab(title);
      
    if (idx == -1) {
      log.info("adding tab with title of "+title);
      tabbed_pane.add(title, FullResultPanelBuilder.build(result));
      tabbed_pane.setSelectedIndex(tabbed_pane.getTabCount()-1);
      
    } else {      
      log.info("Focusing on tab with title "+title);
      tabbed_pane.setSelectedIndex(idx);
    }
  }
  
  
  public void startSearch(Integer start_rec, boolean clear) {
    if (clear==true) {
      results = new ArrayList<SRWResultDTO>(); 
    }

    String query                = constructQuery(start_rec);
    QueryStringBean schema_bean = config.getQueryStringsMap().get("recordSchema");
    String schema               = "dc";
    
    if (schema_bean!=null && schema_bean.getValue()!=null) { 
      schema = schema_bean.getValue();
    }
    
    if (query!=null) {  
      status_pane.setStatus("Searching");
      status_pane.setTicking(true);
    
      tabbed_pane.getSearchPanel().getSearchButton().setEnabled(false);
      tabbed_pane.getSearchPanel().getStopButton().setEnabled(true);
    
      String service     = (String) tabbed_pane.getSearchPanel().getSelectedItem();
      ServiceBean bean  = config.getServiceByName(service);
      searcher           = new Searcher(bean, query, schema, status_pane.getLogger(), config);
      searcher.addObserver(this);
      search_thread = new Thread(searcher);
      tabbed_pane.getResultsPanel().setButtonState(false);
      search_thread.start();
    }
  }
  
  
  /**
   * Creates a {@link SRWResultPackageDTO} object to pass to the {@link ResultPanel}.  
   * 
   * @param start_rec The record position of the first record to get from the cache.
   * @param max_rec   The number of records to get from the cache.
   * @return The created {@link SRWResultPackageDTO}.
   */
  public SRWResultPackageDTO getCachedResults(Integer start_rec, Integer max_rec) {
    if (start_rec<1) { start_rec = 1; }
        
    SRWResultPackageDTO result    = new SRWResultPackageDTO();
    List<SRWResultDTO> cached_res = new ArrayList<SRWResultDTO>();
    result.setStartRecordNumber(start_rec);
    
    for (int i=start_rec-1; i<max_rec; i++) {
      cached_res.add(results.get(i));  
    }
    
    result.setResults(cached_res);
    result.setNumberOfResults(tabbed_pane.getResultsPanel().getCurrentResult().getNumberOfResults());
    
    return result;
  }
  
  
  /**
   * Checks if the range of records starting at the start parameter and ending at start+max_rec are cached. 
   * 
   * @param start The record position to start from.
   * @param end   The end record position.
   * @return Whether the range specified is cached.
   */
  public boolean isCached(Integer start, Integer end) {
    boolean cached = true;
    if (start<1) start = 1;
    
    for (int i=start; i<=(end); i++) {
      if (!hasRecordPos(i)) return false;
    }
    
    return cached;
  }
    
  
  private boolean hasRecordPos(int pos) {
    for (SRWResultDTO r : results) {
      if (r.getRecordPosition()==pos) {
        return true;
      }
    }
    return false;
  }
  
  
  public void stopSearch() {
    searcher.stop();
    search_thread.interrupt();
    searcher = null;
    status_pane.setStatus("Cancelled");
    status_pane.setTicking(false);
    
    tabbed_pane.getSearchPanel().getSearchButton().setEnabled(true);
    tabbed_pane.getSearchPanel().getStopButton().setEnabled(false);
  }
  
  
  /**
   * Updates the search results with the records retrieved by the {@link Searcher}.
   * 
   * @param searcher The {@link Searcher} {@link Observable}.
   * @param arg The {@link SRWResultPackageDTO} that contains the information retrieved from the remote SRU service.
   */
  public void update(Observable obs, Object arg) {
    if (obs instanceof Searcher) {
      try {
        SRWResultPackageDTO result = (SRWResultPackageDTO) arg;
        if (result!=null && result.getResults().size()>0) {
          setResult(result, false);
          tabbed_pane.setSelectedIndex(tabbed_pane.getResultsTabIndex());
          status_pane.setStatus("Got results");
        } else {
          if (result!=null) {
            status_pane.setStatus("No records found.");
          } else {
            status_pane.setStatus("Bad response.");
          }
          Searcher s = (Searcher) searcher;
          if (s!=null && s.getError()!=null) {
            status_pane.setError(s.getError());
          }
        }

      } catch (ClassCastException e) {
        e.printStackTrace();
        
      } finally {
        status_pane.setTicking(false);
        tabbed_pane.getSearchPanel().getSearchButton().setEnabled(true);
        tabbed_pane.getSearchPanel().getStopButton().setEnabled(false);
        tabbed_pane.getResultsPanel().setButtonState(true);
      }
    }
  }  
  
  
  /** 
   * Parses the query strings and returns a URL valid string
   * 
   * @return the parsed list of QueryStringDTO as a String.  
   */
  private String constructQuery(Integer start_rec) {
    String ret = "";
    int i      = 0;
    
    Map<String, QueryStringBean> beans = config.getQueryStringsMap();
    for (Map.Entry<String, QueryStringBean> ent : beans.entrySet()) {
      if (ent.getValue().getEnabled()) {
        String key = ent.getKey();
        String val = ent.getValue().getValue();
      
        if (key.equals("startRecord") && start_rec!=null) {
          val = ""+start_rec;
        }
        ret += key+"="+val;

        if (i<(beans.size()-1)) ret += "&";
      }
      i++;
    }
    
 
    Map<String, String> query_strings = tabbed_pane.getSearchPanel().getQueryStrings();
    if (query_strings.size()>0) {
      String cql = buildCQL(query_strings);
    
      try {
        cql = URLEncoder.encode(cql, "UTF-8");
      } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
      }

      ret  += "&query="+cql;
      
    } else {
      ret = null;
    }
    
    return ret;
  }
 
  
  private String buildCQL(Map<String, String> query_strings) {
    String cql = "";
    int k      = 0;
    
    for (Map.Entry<String, String> ent : query_strings.entrySet()) {
      String key = ent.getKey();
      String val = ent.getValue();

      List<String> vals = new ArrayList<String>();

      // parse and retain any quoted sections of the query
      boolean has_quotes = true;
      while (has_quotes) {

        // is there a quote in the string at all?
        int start = val.indexOf("\"");
        if (start!=-1) {
          int end = val.indexOf("\"", start+1);

          // is there a quoted section?
          if (end!=-1) {
            // first split the start of the string (before quoted section) down and add to list
            vals.addAll(Arrays.asList(val.substring(0, start).split(" ")));

            // add full quoted section to list (removing quotes)
            vals.add(val.substring(start+1, end));

            // finally crop string down, removing quoted section and its prefix
            val = val.substring(end+1);
          } else {
            has_quotes = false;
          }
        } else {
          has_quotes = false;
        }
      }

      // split down rest of string (after having removed any quoted parts)
      vals.addAll(Arrays.asList(val.split(" ")));

      // ensure list contains no blank entries
      List<String> new_vals = new ArrayList<String>();
      for (String s : vals) {
        if (s!=null && !s.equals("") && !s.equals(" ")) {
          new_vals.add(s);
        }
      }
      vals = new_vals;

      // now itterate through the fully cleaned up list of strings and parse them into cql
      String key_query = "(";
      for (int j=0; j<vals.size(); j++) {
        key_query += key+"=\""+vals.get(j)+"\"";

        if (j<(vals.size()-1)) {
          String next = vals.get(j+1);
          String op = "or";
          if (VALID_CQL_OPS.contains(next)) {
            op = next;
            j++;
          }
 
          key_query += " "+op+" ";
        }
      }
      key_query += ")";
  
      cql += key_query;

      if (k < (query_strings.size()-1)) {
        cql+=" and ";
      }
      k++;
    }
    
    return cql;
  }
  
 
  public void setQuery(String query) 
  { 
    if (query==null) throw new NullPointerException("Cannot set a query to the widget which is null.");
    query = query.trim();

    if (!query.substring(0, 1).equals("(") && !query.substring(query.length()-1).equals("\")")) 
    {
      query = "("+query+")";
    }

    System.out.println("Initial query is "+query);
    List<String> strs = new ArrayList<String>();   
    int paren_start   = query.indexOf("(");
    int quote_1       = query.indexOf("\"",paren_start);
    int quote_2       = query.indexOf("\"",quote_1+1);
    int  paren_end    = query.indexOf(")",quote_2);
    
    boolean has_paren = paren_start!=-1;
    while (has_paren) 
    {      
      System.out.println("Quote 1 is "+quote_1);
      System.out.println("Quote 2 is "+quote_2);
      System.out.println("Paren end is "+paren_end);
        
        if(paren_end>quote_1 && paren_end<quote_2)
        {     
          quote_1=query.indexOf("\"",quote_2+1);
          if(quote_1==-1)
          {
            paren_end = query.indexOf(")",quote_2);
            String split_str = query.substring(paren_start+1, paren_end);
            strs.add(split_str);
            has_paren=false;
          }
          else
          {
            quote_2=query.indexOf("\"",quote_1+1);
            paren_end = query.indexOf(")",quote_2);
          }
        }
        else
        {
          quote_1 = query.indexOf("\"",quote_2+1);
          if(quote_1==-1)
          {
            String split_str = query.substring(paren_start+1, paren_end);
            strs.add(split_str);
            has_paren=false;
          }
          else if(paren_end<quote_1)
          {
            String split_str = query.substring(paren_start+1, paren_end);
            System.out.println("Adding str "+split_str);
            strs.add(split_str);
            query=query.substring(paren_end+1);
            System.out.println("New query is "+query);
            paren_start   = query.indexOf("(");
            quote_1       = query.indexOf("\"",paren_start);
            quote_2       = query.indexOf("\"",quote_1+1);
            paren_end     = query.indexOf(")",quote_2);
            has_paren     = paren_start!=-1;          
          }
          else
          {
            quote_2=query.indexOf("\"",quote_1+1);
            //paren_end = query.indexOf(")",quote_2);
          }
        } 
    }
   
    System.out.println("Strs are "+strs);
     // now manipulate the split apart query into a format the search widget understands
    Map<String, String> parsed_query = new HashMap<String, String>();
    for (String s : strs) {
      boolean has_more = true;

      while (has_more) {
        int equals_pos = s.indexOf("=");
        if (equals_pos>0) {
          String op   = null;
          String qual = s.substring(0, equals_pos);

          if (qual.indexOf(" ")!=-1) {
            op   = qual.substring(0, qual.indexOf(" "));
            qual = qual.substring(qual.indexOf(" ")+1);
          }
          
          qual = qual.trim();

          int quote_start = s.indexOf("\"");

          String term = null;
          int end = 0;

          if (quote_start>-1) {
            int quote_end = s.indexOf("\"", s.indexOf("\"")+1);
            if (quote_end>quote_start) {
              end  = s.indexOf(" ", quote_end);
              term = s.substring(quote_start+1, quote_end);
            }
          } else {
            end = s.indexOf(" ");
          }

          if (end==-1) {
            has_more = false;
          } else {
            s = s.substring(end);
          }

          if (term.indexOf(" ")>-1) term = "\""+term+"\"";

          String pq = null;
          if (parsed_query.get(qual)==null) {
            pq = term;
          } else {
            pq = parsed_query.get(qual)+" ";
            if (op!=null) pq += op+" ";
            pq += term;
          }

          parsed_query.put(qual, pq);
          s = s.trim();
        } else {
          has_more = false;
        }
      }
    }

    // finally use the parsed queries as text for search screen
    setQueryValues(parsed_query);
  }
 
 
  public List<SRWResultDTO> getSelectedResults() {
    List<SRWResultDTO> selected_results = new ArrayList<SRWResultDTO>();

    JTable table           = tabbed_pane.getResultsPanel().getResultTable();
    ResultTableModel model = (ResultTableModel)table.getModel();
    
    for (int i=0; i<model.getRowCount(); i++) {
      SRWResultDTO res = model.getResultAt(i);
      if (res.getIsSelected()) {
        selected_results.add(res);
      }
    }

    return selected_results;
  }

  
  public void addAction(List<Action> actions) {
    addAction(actions, WidgetTabbedPane.TAB_RESULTS);
  }
  
  
  public void addAction(List<Action> actions, String tab_name) {
    int idx = tabbed_pane.findTab(tab_name);
      
    if (idx != -1) {
      Component c = tabbed_pane.getComponent(idx);
      if (c instanceof JScrollPane) {
        c = ((JScrollPane)c).getViewport().getView(); 
      }
      if (c instanceof ActionPanel) {
        ActionPanel ap = (ActionPanel) c;
        List<JButton> buttons = new ArrayList<JButton>();
        for (Action action : actions) {
          buttons.add(new JButton(action));
        }
        ap.addButton(buttons);
      } else {
        throw new ClassCastException("Cannot add action buttons to panels that do not implement ActionPanel.");
      }
    }
  }
  

  public void setQueryValues(Map<String, String> query_key_and_values) {
    tabbed_pane.getSearchPanel().setQueryStrings(query_key_and_values);
  }
 
 
  public void savePreferences() {
    config.savePreferences(getSize().width, getSize().height);
  }

  
  public String getQuery() { 
    return buildCQL(tabbed_pane.getSearchPanel().getQueryStrings());
  }
  
  
  public void setConfig(WidgetConfig config) { this.config = config; }
   //perhaps set the config here for the info
   //proxy type stuff
  
  public WidgetConfig     getConfig()        { return config;        }
  public WidgetListener   getListener()      { return listener;      }
  public WidgetTabbedPane getTabbedPane()    { return tabbed_pane;   }
  public StatusPanel      getStatusPanel()   { return status_pane;   }
}