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

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;

import com.google.common.base.MoreObjects;
import org.apache.lucene.search.Query;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.HighlightParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.core.SolrCore;
import org.apache.solr.highlight.DefaultSolrHighlighter;
import org.apache.solr.highlight.PostingsSolrHighlighter;
import org.apache.solr.highlight.SolrHighlighter;
import org.apache.solr.highlight.UnifiedSolrHighlighter;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.QParser;
import org.apache.solr.search.QParserPlugin;
import org.apache.solr.search.QueryParsing;
import org.apache.solr.search.SyntaxError;
import org.apache.solr.util.SolrPluginUtils;
import org.apache.solr.util.plugin.PluginInfoInitialized;
import org.apache.solr.util.plugin.SolrCoreAware;

import static java.util.stream.Collectors.toMap;

/**
 * TODO!
 *
 *
 * @since solr 1.3
 */
public class HighlightComponent extends SearchComponent implements PluginInfoInitialized, SolrCoreAware
{
  public enum HighlightMethod {
    UNIFIED("unified"),
    FAST_VECTOR("fastVector"),
    POSTINGS("postings"),
    ORIGINAL("original");

    private static final Map<String, HighlightMethod> METHODS = Collections.unmodifiableMap(Stream.of(values())
        .collect(toMap(HighlightMethod::getMethodName, Function.identity())));

    private final String methodName;

    HighlightMethod(String method) {
      this.methodName = method;
    }

    public String getMethodName() {
      return methodName;
    }

    public static HighlightMethod parse(String method) {
      return METHODS.get(method);
    }
  }

  public static final String COMPONENT_NAME = "highlight";

  private PluginInfo info = PluginInfo.EMPTY_INFO;

  @Deprecated // DWS: in 7.0 lets restructure the abstractions/relationships
  private SolrHighlighter solrConfigHighlighter;

  /**
   * @deprecated instead depend on {@link #process(ResponseBuilder)} to choose the highlighter based on
   * {@link HighlightParams#METHOD}
   */
  @Deprecated
  public static SolrHighlighter getHighlighter(SolrCore core) {
    HighlightComponent hl = (HighlightComponent) core.getSearchComponents().get(HighlightComponent.COMPONENT_NAME);
    return hl==null ? null: hl.getHighlighter();
  }

  @Deprecated
  public SolrHighlighter getHighlighter() {
    return solrConfigHighlighter;
  }

  @Override
  public void init(PluginInfo info) {
    this.info = info;
  }

  @Override
  public void prepare(ResponseBuilder rb) throws IOException {
    SolrParams params = rb.req.getParams();
    rb.doHighlights = solrConfigHighlighter.isHighlightingEnabled(params);
    if(rb.doHighlights){
      rb.setNeedDocList(true);
      String hlq = params.get(HighlightParams.Q);
      String hlparser = MoreObjects.firstNonNull(params.get(HighlightParams.QPARSER),
                                              params.get(QueryParsing.DEFTYPE, QParserPlugin.DEFAULT_QTYPE));
      if(hlq != null){
        try {
          QParser parser = QParser.getParser(hlq, hlparser, rb.req);
          rb.setHighlightQuery(parser.getHighlightQuery());
        } catch (SyntaxError e) {
          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
        }
      }
    }
  }

  @Override
  public void inform(SolrCore core) {
    List<PluginInfo> children = info.getChildren("highlighting");
    if(children.isEmpty()) {
      DefaultSolrHighlighter defHighlighter = new DefaultSolrHighlighter(core);
      defHighlighter.init(PluginInfo.EMPTY_INFO);
      solrConfigHighlighter = defHighlighter;
    } else {
      solrConfigHighlighter = core.createInitInstance(children.get(0),SolrHighlighter.class,null, DefaultSolrHighlighter.class.getName());
    }

  }

