/*
 * 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.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.util.PivotListEntry;

public class PivotFacetHelper {

  /**
   * Encodes a value path as a string for the purposes of a refinement request
   *
   * @see PivotFacetValue#getValuePath
   * @see #decodeRefinementValuePath
   */
  public static String encodeRefinementValuePath(List<String> values) {
    // HACK: prefix flag every value to account for empty string vs null
    // NOTE: even if we didn't have to worry about null's smartSplit is stupid about
    // pruning empty strings from list
    // "^" prefix = null
    // "~" prefix = not null, may be empty string

    assert null != values;

    // special case: empty list => empty string
    if (values.isEmpty()) {
      return "";
    }
    
    StringBuilder out = new StringBuilder();
    for (String val : values) {
      if (null == val) {
        out.append('^');
      } else {
        out.append('~');
        StrUtils.appendEscapedTextToBuilder(out, val, ',');
      }
      out.append(',');
    }
    out.deleteCharAt(out.length()-1);  // prune the last separator
    return out.toString();
    // return StrUtils.join(values, ',');
  }

  /**
   * Decodes a value path string specified for refinement.
   *
   * @see #encodeRefinementValuePath
   */
  public static List<String> decodeRefinementValuePath(String valuePath) {
    List<String> rawvals = StrUtils.splitSmart(valuePath, ",", true);
    // special case: empty list => empty string
    if (rawvals.isEmpty()) return rawvals;

    List<String> out = new ArrayList<>(rawvals.size());
    for (String raw : rawvals) {
      assert 0 < raw.length();
      if ('^' == raw.charAt(0)) {
        assert 1 == raw.length();
        out.add(null);
      } else {
        assert '~' == raw.charAt(0);
        out.add(raw.substring(1));
      }
    }

    return out;
  }

  /** @see PivotListEntry#VALUE */
  public static Comparable getValue(NamedList<Object> pivotList) {
    return (Comparable) PivotListEntry.VALUE.extract(pivotList);
  }

  /** @see PivotListEntry#FIELD */
  public static String getField(NamedList<Object> pivotList) {
    return (String) PivotListEntry.FIELD.extract(pivotList);
  }
  
  /** @see PivotListEntry#COUNT */
  public static Integer getCount(NamedList<Object> pivotList) {
    return (Integer) PivotListEntry.COUNT.extract(pivotList);
  }

  /** @see PivotListEntry#PIVOT */
  public static List<NamedList<Object>> getPivots(NamedList<Object> pivotList) {
    return (List<NamedList<Object>>) PivotListEntry.PIVOT.extract(pivotList);
  }
  
  /** @see PivotListEntry#STATS */
  public static NamedList<NamedList<NamedList<?>>> getStats(NamedList<Object> pivotList) {
    return (NamedList<NamedList<NamedList<?>>>) PivotListEntry.STATS.extract(pivotList);
  }

  /** @see PivotListEntry#QUERIES */
  public static NamedList<Number> getQueryCounts(NamedList<Object> pivotList) {
    return (NamedList<Number>) PivotListEntry.QUERIES.extract(pivotList);
  }
  
  /** @see PivotListEntry#RANGES */
  public static SimpleOrderedMap<SimpleOrderedMap<Object>> getRanges(NamedList<Object> pivotList) {
    return (SimpleOrderedMap<SimpleOrderedMap<Object>>) PivotListEntry.RANGES.extract(pivotList);
  }
  
  /**
   * Given a mapping of keys to {@link StatsValues} representing the currently 
   * known "merged" stats (which may be null if none exist yet), and a 
   * {@link NamedList} containing the "stats" response block returned by an individual 
   * shard, this method accumulates the stats for each {@link StatsField} found in
   * the shard response with the existing mergeStats
   *
   * @return the original <code>merged</code> Map after modifying, or a new Map if the <code>merged</code> param was originally null.
   * @see StatsInfo#getStatsField
   * @see StatsValuesFactory#createStatsValues
   * @see StatsValues#accumulate(NamedList)
   */
  public static Map<String,StatsValues> mergeStats
    (Map<String,StatsValues> merged, 
     NamedList<NamedList<NamedList<?>>> remoteWrapper, 
     StatsInfo statsInfo) {

    if (null == merged) merged = new LinkedHashMap<>();

    NamedList<NamedList<?>> remoteStats = StatsComponent.unwrapStats(remoteWrapper);

    for (Entry<String,NamedList<?>> entry : remoteStats) {
      StatsValues receivingStatsValues = merged.get(entry.getKey());
      if (receivingStatsValues == null) {
        StatsField receivingStatsField = statsInfo.getStatsField(entry.getKey());
        if (null == receivingStatsField) {
          throw new SolrException(ErrorCode.SERVER_ERROR , "No stats.field found corresponding to pivot stats received from shard: "+entry.getKey());
        }
        receivingStatsValues = StatsValuesFactory.createStatsValues(receivingStatsField);
        merged.put(entry.getKey(), receivingStatsValues);
      }
      receivingStatsValues.accumulate(entry.getValue());
    }
    return merged;
  }

  /**
   * Merges query counts returned by a shard into global query counts.
   * Entries found only in shard's query counts will be added to global counts.
   * Entries found in both shard and global query counts will be summed.
   *
   * @param globalQueryCounts The global query counts (across all shards) in which to merge the shard query counts
   * @param shardQueryCounts  Named list from a shard response to be merged into the global counts.
   * @return NamedList containing merged values
   */
  static NamedList<Number> mergeQueryCounts(
      NamedList<Number> globalQueryCounts, NamedList<Number> shardQueryCounts) {
    if (globalQueryCounts == null) {
      return shardQueryCounts;
    }
    for (Entry<String, Number> entry : shardQueryCounts) {
      int idx = globalQueryCounts.indexOf(entry.getKey(), 0);
      if (idx == -1) {
        globalQueryCounts.add(entry.getKey(), entry.getValue());
      } else {
        globalQueryCounts.setVal(idx, FacetComponent.num(globalQueryCounts.getVal(idx).longValue() + entry.getValue().longValue()));
      }
    }
    return globalQueryCounts;
  }
}
