/*
 * 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.
 */
package org.apache.solr.search.join;

import java.io.IOException;
import java.util.Objects;

import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.join.JoinUtil;
import org.apache.lucene.search.join.ScoreMode;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.search.JoinQParserPlugin;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.uninverting.UninvertingReader;
import org.apache.solr.util.RefCounted;

/**
 * Create a query-time join query with scoring. 
 * It just calls  {@link JoinUtil#createJoinQuery(String, boolean, String, Query, org.apache.lucene.search.IndexSearcher, ScoreMode)}.
 * It runs subordinate query and collects values of "from"  field and scores, then it lookups these collected values in "to" field, and
 * yields aggregated scores.
 * Local parameters are similar to {@link JoinQParserPlugin} <a href="http://wiki.apache.org/solr/Join">{!join}</a>
 * This plugin doesn't have own name, and is called by specifying local parameter <code>{!join score=...}...</code>. 
 * Note: this parser is invoked even if you specify <code>score=none</code>.
 * <br>Example:<code>q={!join from=manu_id_s to=id score=total}foo</code>
 * <ul>
 *  <li>from - "foreign key" field name to collect values while enumerating subordinate query (denoted as <code>foo</code> in example above).
 *             it's better to have this field declared as <code>type="string" docValues="true"</code>.
 *             note: if <a href="http://wiki.apache.org/solr/DocValues">docValues</a> are not enabled for this field, it will work anyway, 
 *             but it costs some memory for {@link UninvertingReader}. 
 *             Also, numeric doc values are not supported until <a href="https://issues.apache.org/jira/browse/LUCENE-5868">LUCENE-5868</a>.
 *             Thus, it only supports {@link DocValuesType#SORTED}, {@link DocValuesType#SORTED_SET}, {@link DocValuesType#BINARY}.  </li>
 *  <li>fromIndex - optional parameter, a core name where subordinate query should run (and <code>from</code> values are collected) rather than current core.
 *             <br>Example:<code>q={!join from=manu_id_s to=id score=total fromIndex=products}foo</code> 
 *  <li>to - "primary key" field name which is searched for values collected from subordinate query. 
 *             it should be declared as <code>indexed="true"</code>. Now it's treated as a single value field.</li>
 *  <li>score - one of {@link ScoreMode}: <code>none,avg,total,max,min</code>. Capital case is also accepted.</li>
 * </ul>
 */
public class ScoreJoinQParserPlugin extends QParserPlugin {

  public static final String SCORE = "score";

  static class OtherCoreJoinQuery extends SameCoreJoinQuery {
    private final String fromIndex;
    private final long fromCoreOpenTime;

    public OtherCoreJoinQuery(Query fromQuery, String fromField,
                              String fromIndex, long fromCoreOpenTime, ScoreMode scoreMode,
                              String toField) {
      super(fromQuery, fromField, toField, scoreMode);
      this.fromIndex = fromIndex;
      this.fromCoreOpenTime = fromCoreOpenTime;
    }

    @Override
    public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
      SolrRequestInfo info = SolrRequestInfo.getRequestInfo();

      CoreContainer container = info.getReq().getCore().getCoreContainer();

      final SolrCore fromCore = container.getCore(fromIndex);

      if (fromCore == null) {
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cross-core join: no such core " + fromIndex);
      }
      RefCounted<SolrIndexSearcher> fromHolder = null;
      fromHolder = fromCore.getRegisteredSearcher();
      final Query joinQuery;
      try {
        joinQuery = JoinUtil.createJoinQuery(fromField, true,
            toField, fromQuery, fromHolder.get(), this.scoreMode);
      } finally {
        fromCore.close();
        fromHolder.decref();
      }
      return joinQuery.rewrite(searcher.getIndexReader()).createWeight(searcher, scoreMode, boost);
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = super.hashCode();
      result = prime * result
          + (int) (fromCoreOpenTime ^ (fromCoreOpenTime >>> 32));
      result = prime * result
          + ((fromIndex == null) ? 0 : fromIndex.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (!super.equals(obj)) return false;
      if (getClass() != obj.getClass()) return false;
      OtherCoreJoinQuery other = (OtherCoreJoinQuery) obj;
      if (fromCoreOpenTime != other.fromCoreOpenTime) return false;
      if (fromIndex == null) {
        if (other.fromIndex != null) return false;
      } else if (!fromIndex.equals(other.fromIndex)) return false;
      return true;
    }

    @Override
    public String toString(String field) {
      return "OtherCoreJoinQuery [fromIndex=" + fromIndex
          + ", fromCoreOpenTime=" + fromCoreOpenTime + " extends "
          + super.toString(field) + "]";
    }
  }

  static class SameCoreJoinQuery extends Query {
    protected final Query fromQuery;
    protected final ScoreMode scoreMode;
    protected final String fromField;
    protected final String toField;

    SameCoreJoinQuery(Query fromQuery, String fromField, String toField,
                      ScoreMode scoreMode) {
      this.fromQuery = fromQuery;
      this.scoreMode = scoreMode;
      this.fromField = fromField;
      this.toField = toField;
    }

    @Override
    public Weight createWeight(IndexSearcher searcher, org.apache.lucene.search.ScoreMode scoreMode, float boost) throws IOException {
      SolrRequestInfo info = SolrRequestInfo.getRequestInfo();
      final Query jq = JoinUtil.createJoinQuery(fromField, true,
          toField, fromQuery, info.getReq().getSearcher(), this.scoreMode);
      return jq.rewrite(searcher.getIndexReader()).createWeight(searcher, scoreMode, boost);
    }