  @Override
  public void process(ResponseBuilder rb) throws IOException {

    if (rb.doHighlights) {
      SolrQueryRequest req = rb.req;
      SolrParams params = req.getParams();

      SolrHighlighter highlighter = getHighlighter(params);

      //TODO: get from builder by default?
      String[] defaultHighlightFields = rb.getQparser() != null ? rb.getQparser().getDefaultHighlightFields() : null;
      
      Query highlightQuery = rb.getHighlightQuery();
      if(highlightQuery==null) {
        if (rb.getQparser() != null) {
          try {
            highlightQuery = rb.getQparser().getHighlightQuery();
            rb.setHighlightQuery( highlightQuery );
          } catch (Exception e) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
          }
        } else {
          highlightQuery = rb.getQuery();
          rb.setHighlightQuery( highlightQuery );
        }
      }

      // No highlighting if there is no query -- consider q.alt=*:*
      if( highlightQuery != null ) {
        @SuppressWarnings({"rawtypes"})
        NamedList sumData = highlighter.doHighlighting(
                rb.getResults().docList,
                highlightQuery,
                req, defaultHighlightFields );
        
        if(sumData != null) {
          // TODO ???? add this directly to the response?
          rb.rsp.add(highlightingResponseField(), convertHighlights(sumData));
        }
      }
    }
  }

  protected SolrHighlighter getHighlighter(SolrParams params) {
    HighlightMethod method = HighlightMethod.parse(params.get(HighlightParams.METHOD));
    if (method == null) {
      return solrConfigHighlighter;
    }

    switch (method) {
      case UNIFIED:
        if (solrConfigHighlighter instanceof UnifiedSolrHighlighter) {
          return solrConfigHighlighter;
        }
        return new UnifiedSolrHighlighter(); // TODO cache one?
      case POSTINGS:
        if (solrConfigHighlighter instanceof PostingsSolrHighlighter) {
          return solrConfigHighlighter;
        }
        return new PostingsSolrHighlighter(); // TODO cache one?
      case FAST_VECTOR: // fall-through
      case ORIGINAL:
        if (solrConfigHighlighter instanceof DefaultSolrHighlighter) {
          return solrConfigHighlighter;
        } else {
          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
              "In order to use " + HighlightParams.METHOD + "=" + method.getMethodName() + " the configured" +
                  " highlighter in solrconfig must be " + DefaultSolrHighlighter.class);
        }
      default: throw new AssertionError();
    }
  }

  @Override
  public void modifyRequest(ResponseBuilder rb, SearchComponent who, ShardRequest sreq) {
    if (!rb.doHighlights) return;

    // Turn on highlighting only only when retrieving fields
    if ((sreq.purpose & ShardRequest.PURPOSE_GET_FIELDS) != 0) {
        sreq.purpose |= ShardRequest.PURPOSE_GET_HIGHLIGHTS;
        // should already be true...
        sreq.params.set(HighlightParams.HIGHLIGHT, "true");      
    } else {
      sreq.params.set(HighlightParams.HIGHLIGHT, "false");      
    }
  }

  @Override
  public void handleResponses(ResponseBuilder rb, ShardRequest sreq) {
  }

  @Override
  public void finishStage(ResponseBuilder rb) {
    if (rb.doHighlights && rb.stage == ResponseBuilder.STAGE_GET_FIELDS) {

      final Object[] objArr = newHighlightsArray(rb.resultIds.size());
      final String highlightingResponseField = highlightingResponseField();

      // TODO: make a generic routine to do automatic merging of id keyed data
      for (ShardRequest sreq : rb.finished) {
        if ((sreq.purpose & ShardRequest.PURPOSE_GET_HIGHLIGHTS) == 0) continue;
        for (ShardResponse srsp : sreq.responses) {
          if (srsp.getException() != null) {
            // can't expect the highlight content if there was an exception for this request
            // this should only happen when using shards.tolerant=true
            continue;
          }
          Object hl = srsp.getSolrResponse().getResponse().get(highlightingResponseField);
          addHighlights(objArr, hl, rb.resultIds);
        }
      }

      rb.rsp.add(highlightingResponseField, getAllHighlights(objArr));
    }
  }

  ////////////////////////////////////////////
  ///  SolrInfoBean
  ////////////////////////////////////////////
  
  @Override
  public String getDescription() {
    return "Highlighting";
  }

  @Override
  public Category getCategory() {
    return Category.HIGHLIGHTER;
  }

  ////////////////////////////////////////////
  ///  highlighting response collation
  ////////////////////////////////////////////

  protected String highlightingResponseField() {
    return "highlighting";
  }

  protected Object convertHighlights(@SuppressWarnings({"rawtypes"})NamedList hl) {
    return hl;
  }

  @SuppressWarnings({"rawtypes"})
  protected Object[] newHighlightsArray(int size) {
    return new NamedList.NamedListEntry[size];
  }

  protected void addHighlights(Object[] objArr, Object obj, Map<Object, ShardDoc> resultIds) {
    @SuppressWarnings({"unchecked"})
    Map.Entry<String, Object>[] arr = (Map.Entry<String, Object>[])objArr;
    @SuppressWarnings({"rawtypes"})
    NamedList hl = (NamedList)obj;
    SolrPluginUtils.copyNamedListIntoArrayByDocPosInResponse(hl, resultIds, arr);
  }

  protected Object getAllHighlights(Object[] objArr) {
    @SuppressWarnings({"unchecked"})
    final Map.Entry<String, Object>[] arr = (Map.Entry<String, Object>[])objArr;
      // remove nulls in case not all docs were able to be retrieved
      return SolrPluginUtils.removeNulls(arr, new SimpleOrderedMap<>());
  }

}