/*
 * 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.commons.geometry.euclidean.twod.path;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.geometry.core.Sized;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.euclidean.twod.BoundarySource2D;
import org.apache.commons.geometry.euclidean.twod.Line;
import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
import org.apache.commons.geometry.euclidean.twod.Lines;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.core.Precision;

/** Class representing a connected path of {@link LineConvexSubset line convex subsets}.
 * The elements in the path are connected end to end, with the end vertex of the previous
 * element equivalent to the start vertex of the next element. Elements are not required to
 * be finite. However, since path elements are connected, only the first element and/or last
 * element may be infinite.
 *
 * <p>Instances of this class are guaranteed to be immutable.</p>
 */
public class LinePath implements BoundarySource2D, Sized {
    /** Line path instance containing no elements. */
    private static final LinePath EMPTY = new LinePath(Collections.emptyList());

    /** The line convex subsets comprising the path. */
    private final List<LineConvexSubset> elements;

    /** Simple constructor. Callers are responsible for ensuring that the given list of
     * line subsets defines a valid path. No validation is performed.
     * @param elements elements defining the path.
     */
    LinePath(final List<LineConvexSubset> elements) {
        this.elements = Collections.unmodifiableList(elements);
    }

    /** {@inheritDoc} */
    @Override
    public Stream<LineConvexSubset> boundaryStream() {
        return getElements().stream();
    }

    /** Get the sequence of line subsets comprising the path.
     * @return the sequence of line subsets comprising the path
     */
    public List<LineConvexSubset> getElements() {
        return elements;
    }

    /** Get the line subset at the start of the path or null if the path is empty. If the
     * path consists of a single line subset, then the returned instance with be the same
     * as that returned by {@link #getEnd()}.
     * @return the line subset at the start of the path or null if the path is empty
     * @see #getEnd()
     */
    public LineConvexSubset getStart() {
        if (!isEmpty()) {
            return elements.get(0);
        }
        return null;
    }

    /** Get the line subset at the end of the path or null if the path is empty. If the
     * path consists of a single line subset, then the returned instance with be the same
     * as that returned by {@link #getStart()}.
     * @return the line subset at the end of the path or null if the path is empty
     * @see #getStart()
     */
    public LineConvexSubset getEnd() {
        if (!isEmpty()) {
            return elements.get(elements.size() - 1);
        }
        return null;
    }

    /** Get the sequence of vertices defined by the path. Vertices appear in the
     * list as many times as they are visited in the path. For example, the vertex
     * sequence for a closed path contains the start point at the beginning
     * of the list as well as the end.
     * @return the sequence of vertices defined by the path
     */
    public List<Vector2D> getVertexSequence() {
        final List<Vector2D> sequence = new ArrayList<>();

        Vector2D pt;

        // add the start point, if present
        pt = getStartVertex();
        if (pt != null) {
            sequence.add(pt);
        }

        // add end points
        for (final LineConvexSubset sub : elements) {
            pt = sub.getEndPoint();
            if (pt != null) {
                sequence.add(pt);
            }
        }

        return sequence;
    }

    /** Return true if the path has an element with infinite size.
     * @return true if the path is infinite
     */
    @Override
    public boolean isInfinite() {
        return !isEmpty() && (getStartVertex() == null || getEndVertex() == null);
    }

    /** Return true if the path has a finite size. This will be true if there are
     * no elements in the path or if all elements have a finite length.
     * @return true if the path is finite
     */
    @Override
    public boolean isFinite() {
        return !isInfinite();
    }

    /** {@inheritDoc}
     *
     * <p>The size of the path is defined as the sum of the sizes (lengths) of all path elements.</p>
     */
    @Override
    public double getSize() {
        double sum = 0.0;
        for (final LineConvexSubset element : elements) {
            sum += element.getSize();
        }

        return sum;
    }

    /** Return true if the path does not contain any elements.
     * @return true if the path does not contain any elements
     */
    public boolean isEmpty() {
        return elements.isEmpty();
    }

    /** Return true if the path is closed, meaning that the end point for the last
     * element is equivalent to the start point of the first.
     * @return true if the end point for the last element is equivalent to the
     *      start point for the first
     */
    public boolean isClosed() {
        final LineConvexSubset endElement = getEnd();

        if (endElement != null) {
            final Vector2D start = getStartVertex();
            final Vector2D end = endElement.getEndPoint();

            return start != null && end != null && start.eq(end, endElement.getPrecision());
        }

        return false;
    }

