package org.apache.solr.search.xjoin;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.Iterator;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.iterators.TransformIterator;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.AutomatonQuery;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DocValuesTermsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.automaton.Automata;
import org.apache.lucene.util.automaton.Automaton;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.FieldType;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.search.QueryWrapperFilter;
import org.apache.solr.search.SolrConstantScoreQuery;
import org.apache.solr.search.SyntaxError;

/**
 * QParserPlugin for extracting join ids from the results stored in XJoin search
 * components.
 */
public class XJoinQParserPlugin extends QParserPlugin {
  
  public static final String NAME = "xjoin";

  /** For choosing the internal algorithm */
  public static final String METHOD = "method";
  
  @Override @SuppressWarnings("rawtypes")
  public void init(NamedList args) {
    // nothing to do
  }
 
  // this code is modified from TermsQParserPlugin
  private static enum Method {
    termsFilter {
      @Override
      @SuppressWarnings("unchecked")
      Query makeQuery(String fname, Iterator<BytesRef> it) {
        return new TermInSetQuery(fname, IteratorUtils.toList(it));
      }
    },
    booleanQuery {
      @Override
      Query makeQuery(String fname, Iterator<BytesRef> it) {
        BooleanQuery.Builder bq = new BooleanQuery.Builder();
        while (it.hasNext()) {
          bq.add(new TermQuery(new Term(fname, it.next())), BooleanClause.Occur.SHOULD);
        }
        return bq.build();
      }
    },
    automaton {
      @Override
      @SuppressWarnings("unchecked")
      Query makeQuery(String fname, Iterator<BytesRef> it) {
        Automaton union = Automata.makeStringUnion(IteratorUtils.toList(it));
        return new AutomatonQuery(new Term(fname), union);
      }
    },
    docValuesTermsFilter {
      @Override
      Query makeQuery(String fname, Iterator<BytesRef> it) {
        return new DocValuesTermsQuery(fname, (BytesRef[])IteratorUtils.toArray(it, BytesRef.class));
      }
    };

    abstract Query makeQuery(String fname, Iterator<BytesRef> it);
  }
  
  // transformer from Object to BytesRef (using the given FieldType)
  static private Transformer transformer(final FieldType ft) {
    return new Transformer() {
      
      BytesRefBuilder term = new BytesRefBuilder();
      
      @Override
      public BytesRef transform(Object joinId) {
        String joinStr = joinId.toString();
        // logic same as TermQParserPlugin
        if (ft != null) {
          ft.readableToIndexed(joinStr, term);
        } else {
          term.copyChars(joinStr);
        }
        return term.toBytesRef();
      }
      
    };
  }

  /**
   * Like fq={!xjoin}xjoin_component_name OR xjoin_component_name2
   */
  @Override
  @SuppressWarnings("rawtypes")
  public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
    return new XJoinQParser(qstr, localParams, params, req);
  }
  
  static class XJoinQParser<T extends Comparable<T>> extends QParser implements JoinSpec.Iterable {
    
    // record the join field when retrieving external results
    // must be the same for all external sources referenced in our query
    private String joinField;

    public XJoinQParser(String qstr, SolrParams localParams, SolrParams params,SolrQueryRequest req) {
      super(qstr, localParams, params, req);
      joinField = null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Query parse() throws SyntaxError {
      Method method = Method.valueOf(localParams.get(METHOD, Method.termsFilter.name()));
      JoinSpec<T> js = JoinSpec.parse(localParams.get(QueryParsing.V));
      Iterator<T> it = js.iterator(this);
      if (joinField == null) {
        throw new Exception("No XJoin component referenced by query");
      }
      FieldType ft = req.getSchema().getFieldTypeNoEx(joinField);
      Iterator<BytesRef> bytesRefs = new TransformIterator(it, transformer(ft));
      if (! bytesRefs.hasNext()) {
        return new BooleanQuery.Builder().build(); // matches nothing
      }
      Query query = method.makeQuery(joinField, bytesRefs);
      return new SolrConstantScoreQuery(new QueryWrapperFilter(query));
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public Iterator<T> iterator(String componentName) {
      XJoinSearchComponent xJoin = (XJoinSearchComponent)req.getCore().getSearchComponent(componentName);
      if (joinField == null) {
        joinField = xJoin.getJoinField();
      } else if (! xJoin.getJoinField().equals(joinField)) {
        throw new Exception("XJoin components used in the same query must have same join field");
      }
      XJoinResults<T> results = (XJoinResults<T>)req.getContext().get(xJoin.getResultsTag());
      if (results == null) {
        throw new Exception("No xjoin results in request context");
      }
      return results.getJoinIds().iterator();
    }
    
  }
  
  @SuppressWarnings("serial")
  static class Exception extends RuntimeException {
    public Exception(String message) {
      super(message);
    }
  }
  
}