/*
 * 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.lucene.spatial.prefix;

import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.spatial.prefix.tree.Cell;
import org.apache.lucene.spatial.prefix.tree.CellIterator;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;

/**
 * A basic implementation of {@link PrefixTreeStrategy} using a large {@link TermInSetQuery} of all
 * the cells from {@link
 * SpatialPrefixTree#getTreeCellIterator(org.locationtech.spatial4j.shape.Shape, int)}. It only
 * supports the search of indexed Point shapes.
 *
 * <p>The precision of query shapes (distErrPct) is an important factor in using this Strategy. If
 * the precision is too precise then it will result in many terms which will amount to a slower
 * query.
 *
 * @lucene.experimental
 */
public class TermQueryPrefixTreeStrategy extends PrefixTreeStrategy {

  protected boolean simplifyIndexedCells = false;

  public TermQueryPrefixTreeStrategy(SpatialPrefixTree grid, String fieldName) {
    super(grid, fieldName);
  }

  @Override
  protected CellToBytesRefIterator newCellToBytesRefIterator() {
    // Ensure we don't have leaves, as this strategy doesn't handle them.
    return new CellToBytesRefIterator() {
      @Override
      public BytesRef next() {
        if (!cellIter.hasNext()) {
          return null;
        }
        return cellIter.next().getTokenBytesNoLeaf(bytesRef);
      }
    };
  }

  @Override
  public Query makeQuery(SpatialArgs args) {
    final SpatialOperation op = args.getOperation();
    if (op != SpatialOperation.Intersects) throw new UnsupportedSpatialOperation(op);

    Shape shape = args.getShape();
    int detailLevel = grid.getLevelForDistance(args.resolveDistErr(ctx, distErrPct));

    // --get a List of BytesRef for each term we want (no parents, no leaf bytes))
    final int GUESS_NUM_TERMS;
    if (shape instanceof Point) {
      GUESS_NUM_TERMS = detailLevel; // perfect guess
    } else {
      GUESS_NUM_TERMS = 4096; // should this be a method on SpatialPrefixTree?
    }

    BytesRefBuilder masterBytes = new BytesRefBuilder(); // shared byte array for all terms
    List<BytesRef> terms = new ArrayList<>(GUESS_NUM_TERMS);

    CellIterator cells = grid.getTreeCellIterator(shape, detailLevel);
    while (cells.hasNext()) {
      Cell cell = cells.next();
      if (!cell.isLeaf()) continue;
      BytesRef term = cell.getTokenBytesNoLeaf(null); // null because we want a new BytesRef
      // We copy out the bytes because it may be re-used across the iteration. This also gives us
      // the opportunity
      // to use one contiguous block of memory for the bytes of all terms we need.
      masterBytes.grow(masterBytes.length() + term.length);
      masterBytes.append(term);
      term.bytes = null; // don't need; will reset later
      term.offset = masterBytes.length() - term.length;
      terms.add(term);
    }
    // doing this now because if we did earlier, it's possible the bytes needed to grow()
    for (BytesRef byteRef : terms) {
      byteRef.bytes = masterBytes.bytes();
    }
    // unfortunately TermsQuery will needlessly sort & dedupe
    // TODO an automatonQuery might be faster?
    return new TermInSetQuery(getFieldName(), terms);
  }
}