    /** Transform this instance with the argument, returning the result in a new instance.
     * @param transform the transform to apply
     * @return a new instance, transformed by the argument
     */
    public LinePath transform(final Transform<Vector2D> transform) {
        if (!isEmpty()) {
            final List<LineConvexSubset> transformed = elements.stream()
                .map(s -> s.transform(transform))
                .collect(Collectors.toCollection(ArrayList::new));

            return new LinePath(transformed);
        }

        return this;
    }

    /** Return a new instance with all line subset directions, and their order,
     * reversed. The last line subset in this instance will be the first in the
     * returned instance.
     * @return a new instance with the path reversed
     */
    public LinePath reverse() {
        if (!isEmpty()) {
            final List<LineConvexSubset> reversed = elements.stream()
                .map(LineConvexSubset::reverse)
                .collect(Collectors.toCollection(ArrayList::new));
            Collections.reverse(reversed);

            return new LinePath(reversed);
        }

        return this;
    }

    /** Simplify this path, if possible, by combining adjacent elements that lie on the
     * same line (as determined by {@link Line#equals(Object)}).
     * @return a simplified instance
     */
    public LinePath simplify() {
        final List<LineConvexSubset> simplified = new ArrayList<>();

        final int size = elements.size();

        LineConvexSubset current;
        Line currentLine;
        double end;

        int idx = 0;
        int testIdx;
        while (idx < size) {
            current = elements.get(idx);
            currentLine = current.getLine();
            end = current.getSubspaceEnd();

            // try to combine with forward neighbors
            testIdx = idx + 1;
            while (testIdx < size && currentLine.equals(elements.get(testIdx).getLine())) {
                end = Math.max(end, elements.get(testIdx).getSubspaceEnd());
                ++testIdx;
            }

            if (testIdx > idx + 1) {
                // we found something to merge
                simplified.add(Lines.subsetFromInterval(currentLine, current.getSubspaceStart(), end));
            } else {
                simplified.add(current);
            }

            idx = testIdx;
        }

        // combine the first and last items if needed
        if (isClosed() && simplified.size() > 2 && simplified.get(0).getLine().equals(
                simplified.get(simplified.size() - 1).getLine())) {

            final LineConvexSubset startElement = simplified.get(0);
            final LineConvexSubset endElement = simplified.remove(simplified.size() - 1);

            final LineConvexSubset combined = Lines.subsetFromInterval(
                    endElement.getLine(), endElement.getSubspaceStart(), startElement.getSubspaceEnd());

            simplified.set(0, combined);
        }

        return new SimplifiedLinePath(simplified);
    }

    /** Return a string representation of the path.
     *
     * <p>In order to keep the string representation short but useful, the exact format of the return
     * value depends on the properties of the path. See below for examples.
     *
     * <ul>
     *      <li>Empty path
     *          <ul>
     *              <li>{@code LinePath[empty= true]}</li>
     *          </ul>
     *      </li>
     *      <li>Single element
     *          <ul>
     *              <li>{@code LinePath[single= Segment[startPoint= (0.0, 0.0), endPoint= (1.0, 0.0)]]}</li>
     *          </ul>
     *      </li>
     *      <li>Path with infinite start element
     *          <ul>
     *              <li>{@code LinePath[startDirection= (1.0, 0.0), vertices= [(1.0, 0.0), (1.0, 1.0)]]}</li>
     *          </ul>
     *      </li>
     *      <li>Path with infinite end element
     *          <ul>
     *              <li>{@code LinePath[vertices= [(0.0, 1.0), (0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
     *          </ul>
     *      </li>
     *      <li>Path with infinite start and end elements
     *          <ul>
     *              <li>{@code LinePath[startDirection= (0.0, 1.0), vertices= [(0.0, 0.0)], endDirection= (1.0, 0.0)]}</li>
     *          </ul>
     *      </li>
     *      <li>Path with no infinite elements
     *          <ul>
     *              <li>{@code LinePath[vertices= [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]]}</li>
     *          </ul>
     *      </li>
     * </ul>
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(this.getClass().getSimpleName())
            .append('[');

        if (elements.isEmpty()) {
            sb.append("empty= true");
        } else if (elements.size() == 1) {
            sb.append("single= ")
                .append(elements.get(0));
        } else {
            final LineConvexSubset startElement = getStart();
            if (startElement.getStartPoint() == null) {
                sb.append("startDirection= ")
                    .append(startElement.getLine().getDirection())
                    .append(", ");
            }

            sb.append("vertexSequence= ")
                .append(getVertexSequence());

            final LineConvexSubset endElement = getEnd();
            if (endElement.getEndPoint() == null) {
                sb.append(", endDirection= ")
                    .append(endElement.getLine().getDirection());
            }
        }

        sb.append(']');

        return sb.toString();
    }

    /** Get the start vertex for the path or null if the path is empty
     * or has an infinite start line subset.
     * @return the start vertex for the path or null if the path does
     *      not start with a vertex
     */
    private Vector2D getStartVertex() {
        final LineConvexSubset seg = getStart();
        return (seg != null) ? seg.getStartPoint() : null;
    }

