package com.freetymekiyan.algorithms.level.hard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeMap;

/**
 * 218. The Skyline Problem
 * <p>
 * A city's skyline is the outer contour of the silhouette formed by all the buildings in that city when viewed from a
 * distance. Now suppose you are given the locations and height of all the buildings as shown on a cityscape photo
 * (Figure A), write a program to output the skyline formed by these buildings collectively (Figure B).
 * <p>
 * Figure A: https://leetcode.com/static/images/problemset/skyline1.jpg
 * Figure B: https://leetcode.com/static/images/problemset/skyline2.jpg
 * <p>
 * Buildings Skyline Contour
 * The geometric information of each building is represented by a triplet of integers [Li, Ri, Hi], where Li and Ri are
 * the x coordinates of the left and right edge of the ith building, respectively, and Hi is its height. It is
 * guaranteed that 0 ≤ Li, Ri ≤ INT_MAX, 0 < Hi ≤ INT_MAX, and Ri - Li > 0. You may assume all buildings are perfect
 * rectangles grounded on an absolutely flat surface at height 0.
 * <p>
 * For instance, the dimensions of all buildings in Figure A are recorded as: [ [2 9 10], [3 7 15], [5 12 12], [15 20
 * 10], [19 24 8] ] .
 * <p>
 * The output is a list of "key points" (red dots in Figure B) in the format of [ [x1,y1], [x2, y2], [x3, y3], ... ]
 * that uniquely defines a skyline. A key point is the left endpoint of a horizontal line segment. Note that the last
 * key point, where the rightmost building ends, is merely used to mark the termination of the skyline, and always has
 * zero height. Also, the ground in between any two adjacent buildings should be considered part of the skyline
 * contour.
 * <p>
 * For instance, the skyline in Figure B should be represented as:[ [2 10], [3 15], [7 12], [12 0], [15 10], [20 8],
 * [24, 0] ].
 * <p>
 * Notes:
 * <p>
 * The number of buildings in any input list is guaranteed to be in the range [0, 10000].
 * The input list is already sorted in ascending order by the left x position Li.
 * The output list must be sorted by the x position.
 * There must be no consecutive horizontal lines of equal height in the output skyline. For instance, [...[2 3], [4 5],
 * [7 5], [11 5], [12 7]...] is not acceptable; the three lines of height 5 should be merged into one in the final
 * output as such: [...[2 3], [4 5], [12 7], ...]
 * <p>
 * Company Tags: Microsoft, Google, Facebook, Twitter, Yelp
 * Tags: Binary Indexed Tree, Segment Tree, Heap, Divide and Conquer
 */
public class TheSkylineProblem {

  /**
   * My own implementation with max heap.
   * The candidates are sorted by x asc, y desc.
   * For each candidate point:
   * | If it's a left point, it's a start of a segment.
   * |   Add the height to heap.
   * |   If the height is higher than previous height, add it as key point and update previous height.
   * | If it's a right point, it's an end of a segment.
   * |   Remove the height from heap. Now the height may change.
   * |   If there is no segment, add (x, 0) to key points, update previous height as 0.
   * |   If there are more segments, add (x, highest) to key points, update previous height as highest.
   * The logic above can further simplify to: 1. Update heap. 2. If height changes, add key points and update height.
   * If height doesn't change, of course there is no new key points.
   * Return a list of key points.
   */
  public List<int[]> getSkyline(int[][] buildings) {
    List<int[]> corners = new ArrayList<>(buildings.length * 2);
    for (int[] b : buildings) {
      corners.add(new int[]{b[0], b[2]});
      corners.add(new int[]{b[1], -b[2]});
    }
    corners.sort((a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);

    PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder()); // Max heap.
    pq.offer(0); // Add 0 as minimum height so that we won't need to check whether heap is empty when we remove a height.
    List<int[]> keyPoints = new ArrayList<>();
    int height = 0;
    for (int[] c : corners) {
      if (c[1] > 0) { // Top-left. Add a segment height to heap.
        pq.offer(c[1]);
      } else { // Top-right. Remove a segment height from heap.
        pq.remove(-c[1]); // This is O(n). Can be faster with TreeMap.
      }
      if (pq.peek() != height) { // If the height changes, that means a key point.
        keyPoints.add(new int[]{c[0], pq.peek()});
        height = pq.peek();
      }
    }
    return keyPoints;
  }

  /**
   * Heap. Line Sweep Algorithm.
   * All possible key points are at the edges of rectangle.
   * Add all top-left and top-right corners to a list.
   * Set top-left's height to negative to indicate it's left and put them before right points.
   * Sort the points by x asc, y asc.
   * Create a tree map with 0,1 in it by default.
   * Track the height of previous added key point.
   * Then for each point p in the list:
   * | If p[1] < 0, left point:
   * |   Add the height count to tree map.
   * | If p[1] > 0, right point:
   * |   Reduce the height count to tree map. If count is 1, remove the height.
   * | Get the highest by map.firstKey().
   * | If the highest != previous height, a key point found:
   * |   Add it to result.
   * |   Update previous height to current height.
   * <p>
   * Tricks:
   * 1. Store left points height as negative, then they can be distinguished from right points.
   * 2. Put height 0 count 1 into tree map as a dummy rectangle. 0 is the max height when there is no rectangle.
   * <p>
   * https://briangordon.github.io/2014/08/the-skyline-problem.html
   * <p>
   * My comments:
   * The use of tree map makes me feel a bit unnecessary, since priority queue or heap can contain duplicates.
   * A priority queue or max heap is enough.
   * The only upside is that TreeMap get/put in O(logn). Priority queue removes a specific value in O(n).
   * Because it scans through all values to find the one to remove.
   */
  public List<int[]> getSkyline2(int[][] buildings) {
    // Build and sort critical points.
    List<int[]> corners = new ArrayList<>();
    for (int[] b : buildings) {
      corners.add(new int[]{b[0], -b[2]}); // Left point. Set to negative to benefit sorting.
      corners.add(new int[]{b[1], b[2]}); // Right point.
    }
    Collections.sort(corners, (a, b) -> (a[0] == b[0]) ? a[1] - b[1] : a[0] - b[0]);
    // A hash heap. Store height and number of rectangles available. Get max height by calling firstKey().
    TreeMap<Integer, Integer> heights = new TreeMap<>(Collections.reverseOrder()); // Note reverse order, max heap.
    heights.put(0, 1); // Add a dummy rectangle with height 0.
    int prevHeight = 0; // Store previous height for comparison later.
    List<int[]> skyLine = new ArrayList<>();
    for (int[] c : corners) {
      // Update heap according to left/right point.
      if (c[1] < 0) { // Height < 0, left point, add rectangle.
        heights.merge(-c[1], 1, (oldValue, newValue) -> oldValue + newValue);
      } else { // Height > 0, right point, remove rectangle.
        heights.merge(c[1], 1, (oldValue, newValue) -> oldValue.equals(newValue) ? null : oldValue - newValue);
      }
      if (prevHeight != heights.firstKey()) { // If current max height is the same as prevHeight, not a contour.
        skyLine.add(new int[]{c[0], heights.firstKey()});
        prevHeight = heights.firstKey();
      }
    }
    return skyLine;
  }
}