    @Override
    public String toString(String field) {
      return "SameCoreJoinQuery [fromQuery=" + fromQuery + ", fromField="
          + fromField + ", toField=" + toField + ", scoreMode=" + scoreMode
          + "]";
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = classHash();
      result = prime * result + Objects.hashCode(fromField);
      result = prime * result + Objects.hashCode(fromQuery);
      result = prime * result + Objects.hashCode(scoreMode);
      result = prime * result + Objects.hashCode(toField);
      return result;
    }

    @Override
    public boolean equals(Object other) {
      return sameClassAs(other) &&
             equalsTo(getClass().cast(other));
    }

    private boolean equalsTo(SameCoreJoinQuery other) {
      return Objects.equals(fromField, other.fromField) &&
             Objects.equals(fromQuery, other.fromQuery) &&
             Objects.equals(scoreMode, other.scoreMode) &&
             Objects.equals(toField, other.toField);
    }

    @Override
    public void visit(QueryVisitor visitor) {
      visitor.visitLeaf(this);
    }
  }


  @Override
  public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
    return new QParser(qstr, localParams, params, req) {
      @Override
      public Query parse() throws SyntaxError {
        final String fromField = localParams.get("from");
        final String fromIndex = localParams.get("fromIndex");
        final String toField = localParams.get("to");
        final ScoreMode scoreMode = ScoreModeParser.parse(getParam(SCORE));

        final String v = localParams.get(CommonParams.VALUE);

        final Query q = createQuery(fromField, v, fromIndex, toField, scoreMode,
            CommonParams.TRUE.equals(localParams.get("TESTenforceSameCoreAsAnotherOne")));

        return q;
      }

      private Query createQuery(final String fromField, final String fromQueryStr,
                                String fromIndex, final String toField, final ScoreMode scoreMode,
                                boolean byPassShortCircutCheck) throws SyntaxError {

        final String myCore = req.getCore().getCoreDescriptor().getName();

        if (fromIndex != null && (!fromIndex.equals(myCore) || byPassShortCircutCheck)) {
          CoreContainer container = req.getCore().getCoreContainer();

          final String coreName = getCoreName(fromIndex, container);
          final SolrCore fromCore = container.getCore(coreName);
          RefCounted<SolrIndexSearcher> fromHolder = null;

          if (fromCore == null) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cross-core join: no such core " + coreName);
          }

          long fromCoreOpenTime = 0;
          LocalSolrQueryRequest otherReq = new LocalSolrQueryRequest(fromCore, params);

          try {
            QParser fromQueryParser = QParser.getParser(fromQueryStr, otherReq);
            Query fromQuery = fromQueryParser.getQuery();

            fromHolder = fromCore.getRegisteredSearcher();
            if (fromHolder != null) {
              fromCoreOpenTime = fromHolder.get().getOpenNanoTime();
            }
            return new OtherCoreJoinQuery(fromQuery, fromField, coreName, fromCoreOpenTime,
                scoreMode, toField);
          } finally {
            otherReq.close();
            fromCore.close();
            if (fromHolder != null) fromHolder.decref();
          }
        } else {
          QParser fromQueryParser = subQuery(fromQueryStr, null);
          final Query fromQuery = fromQueryParser.getQuery();
          return new SameCoreJoinQuery(fromQuery, fromField, toField, scoreMode);
        }
      }
    };
  }

  /**
   * Returns an String with the name of a core.
   * <p>
   * This method searches the core with fromIndex name in the core's container.
   * If fromIndex isn't name of collection or alias it's returns fromIndex without changes.
   * If fromIndex is name of alias but if the alias points to multiple collections it's throw
   * SolrException.ErrorCode.BAD_REQUEST because multiple shards not yet supported.
   *
   * @param  fromIndex name of the index
   * @param  container the core container for searching the core with fromIndex name or alias
   * @return      the string with name of core
   */
  public static String getCoreName(final String fromIndex, CoreContainer container) {
    if (container.isZooKeeperAware()) {
      ZkController zkController = container.getZkController();
      final String resolved = resolveAlias(fromIndex, zkController);
      // TODO DWS: no need for this since later, clusterState.getCollection will throw a reasonable error
      if (!zkController.getClusterState().hasCollection(resolved)) {
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
            "SolrCloud join: Collection '" + fromIndex + "' not found!");
      }
      return findLocalReplicaForFromIndex(zkController, resolved);
    }
    return fromIndex;
  }

  private static String resolveAlias(String fromIndex, ZkController zkController) {
    final Aliases aliases = zkController.getZkStateReader().getAliases();
    try {
      return aliases.resolveSimpleAlias(fromIndex); // if not an alias, returns input
    } catch (IllegalArgumentException e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
          "SolrCloud join: Collection alias '" + fromIndex +
              "' maps to multiple collectiions, which is not currently supported for joins.", e);
    }
  }

  private static String findLocalReplicaForFromIndex(ZkController zkController, String fromIndex) {
    String fromReplica = null;

    String nodeName = zkController.getNodeName();
    for (Slice slice : zkController.getClusterState().getCollection(fromIndex).getActiveSlicesArr()) {
      if (fromReplica != null)
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
            "SolrCloud join: multiple shards not yet supported " + fromIndex);

      for (Replica replica : slice.getReplicas()) {
        if (replica.getNodeName().equals(nodeName)) {
          fromReplica = replica.getStr(ZkStateReader.CORE_NAME_PROP);
          // found local replica, but is it Active?
          if (replica.getState() != Replica.State.ACTIVE)
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
                "SolrCloud join: "+fromIndex+" has a local replica ("+fromReplica+
                    ") on "+nodeName+", but it is "+replica.getState());

          break;
        }
      }
    }

    if (fromReplica == null)
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
          "SolrCloud join: No active replicas for "+fromIndex+
              " found in node " + nodeName);

    return fromReplica;
  }
}