    /** Get the end vertex for the path or null if the path is empty
     * or has an infinite end line subset.
     * @return the end vertex for the path or null if the path does
     *      not end with a vertex
     */
    private Vector2D getEndVertex() {
        final LineConvexSubset seg = getEnd();
        return (seg != null) ? seg.getEndPoint() : null;
    }

    /** Build a new path from the given line subsets.
     * @param subsets the line subsets to comprise the path
     * @return new path containing the given line subsets in order
     * @throws IllegalStateException if the line subsets do not form a connected path
     */
    public static LinePath from(final LineConvexSubset... subsets) {
        return from(Arrays.asList(subsets));
    }

    /** Build a new path from the given line subsets.
     * @param subsets the line subsets to comprise the path
     * @return new path containing the given line subsets in order
     * @throws IllegalStateException if the subsets do not form a connected path
     */
    public static LinePath from(final Collection<? extends LineConvexSubset> subsets) {
        final Builder builder = builder(null);

        for (final LineConvexSubset subset : subsets) {
            builder.append(subset);
        }

        return builder.build();
    }

    /** Build a new path from the given vertices. A line segment is created
     * from the last vertex to the first one, if the two vertices are not already
     * considered equal using the given precision context. This method is equivalent to
     * calling {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
     * fromVertices(vertices, true, precision)}
     * @param vertices the vertices to construct the closed path from
     * @param precision precision context used to construct the line segment
     *      instances for the path
     * @return new closed path constructed from the given vertices
     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
     * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
     */
    public static LinePath fromVertexLoop(final Collection<Vector2D> vertices,
            final Precision.DoubleEquivalence precision) {

        return fromVertices(vertices, true, precision);
    }

    /** Build a new path from the given vertices. No additional segment is added
     * from the last vertex to the first. This method is equivalent to calling
     * {@link #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
     * fromVertices(vertices, false, precision)}.
     * @param vertices the vertices to construct the path from
     * @param precision precision context used to construct the line segment
     *      instances for the path
     * @return new path constructed from the given vertices
     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
     * @see #fromVertices(Collection, boolean, Precision.DoubleEquivalence)
     */
    public static LinePath fromVertices(final Collection<Vector2D> vertices,
            final Precision.DoubleEquivalence precision) {

        return fromVertices(vertices, false, precision);
    }

    /** Build a new path from the given vertices.
     * @param vertices the vertices to construct the path from
     * @param close if true, a line segment is created from the last vertex
     *      given to the first one, if the two vertices are not already considered
     *      equal using the given precision context.
     * @param precision precision context used to construct the line segment
     *      instances for the path
     * @return new path constructed from the given vertices
     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
     */
    public static LinePath fromVertices(final Collection<Vector2D> vertices,
            final boolean close, final Precision.DoubleEquivalence precision) {

        return builder(precision)
                .appendVertices(vertices)
                .build(close);
    }

    /** Return a path containing no elements.
     * @return a path containing no elements
     */
    public static LinePath empty() {
        return EMPTY;
    }

    /** Return a {@link Builder} instance configured with the given precision
     * context. The precision context is used when building line segments from
     * vertices and may be omitted if raw vertices are not used.
     * @param precision precision context to use when building line segments from
     *      raw vertices; may be null if raw vertices are not used.
     * @return a new {@link Builder} instance
     */
    public static Builder builder(final Precision.DoubleEquivalence precision) {
        return new Builder(precision);
    }

