/*
 * Copyright (c) 2022 Martin Davis.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jts.coverage;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateArrays;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.io.WKTWriter;

/**
 * An edge of a polygonal coverage formed from all or a section of a polygon ring.
 * An edge may be a free ring, which is a ring which has no node points
 * (i.e. does not share a vertex with any other rings in the parent coverage).
 * 
 * @author mdavis
 *
 */
class CoverageEdge {

  public static final int RING_COUNT_INNER = 2;
  public static final int RING_COUNT_OUTER = 1;

  public static CoverageEdge createEdge(Coordinate[] ring, boolean isPrimary) {
    Coordinate[] pts = extractEdgePoints(ring, 0, ring.length - 1);
    CoverageEdge edge = new CoverageEdge(pts, isPrimary, true);
    return edge;
  }

  public static CoverageEdge createEdge(Coordinate[] ring, int start, int end, boolean isPrimary) {
    Coordinate[] pts = extractEdgePoints(ring, start, end);
    CoverageEdge edge = new CoverageEdge(pts, isPrimary, false);
    return edge;
  }

  private static Coordinate[] extractEdgePoints(Coordinate[] ring, int start, int end) {
    int size = start < end 
                  ? end - start + 1 
                  : ring.length - start + end;
    Coordinate[] pts = new Coordinate[size];
    int iring = start;
    for (int i = 0; i < size; i++) {
      pts[i] = ring[iring].copy();
      iring += 1;
      if (iring >= ring.length) iring = 1;
    }
    return pts;
  }

  /**
   * Computes a key segment for a ring.
   * The key is the segment starting at the lowest vertex,
   * towards the lowest adjacent distinct vertex.
   * 
   * @param ring a linear ring
   * @return a LineSegment representing the key
   */
  public static LineSegment key(Coordinate[] ring) {
   // find lowest vertex index
    int indexLow = 0;
    for (int i = 1; i < ring.length - 1; i++) {
      if (ring[indexLow].compareTo(ring[i]) < 0)
        indexLow = i;
    }
    Coordinate key0 = ring[indexLow];
    // find distinct adjacent vertices
    Coordinate adj0 = findDistinctPoint(ring, indexLow, true, key0);
    Coordinate adj1 = findDistinctPoint(ring, indexLow, false, key0);
    Coordinate key1 = adj0.compareTo(adj1) < 0 ? adj0 : adj1;
    return new LineSegment(key0, key1);
  }
  
  /**
   * Computes a distinct key for a section of a linear ring.
   * 
   * @param ring the linear ring
   * @param start index of the start of the section
   * @param end end index of the end of the section
   * @return a LineSegment representing the key
   */
  public static LineSegment key(Coordinate[] ring, int start, int end) {
    //-- endpoints are distinct in a line edge
    Coordinate end0 = ring[start];
    Coordinate end1 = ring[end];
    boolean isForward = 0 > end0.compareTo(end1);
    Coordinate key0, key1;
    if (isForward) {
      key0 = end0;
      key1 = findDistinctPoint(ring, start, true, key0);
    }
    else {
      key0 = end1;
      key1 = findDistinctPoint(ring, end, false, key0);
    }
    return new LineSegment(key0, key1);  
  }

  private static Coordinate findDistinctPoint(Coordinate[] pts, int index, boolean isForward, Coordinate pt) {
    int inc = isForward ? 1 : -1;
    int i = index;
    do {
      if (! pts[i].equals2D(pt)) {
        return pts[i];
      }
      // increment index with wrapping
      i += inc;
      if (i < 0) {
        i = pts.length - 1;
      }
      else if (i > pts.length - 1) {
        i = 0;
      }
    } while (i != index);
    throw new IllegalStateException("Edge does not contain distinct points");
  }

  private Coordinate[] pts;
  private int ringCount = 0;
  private boolean isFreeRing = true;
  private boolean isPrimary = true;
  private int adjacentIndex0 = -1;
  private int adjacentIndex1 = -1;

  public CoverageEdge(Coordinate[] pts, boolean isPrimary, boolean isFreeRing) {
    this.pts = pts;
    this.isPrimary = isPrimary;
    this.isFreeRing = isFreeRing;
  }
  
  public void incRingCount() {
    ringCount++;
  }
  
  public int getRingCount() {
    return ringCount;
  }

  public boolean isInner() {
    return ringCount == RING_COUNT_INNER;
  }
  
  public boolean isOuter() {
    return ringCount == RING_COUNT_OUTER;
  }
  
  public void setPrimary(boolean isPrimary) {
    //-- preserve primary status if set
    if (this.isPrimary)
      return;
    this.isPrimary = isPrimary;
  }
  
  public boolean isRemovableRing() {
    boolean isRing = CoordinateArrays.isRing(pts);
    return isRing && ! isPrimary;
  }

  /**
   * Returns whether this edge is a free ring;
   * i.e. one that does not have nodes
   * which are anchored because they occur in another ring.
   * 
   * @return true if this is a free ring
   */
  public boolean isFreeRing() {
    return isFreeRing;
  }

  public void setCoordinates(Coordinate[] pts) {
    this.pts = pts;
  }

  public Coordinate[] getCoordinates() {
    return pts;
  }

  public Coordinate getEndCoordinate() {
    return pts[pts.length - 1];
  }
  
  public Coordinate getStartCoordinate() {
    return pts[0];
  }
  
  public LineString toLineString(GeometryFactory geomFactory) {
    return geomFactory.createLineString(getCoordinates());
  }
  
  public String toString() {
    return WKTWriter.toLineString(pts);
  }

  public void addIndex(int index) {
    //TODO: keep information about which element is L and R?
    
    // assert: at least one elementIndex is unset (< 0)
    if (adjacentIndex0 < 0) {
      adjacentIndex0 = index;
    }
    else {
      adjacentIndex1 = index;
    }
  }

  public int getAdjacentIndex(int index) {
    if (index == 0)
      return adjacentIndex0;
    return adjacentIndex1;
  }

  public boolean hasAdjacentIndex(int index) {
    if (index == 0)
      return adjacentIndex0 >= 0;
    return adjacentIndex1 >= 0;
  }

}
