/*
 * Copyright 2005 Google Inc.
 *
 * Licensed 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 com.google.common.geometry;

import com.google.common.base.Preconditions;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;

/**
 * An S2RegionCoverer is a class that allows arbitrary regions to be
 * approximated as unions of cells (S2CellUnion). This is useful for
 * implementing various sorts of search and precomputation operations.
 *
 * Typical usage: {@code S2RegionCoverer coverer; coverer.setMaxCells(5); S2Cap
 * cap = S2Cap.fromAxisAngle(...); S2CellUnion covering;
 * coverer.getCovering(cap, covering); * }
 *
 * This yields a cell union of at most 5 cells that is guaranteed to cover the
 * given cap (a disc-shaped region on the sphere).
 *
 *  The approximation algorithm is not optimal but does a pretty good job in
 * practice. The output does not always use the maximum number of cells allowed,
 * both because this would not always yield a better approximation, and because
 * max_cells() is a limit on how much work is done exploring the possible
 * covering as well as a limit on the final output size.
 *
 *  One can also generate interior coverings, which are sets of cells which are
 * entirely contained within a region. Interior coverings can be empty, even for
 * non-empty regions, if there are no cells that satisfy the provided
 * constraints and are contained by the region. Note that for performance
 * reasons, it is wise to specify a max_level when computing interior coverings
 * - otherwise for regions with small or zero area, the algorithm may spend a
 * lot of time subdividing cells all the way to leaf level to try to find
 * contained cells.
 *
 *  This class is thread-unsafe. Simultaneous calls to any of the getCovering
 * methods will conflict and produce unpredictable results.
 *
 */