    /** Class used to build line paths.
     */
    public static final class Builder {
        /** Line subsets appended to the path. */
        private List<LineConvexSubset> appended;

        /** Line subsets prepended to the path. */
        private List<LineConvexSubset> prepended;

        /** Precision context used when creating line segments directly from vertices. */
        private Precision.DoubleEquivalence precision;

        /** The current vertex at the start of the path. */
        private Vector2D startVertex;

        /** The current vertex at the end of the path. */
        private Vector2D endVertex;

        /** The precision context used when performing comparisons involving the current
         * end vertex.
         */
        private Precision.DoubleEquivalence endVertexPrecision;

        /** Construct a new instance configured with the given precision context. The
         * precision context is used when building line segments from vertices and
         * may be omitted if raw vertices are not used.
         * @param precision precision context to use when creating line segments
         *      from vertices
         */
        private Builder(final Precision.DoubleEquivalence precision) {
            setPrecision(precision);
        }

        /** Set the precision context. This context is used only when creating line segments
         * from appended or prepended vertices. It is not used when adding existing
         * {@link LineConvexSubset} instances since those contain their own precision contexts.
         * @param builderPrecision precision context to use when creating line segments
         *      from vertices
         * @return this instance
         */
        public Builder setPrecision(final Precision.DoubleEquivalence builderPrecision) {
            this.precision = builderPrecision;

            return this;
        }

        /** Get the line subset at the start of the path or null if it does not exist.
         * @return the line subset at the start of the path
         */
        public LineConvexSubset getStart() {
            LineConvexSubset start = getLast(prepended);
            if (start == null) {
                start = getFirst(appended);
            }
            return start;
        }

        /** Get the line subset at the end of the path or null if it does not exist.
         * @return the line subset at the end of the path
         */
        public LineConvexSubset getEnd() {
            LineConvexSubset end = getLast(appended);
            if (end == null) {
                end = getFirst(prepended);
            }
            return end;
        }

        /** Append a line subset to the end of the path.
         * @param subset line subset to append to the path
         * @return the current builder instance
         * @throws IllegalStateException if the path contains a previous element
         *      and the end vertex of the previous element is not equivalent to the
         *      start vertex of the argument
         */
        public Builder append(final LineConvexSubset subset) {
            validateConnected(getEnd(), subset);
            appendInternal(subset);

            return this;
        }

        /** Add a vertex to the end of this path. If the path already has an end vertex,
         * then a line segment is added between the previous end vertex and this vertex,
         * using the configured precision context.
         * @param vertex the vertex to add
         * @return this instance
         * @see #setPrecision(Precision.DoubleEquivalence)
         */
        public Builder append(final Vector2D vertex) {
            final Precision.DoubleEquivalence vertexPrecision = getAddVertexPrecision();

            if (endVertex == null) {
                // make sure that we're not adding to an infinite element
                final LineConvexSubset end = getEnd();
                if (end != null) {
                    throw new IllegalStateException(
                            MessageFormat.format("Cannot add vertex {0} after infinite line subset: {1}",
                                    vertex, end));
                }

                // this is the first vertex added
                startVertex = vertex;
                endVertex = vertex;
                endVertexPrecision = vertexPrecision;
            } else if (!endVertex.eq(vertex, endVertexPrecision)) {
                // only add the vertex if its not equal to the end point
                // of the last element
                appendInternal(Lines.segmentFromPoints(endVertex, vertex, endVertexPrecision));
            }

            return this;
        }

        /** Convenience method for appending a collection of vertices to the path in a single method call.
         * @param vertices the vertices to append
         * @return this instance
         * @see #append(Vector2D)
         */
        public Builder appendVertices(final Collection<? extends Vector2D> vertices) {
            for (final Vector2D vertex : vertices) {
                append(vertex);
            }

            return this;
        }

        /** Convenience method for appending multiple vertices to the path at once.
         * @param vertices the vertices to append
         * @return this instance
         * @see #append(Vector2D)
         */
        public Builder appendVertices(final Vector2D... vertices) {
            return appendVertices(Arrays.asList(vertices));
        }

        /** Prepend a line subset to the beginning of the path.
         * @param subset line subset to prepend to the path
         * @return the current builder instance
         * @throws IllegalStateException if the path contains a start element
         *      and the end vertex of the argument is not equivalent to the
         *      start vertex of the start element.
         */
        public Builder prepend(final LineConvexSubset subset) {
            validateConnected(subset, getStart());
            prependInternal(subset);

            return this;
        }

        /** Add a vertex to the front of this path. If the path already has a start vertex,
         * then a line segment is added between this vertex and the previous start vertex,
         * using the configured precision context.
         * @param vertex the vertex to add
         * @return this instance
         * @see #setPrecision(Precision.DoubleEquivalence)
         */
        public Builder prepend(final Vector2D vertex) {
            final Precision.DoubleEquivalence vertexPrecision = getAddVertexPrecision();

            if (startVertex == null) {
                // make sure that we're not adding to an infinite element
                final LineConvexSubset start = getStart();
                if (start != null) {
                    throw new IllegalStateException(
                            MessageFormat.format("Cannot add vertex {0} before infinite line subset: {1}",
                                    vertex, start));
                }

                // this is the first vertex added
                startVertex = vertex;
                endVertex = vertex;
                endVertexPrecision = vertexPrecision;
            } else if (!vertex.eq(startVertex, vertexPrecision)) {
                // only add if the vertex is not equal to the start
                // point of the first element
                prependInternal(Lines.segmentFromPoints(vertex, startVertex, vertexPrecision));
            }

            return this;
        }

        /** Convenience method for prepending a collection of vertices to the path in a single method call.
         * The vertices are logically prepended as a single group, meaning that the first vertex
         * in the given collection appears as the first vertex in the path after this method call.
         * Internally, this means that the vertices are actually passed to the {@link #prepend(Vector2D)}
         * method in reverse order.
         * @param vertices the vertices to prepend
         * @return this instance
         * @see #prepend(Vector2D)
         */
        public Builder prependVertices(final Collection<Vector2D> vertices) {
            return prependVertices(vertices.toArray(new Vector2D[0]));
        }

        /** Convenience method for prepending multiple vertices to the path in a single method call.
         * The vertices are logically prepended as a single group, meaning that the first vertex
         * in the given collection appears as the first vertex in the path after this method call.
         * Internally, this means that the vertices are actually passed to the {@link #prepend(Vector2D)}
         * method in reverse order.
         * @param vertices the vertices to prepend
         * @return this instance
         * @see #prepend(Vector2D)
         */
        public Builder prependVertices(final Vector2D... vertices) {
            for (int i = vertices.length - 1; i >= 0; --i) {
                prepend(vertices[i]);
            }

            return this;
        }

        /** Close the current path and build a new {@link LinePath} instance.  This method is equivalent
         * to {@code builder.build(true)}.
         * @return new closed path instance
         * @throws IllegalStateException if the builder was given only a single unique vertex
         */
        public LinePath close() {
            return build(true);
        }

        /** Build a {@link LinePath} instance from the configured path. This method is equivalent
         * to {@code builder.build(false)}.
         * @return new path instance
         * @throws IllegalStateException if the builder was given only a single unique vertex
         */
        public LinePath build() {
            return build(false);
        }

        /** Build a {@link LinePath} instance from the configured path.
         * @param close if true, the path will be closed by adding an end point equivalent to the
         *      start point
         * @return new path instance
         * @throws IllegalStateException if the builder was given only a single unique vertex
         */
        public LinePath build(final boolean close) {
            if (close) {
                closePath();
            }

            // combine all of the line subsets
            List<LineConvexSubset> result = null;

            if (prepended != null) {
                result = prepended;
                Collections.reverse(result);
            }

            if (appended != null) {
                if (result == null) {
                    result = appended;
                } else {
                    result.addAll(appended);
                }
            }

            if (result == null) {
                result = Collections.emptyList();
            }

            if (result.isEmpty() && startVertex != null) {
                throw new IllegalStateException(
                        MessageFormat.format("Unable to create line path; only a single unique vertex provided: {0} ",
                                startVertex));
            }

            // clear internal state
            appended = null;
            prepended = null;

            // build the final path instance, using the shared empty instance if
            // no line subsets are present

            return result.isEmpty() ? empty() : new LinePath(result);
        }