public final strictfp class S2RegionCoverer {

  /**
   * By default, the covering uses at most 8 cells at any level. This gives a
   * reasonable tradeoff between the number of cells used and the accuracy of
   * the approximation (see table below).
   */
  public static final int DEFAULT_MAX_CELLS = 8;

  private static final S2Cell[] FACE_CELLS = new S2Cell[6];
  static {
    for (int face = 0; face < 6; ++face) {
      FACE_CELLS[face] = S2Cell.fromFacePosLevel(face, (byte) 0, 0);
    }
  }


  private int minLevel;
  private int maxLevel;
  private int levelMod;
  private int maxCells;

  // True if we're computing an interior covering.
  private boolean interiorCovering;

  // Counter of number of candidates created, for performance evaluation.
  private int candidatesCreatedCounter;

  /**
   * We save a temporary copy of the pointer passed to GetCovering() in order to
   * avoid passing this parameter around internally. It is only used (and only
   * valid) for the duration of a single GetCovering() call.
   */
  S2Region region;

  /**
   * A temporary variable used by GetCovering() that holds the cell ids that
   * have been added to the covering so far.
   */
  ArrayList<S2CellId> result;


  static class Candidate {
    private S2Cell cell;
    private boolean isTerminal; // Cell should not be expanded further.
    private int numChildren; // Number of children that intersect the region.
    private Candidate[] children; // Actual size may be 0, 4, 16, or 64
    // elements.
  }

  static class QueueEntry {
    private int id;
    private Candidate candidate;

    public QueueEntry(int id, Candidate candidate) {
      this.id = id;
      this.candidate = candidate;
    }
  }

  /**
   * We define our own comparison function on QueueEntries in order to make the
   * results deterministic. Using the default less<QueueEntry>, entries of equal
   * priority would be sorted according to the memory address of the candidate.
   */
  static class QueueEntriesComparator implements Comparator<QueueEntry> {
    @Override
    public int compare(S2RegionCoverer.QueueEntry x, S2RegionCoverer.QueueEntry y) {
      return x.id < y.id ? 1 : (x.id > y.id ? -1 : 0);
    }
  }


  /**
   * We keep the candidates in a priority queue. We specify a vector to hold the
   * queue entries since for some reason priority_queue<> uses a deque by
   * default.
   */
  private PriorityQueue<QueueEntry> candidateQueue;

  /**
   * Default constructor, sets all fields to default values.
   */
  public S2RegionCoverer() {
    minLevel = 0;
    maxLevel = S2CellId.MAX_LEVEL;
    levelMod = 1;
    maxCells = DEFAULT_MAX_CELLS;
    this.region = null;
    result = new ArrayList<S2CellId>();
    // TODO(kirilll?): 10 is a completely random number, work out a better
    // estimate
    candidateQueue = new PriorityQueue<QueueEntry>(10, new QueueEntriesComparator());
  }

  // Set the minimum and maximum cell level to be used. The default is to use
  // all cell levels. Requires: max_level() >= min_level().
  //
  // To find the cell level corresponding to a given physical distance, use
  // the S2Cell metrics defined in s2.h. For example, to find the cell
  // level that corresponds to an average edge length of 10km, use:
  //
  // int level = S2::kAvgEdge.GetClosestLevel(
  // geostore::S2Earth::KmToRadians(length_km));
  //
  // Note: min_level() takes priority over max_cells(), i.e. cells below the
  // given level will never be used even if this causes a large number of
  // cells to be returned.

  /**
   * Sets the minimum level to be used.
   */
  public void setMinLevel(int minLevel) {
    // assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL);
    this.minLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, minLevel));
  }

  /**
   * Sets the maximum level to be used.
   */
  public void setMaxLevel(int maxLevel) {
    // assert (maxLevel >= 0 && maxLevel <= S2CellId.MAX_LEVEL);
    this.maxLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, maxLevel));
  }

  public int minLevel() {
    return minLevel;
  }

  public int maxLevel() {
    return maxLevel;
  }

  public int maxCells() {
    return maxCells;
  }

  /**
   * If specified, then only cells where (level - min_level) is a multiple of
   * "level_mod" will be used (default 1). This effectively allows the branching
   * factor of the S2CellId hierarchy to be increased. Currently the only
   * parameter values allowed are 1, 2, or 3, corresponding to branching factors
   * of 4, 16, and 64 respectively.
   */
  public void setLevelMod(int levelMod) {
    // assert (levelMod >= 1 && levelMod <= 3);
    this.levelMod = Math.max(1, Math.min(3, levelMod));
  }

  public int levelMod() {
    return levelMod;
  }


  /**
   * Sets the maximum desired number of cells in the approximation (defaults to
   * kDefaultMaxCells). Note the following:
   *
   * <ul>
   * <li>For any setting of max_cells(), up to 6 cells may be returned if that
   * is the minimum number of cells required (e.g. if the region intersects all
   * six face cells). Up to 3 cells may be returned even for very tiny convex
   * regions if they happen to be located at the intersection of three cube
   * faces.
   *
   * <li>For any setting of max_cells(), an arbitrary number of cells may be
   * returned if min_level() is too high for the region being approximated.
   *
   * <li>If max_cells() is less than 4, the area of the covering may be
   * arbitrarily large compared to the area of the original region even if the
   * region is convex (e.g. an S2Cap or S2LatLngRect).
   * </ul>
   *
   * Accuracy is measured by dividing the area of the covering by the area of
   * the original region. The following table shows the median and worst case
   * values for this area ratio on a test case consisting of 100,000 spherical
   * caps of random size (generated using s2regioncoverer_unittest):
   *
   * <pre>
   * max_cells: 3 4 5 6 8 12 20 100 1000
   * median ratio: 5.33 3.32 2.73 2.34 1.98 1.66 1.42 1.11 1.01
   * worst case: 215518 14.41 9.72 5.26 3.91 2.75 1.92 1.20 1.02
   * </pre>
   */
  public void setMaxCells(int maxCells) {
    this.maxCells = maxCells;
  }

  /**
   * Computes a list of cell ids that covers the given region and satisfies the
   * various restrictions specified above.
   *
   * @param region The region to cover
   * @param covering The list filled in by this method
   */
  public void getCovering(S2Region region, ArrayList<S2CellId> covering) {
    // Rather than just returning the raw list of cell ids generated by
    // GetCoveringInternal(), we construct a cell union and then denormalize it.
    // This has the effect of replacing four child cells with their parent
    // whenever this does not violate the covering parameters specified
    // (min_level, level_mod, etc). This strategy significantly reduces the
    // number of cells returned in many cases, and it is cheap compared to
    // computing the covering in the first place.

    S2CellUnion tmp = getCovering(region);
    tmp.denormalize(minLevel(), levelMod(), covering);
  }

  /**
   * Computes a list of cell ids that is contained within the given region and
   * satisfies the various restrictions specified above.
   *
   * @param region The region to fill
   * @param interior The list filled in by this method
   */
  public void getInteriorCovering(S2Region region, ArrayList<S2CellId> interior) {
    S2CellUnion tmp = getInteriorCovering(region);
    tmp.denormalize(minLevel(), levelMod(), interior);
  }

  /**
   * Return a normalized cell union that covers the given region and satisfies
   * the restrictions *EXCEPT* for min_level() and level_mod(). These criteria
   * cannot be satisfied using a cell union because cell unions are
   * automatically normalized by replacing four child cells with their parent
   * whenever possible. (Note that the list of cell ids passed to the cell union
   * constructor does in fact satisfy all the given restrictions.)
   */
  public S2CellUnion getCovering(S2Region region) {
    S2CellUnion covering = new S2CellUnion();
    getCovering(region, covering);
    return covering;
  }

  public void getCovering(S2Region region, S2CellUnion covering) {
    interiorCovering = false;
    getCoveringInternal(region);
    covering.initSwap(result);
  }

  /**
   * Return a normalized cell union that is contained within the given region
   * and satisfies the restrictions *EXCEPT* for min_level() and level_mod().
   */
  public S2CellUnion getInteriorCovering(S2Region region) {
    S2CellUnion covering = new S2CellUnion();
    getInteriorCovering(region, covering);
    return covering;
  }

  public void getInteriorCovering(S2Region region, S2CellUnion covering) {
    interiorCovering = true;
    getCoveringInternal(region);
    covering.initSwap(result);
  }

  /**
   * Given a connected region and a starting point, return a set of cells at the
   * given level that cover the region.
   */
  public static void getSimpleCovering(
      S2Region region, S2Point start, int level, ArrayList<S2CellId> output) {
    floodFill(region, S2CellId.fromPoint(start).parent(level), output);
  }

  /**
   * If the cell intersects the given region, return a new candidate with no
   * children, otherwise return null. Also marks the candidate as "terminal" if
   * it should not be expanded further.
   */
  private Candidate newCandidate(S2Cell cell) {
    if (!region.mayIntersect(cell)) {
      return null;
    }

    boolean isTerminal = false;
    if (cell.level() >= minLevel) {
      if (interiorCovering) {
        if (region.contains(cell)) {
          isTerminal = true;
        } else if (cell.level() + levelMod > maxLevel) {
          return null;
        }
      } else {
        if (cell.level() + levelMod > maxLevel || region.contains(cell)) {
          isTerminal = true;
        }
      }
    }
    Candidate candidate = new Candidate();
    candidate.cell = cell;
    candidate.isTerminal = isTerminal;
    if (!isTerminal) {
      candidate.children = new Candidate[1 << maxChildrenShift()];
    }
    candidatesCreatedCounter++;
    return candidate;
  }

  /** Return the log base 2 of the maximum number of children of a candidate. */
  private int maxChildrenShift() {
    return 2 * levelMod;
  }

  /**
   * Process a candidate by either adding it to the result list or expanding its
   * children and inserting it into the priority queue. Passing an argument of
   * NULL does nothing.
   */
  private void addCandidate(Candidate candidate) {
    if (candidate == null) {
      return;
    }

    if (candidate.isTerminal) {
      result.add(candidate.cell.id());
      return;
    }
    // assert (candidate.numChildren == 0);

    // Expand one level at a time until we hit min_level_ to ensure that
    // we don't skip over it.
    int numLevels = (candidate.cell.level() < minLevel) ? 1 : levelMod;
    int numTerminals = expandChildren(candidate, candidate.cell, numLevels);

    if (candidate.numChildren == 0) {
      // Do nothing
    } else if (!interiorCovering && numTerminals == 1 << maxChildrenShift()
        && candidate.cell.level() >= minLevel) {
      // Optimization: add the parent cell rather than all of its children.
      // We can't do this for interior coverings, since the children just
      // intersect the region, but may not be contained by it - we need to
      // subdivide them further.
      candidate.isTerminal = true;
      addCandidate(candidate);

    } else {
      // We negate the priority so that smaller absolute priorities are returned
      // first. The heuristic is designed to refine the largest cells first,
      // since those are where we have the largest potential gain. Among cells
      // at the same level, we prefer the cells with the smallest number of
      // intersecting children. Finally, we prefer cells that have the smallest
      // number of children that cannot be refined any further.
      int priority = -((((candidate.cell.level() << maxChildrenShift()) + candidate.numChildren)
          << maxChildrenShift()) + numTerminals);
      candidateQueue.add(new QueueEntry(priority, candidate));
      // logger.info("Push: " + candidate.cell.id() + " (" + priority + ") ");
    }
  }

  /**
   * Populate the children of "candidate" by expanding the given number of
   * levels from the given cell. Returns the number of children that were marked
   * "terminal".
   */
  private int expandChildren(Candidate candidate, S2Cell cell, int numLevels) {
    numLevels--;
    S2Cell[] childCells = new S2Cell[4];
    for (int i = 0; i < 4; ++i) {
      childCells[i] = new S2Cell();
    }
    cell.subdivide(childCells);
    int numTerminals = 0;
    for (int i = 0; i < 4; ++i) {
      if (numLevels > 0) {
        if (region.mayIntersect(childCells[i])) {
          numTerminals += expandChildren(candidate, childCells[i], numLevels);
        }
        continue;
      }
      Candidate child = newCandidate(childCells[i]);
      if (child != null) {
        candidate.children[candidate.numChildren++] = child;
        if (child.isTerminal) {
          ++numTerminals;
        }
      }
    }
    return numTerminals;
  }

  /** Computes a set of initial candidates that cover the given region. */
  private void getInitialCandidates() {
    // Optimization: if at least 4 cells are desired (the normal case),
    // start with a 4-cell covering of the region's bounding cap. This
    // lets us skip quite a few levels of refinement when the region to
    // be covered is relatively small.
    if (maxCells >= 4) {
      // Find the maximum level such that the bounding cap contains at most one
      // cell vertex at that level.
      S2Cap cap = region.getCapBound();
      int level = Math.min(S2Projections.MIN_WIDTH.getMaxLevel(2 * cap.angle().radians()),
          Math.min(maxLevel(), S2CellId.MAX_LEVEL - 1));
      if (levelMod() > 1 && level > minLevel()) {
        level -= (level - minLevel()) % levelMod();
      }
      // We don't bother trying to optimize the level == 0 case, since more than
      // four face cells may be required.
      if (level > 0) {
        // Find the leaf cell containing the cap axis, and determine which
        // subcell of the parent cell contains it.
        ArrayList<S2CellId> base = new ArrayList<S2CellId>(4);
        S2CellId id = S2CellId.fromPoint(cap.axis());
        id.getVertexNeighbors(level, base);
        for (int i = 0; i < base.size(); ++i) {
          addCandidate(newCandidate(new S2Cell(base.get(i))));
        }
        return;
      }
    }
    // Default: start with all six cube faces.
    for (int face = 0; face < 6; ++face) {
      addCandidate(newCandidate(FACE_CELLS[face]));
    }
  }

  /** Generates a covering and stores it in result. */
  private void getCoveringInternal(S2Region region) {
    // Strategy: Start with the 6 faces of the cube. Discard any
    // that do not intersect the shape. Then repeatedly choose the
    // largest cell that intersects the shape and subdivide it.
    //
    // result contains the cells that will be part of the output, while the
    // priority queue contains cells that we may still subdivide further. Cells
    // that are entirely contained within the region are immediately added to
    // the output, while cells that do not intersect the region are immediately
    // discarded.
    // Therefore pq_ only contains cells that partially intersect the region.
    // Candidates are prioritized first according to cell size (larger cells
    // first), then by the number of intersecting children they have (fewest
    // children first), and then by the number of fully contained children
    // (fewest children first).

    Preconditions.checkState(candidateQueue.isEmpty() && result.isEmpty());

    this.region = region;
    candidatesCreatedCounter = 0;

    getInitialCandidates();
    while (!candidateQueue.isEmpty() && (!interiorCovering || result.size() < maxCells)) {
      Candidate candidate = candidateQueue.poll().candidate;
      // logger.info("Pop: " + candidate.cell.id());
      if (candidate.cell.level() < minLevel || candidate.numChildren == 1
          || result.size() + (interiorCovering ? 0 : candidateQueue.size()) + candidate.numChildren
              <= maxCells) {
        // Expand this candidate into its children.
        for (int i = 0; i < candidate.numChildren; ++i) {
          addCandidate(candidate.children[i]);
        }
      } else if (interiorCovering) {
        // Do nothing
      } else {
        candidate.isTerminal = true;
        addCandidate(candidate);
      }
    }

    candidateQueue.clear();
    this.region = null;
  }

  /**
   * Given a region and a starting cell, return the set of all the
   * edge-connected cells at the same level that intersect "region". The output
   * cells are returned in arbitrary order.
   */
  private static void floodFill(S2Region region, S2CellId start, ArrayList<S2CellId> output) {
    HashSet<S2CellId> all = new HashSet<S2CellId>();
    ArrayList<S2CellId> frontier = new ArrayList<S2CellId>();
    output.clear();
    all.add(start);
    frontier.add(start);
    while (!frontier.isEmpty()) {
      S2CellId id = frontier.get(frontier.size() - 1);
      frontier.remove(frontier.size() - 1);
      if (!region.mayIntersect(new S2Cell(id))) {
        continue;
      }
      output.add(id);

      S2CellId[] neighbors = new S2CellId[4];
      id.getEdgeNeighbors(neighbors);
      for (int edge = 0; edge < 4; ++edge) {
        S2CellId nbr = neighbors[edge];
        boolean hasNbr = all.contains(nbr);
        if (!all.contains(nbr)) {
          frontier.add(nbr);
          all.add(nbr);
        }
      }
    }
  }
}