        /** Close the path by adding an end point equivalent to the path start point.
         * @throws IllegalStateException if the path cannot be closed
         */
        private void closePath() {
            final LineConvexSubset end = getEnd();

            if (end != null) {
                if (startVertex != null && endVertex != null) {
                    if (!endVertex.eq(startVertex, endVertexPrecision)) {
                        appendInternal(Lines.segmentFromPoints(endVertex, startVertex, endVertexPrecision));
                    }
                } else {
                    throw new IllegalStateException("Unable to close line path: line path is infinite");
                }
            }
        }

        /** Validate that the given line subsets  are connected, meaning that the end vertex of {@code previous}
         * is equivalent to the start vertex of {@code next}. The line subsets are considered valid if either
         * line subset is null.
         * @param previous previous line subset
         * @param next next line subset
         * @throws IllegalStateException if previous and next are not null and the end vertex of previous
         *      is not equivalent the start vertex of next
         */
        private void validateConnected(final LineConvexSubset previous, final LineConvexSubset next) {
            if (previous != null && next != null) {
                final Vector2D nextStartVertex = next.getStartPoint();
                final Vector2D previousEndVertex = previous.getEndPoint();
                final Precision.DoubleEquivalence previousPrecision = previous.getPrecision();

                if (nextStartVertex == null || previousEndVertex == null ||
                        !(nextStartVertex.eq(previousEndVertex, previousPrecision))) {

                    throw new IllegalStateException(
                            MessageFormat.format("Path line subsets are not connected: previous= {0}, next= {1}",
                                    previous, next));
                }
            }
        }

        /** Get the precision context used when adding raw vertices to the path. An exception is thrown
         * if no precision has been specified.
         * @return the precision context used when creating working with raw vertices
         * @throws IllegalStateException if no precision context is configured
         */
        private Precision.DoubleEquivalence getAddVertexPrecision() {
            if (precision == null) {
                throw new IllegalStateException("Unable to create line segment: no vertex precision specified");
            }

            return precision;
        }

        /** Append the given, validated line subsets to the path.
         * @param subset validated line subset to append
         */
        private void appendInternal(final LineConvexSubset subset) {
            if (appended == null) {
                appended = new ArrayList<>();
            }

            if (appended.isEmpty() &&
                    (prepended == null || prepended.isEmpty())) {
                startVertex = subset.getStartPoint();
            }

            endVertex = subset.getEndPoint();
            endVertexPrecision = subset.getPrecision();

            appended.add(subset);
        }

        /** Prepend the given, validated line subset to the path.
         * @param subset validated line subset to prepend
         */
        private void prependInternal(final LineConvexSubset subset) {
            if (prepended == null) {
                prepended = new ArrayList<>();
            }

            startVertex = subset.getStartPoint();

            if (prepended.isEmpty() &&
                    (appended == null || appended.isEmpty())) {
                endVertex = subset.getEndPoint();
                endVertexPrecision = subset.getPrecision();
            }

            prepended.add(subset);
        }

        /** Get the first element in the list or null if the list is null
         * or empty.
         * @param list the list to return the first item from
         * @return the first item from the given list or null if it does not exist
         */
        private LineConvexSubset getFirst(final List<? extends LineConvexSubset> list) {
            if (list != null && !list.isEmpty()) {
                return list.get(0);
            }
            return null;
        }

        /** Get the last element in the list or null if the list is null
         * or empty.
         * @param list the list to return the last item from
         * @return the last item from the given list or null if it does not exist
         */
        private LineConvexSubset getLast(final List<? extends LineConvexSubset> list) {
            if (list != null && !list.isEmpty()) {
                return list.get(list.size() - 1);
            }
            return null;
        }
    }

    /** Internal class returned when a line path is simplified to remove unnecessary line subset divisions.
     * The {@link #simplify()} method on this class simply returns the same instance.
     */
    private static final class SimplifiedLinePath extends LinePath {
        /** Create a new instance containing the given line subsets. No validation is
         * performed on the inputs. Caller must ensure that the given line subsets represent
         * a valid, simplified path.
         * @param elements line subsets comprising the path
         */
        private SimplifiedLinePath(final List<LineConvexSubset> elements) {
            super(elements);
        }

        /** {@inheritDoc} */
        @Override
        public SimplifiedLinePath simplify() {
            // already simplified
            return this;
        }
    }
}
