/*
 * 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.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.apache.commons.geometry.euclidean.internal.AbstractPathConnector;
import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.angle.PlaneAngleRadians;

/** Abstract class for joining collections of line subsets into connected
 * paths. This class is not thread-safe.
 */
public abstract class AbstractLinePathConnector
    extends AbstractPathConnector<AbstractLinePathConnector.ConnectableLineSubset> {
    /** Add a line subset to the connector, leaving it unconnected until a later call to
     * to {@link #connect(Iterable)} or {@link #connectAll()}.
     * @param subset line subset to add
     * @see #connect(Iterable)
     * @see #connectAll()
     */
    public void add(final LineConvexSubset subset) {
        addPathElement(new ConnectableLineSubset(subset));
    }

    /** Add a collection of line subsets to the connector, leaving them unconnected
     * until a later call to {@link #connect(Iterable)} or
     * {@link #connectAll()}.
     * @param subsets line subsets to add
     * @see #connect(Iterable)
     * @see #connectAll()
     * @see #add(LineConvexSubset)
     */
    public void add(final Iterable<LineConvexSubset> subsets) {
        for (final LineConvexSubset subset : subsets) {
            add(subset);
        }
    }

    /** Add a collection of line subsets to the connector and attempt to connect each new
     * line subset with existing subsets. Connections made at this time will not be
     * overwritten by subsequent calls to this or other connection methods.
     * (eg, {@link #connectAll()}).
     *
     * <p>The connector is not reset by this call. Additional line subsets can still be added
     * to the current set of paths.</p>
     * @param subsets line subsets to connect
     * @see #connectAll()
     */
    public void connect(final Iterable<LineConvexSubset> subsets) {
        final List<ConnectableLineSubset> newEntries = new ArrayList<>();

        for (final LineConvexSubset subset : subsets) {
            newEntries.add(new ConnectableLineSubset(subset));
        }

        connectPathElements(newEntries);
    }

    /** Add the given line subsets to this instance and connect all current
     * subsets into connected paths. This call is equivalent to
     * <pre>
     *      connector.add(subsets);
     *      List&lt;LinePath&gt; result = connector.connectAll();
     * </pre>
     *
     * <p>The connector is reset after this call. Further calls to
     * add or connect line subsets will result in new paths being generated.</p>
     * @param subsets line subsets to add
     * @return the connected 2D paths
     * @see #add(Iterable)
     * @see #connectAll()
     */
    public List<LinePath> connectAll(final Iterable<LineConvexSubset> subsets) {
        add(subsets);
        return connectAll();
    }

    /** Connect all current line subsets into connected paths, returning the result as a
     * list of line paths.
     *
     * <p>The connector is reset after this call. Further calls to
     * add or connect line subsets will result in new paths being generated.</p>
     * @return the connected 2D paths
     */
    public List<LinePath> connectAll() {
        final List<ConnectableLineSubset> roots = computePathRoots();
        final List<LinePath> paths = new ArrayList<>(roots.size());

        for (final ConnectableLineSubset root : roots) {
            paths.add(toPath(root));
        }

        return paths;
    }

    /** Convert the linked list of path elements starting at the argument
     * into a {@link LinePath}.
     * @param root root of a connected path linked list
     * @return a line path representing the linked list path
     */
    private LinePath toPath(final ConnectableLineSubset root) {
        final LinePath.Builder builder = LinePath.builder(null);

        builder.append(root.getLineSubset());

        ConnectableLineSubset current = root.getNext();

        while (current != null && current != root) {
            builder.append(current.getLineSubset());
            current = current.getNext();
        }

        return builder.build();
    }

    /** Internal class used to connect line subsets together.
     */
    protected static class ConnectableLineSubset
        extends AbstractPathConnector.ConnectableElement<ConnectableLineSubset> {
        /** Line subset start point. This will be used to connect to other path elements. */
        private final Vector2D start;

        /** Line subset for the entry. */
        private final LineConvexSubset subset;

        /** Create a new instance with the given start point. This constructor is
         * intended only for performing searches for other path elements.
         * @param start start point
         */
        public ConnectableLineSubset(final Vector2D start) {
            this(start, null);
        }

        /** Create a new instance from the given line subset.
         * @param subset subset instance
         */
        public ConnectableLineSubset(final LineConvexSubset subset) {
            this(subset.getStartPoint(), subset);
        }

        /** Create a new instance with the given start point and line subset.
         * @param start start point
         * @param subset line subset instance
         */
        private ConnectableLineSubset(final Vector2D start, final LineConvexSubset subset) {
            this.start = start;
            this.subset = subset;
        }

        /** Get the line subset for this instance.
         * @return the line subset for this instance
         */
        public LineConvexSubset getLineSubset() {
            return subset;
        }

        /** {@inheritDoc} */
        @Override
        public boolean hasStart() {
            return start != null;
        }

        /** {@inheritDoc} */
        @Override
        public boolean hasEnd() {
            return subset != null && subset.getEndPoint() != null;
        }

        /** Return true if this instance has a size equivalent to zero.
         * @return true if this instance has a size equivalent to zero.
         */
        public boolean hasZeroSize() {
            return subset != null && subset.getPrecision().eqZero(subset.getSize());
        }

        /** {@inheritDoc} */
        @Override
        public boolean endPointsEq(final ConnectableLineSubset other) {
            if (hasEnd() && other.hasEnd()) {
                return subset.getEndPoint()
                        .eq(other.subset.getEndPoint(), subset.getPrecision());
            }

            return false;
        }

        /** {@inheritDoc} */
        @Override
        public boolean canConnectTo(final ConnectableLineSubset next) {
            final Vector2D end = subset.getEndPoint();
            final Vector2D nextStart = next.start;

            return end != null && nextStart != null &&
                    end.eq(nextStart, subset.getPrecision());
        }

        /** {@inheritDoc} */
        @Override
        public double getRelativeAngle(final ConnectableLineSubset next) {
            return subset.getLine().angle(next.getLineSubset().getLine());
        }

        /** {@inheritDoc} */
        @Override
        public ConnectableLineSubset getConnectionSearchKey() {
            return new ConnectableLineSubset(subset.getEndPoint());
        }

        /** {@inheritDoc} */
        @Override
        public boolean shouldContinueConnectionSearch(final ConnectableLineSubset candidate, final boolean ascending) {

            if (candidate.hasStart()) {
                final double candidateX = candidate.getLineSubset().getStartPoint().getX();
                final double thisX = subset.getEndPoint().getX();
                final int cmp = subset.getPrecision().compare(candidateX, thisX);

                return ascending ? cmp <= 0 : cmp >= 0;
            }

            return true;
        }

        /** {@inheritDoc} */
        @Override
        public int compareTo(final ConnectableLineSubset other) {
            // sort by coordinates
            int cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(start, other.start);
            if (cmp == 0) {
                // sort entries without line subsets before ones with
                final boolean thisHasSubset = subset != null;
                final boolean otherHasSubset = other.subset != null;

                cmp = Boolean.compare(thisHasSubset, otherHasSubset);

                if (cmp == 0 && thisHasSubset) {
                    // place point-like line subsets before ones with non-zero length
                    cmp = Boolean.compare(this.hasZeroSize(), other.hasZeroSize());

                    if (cmp == 0) {
                        // sort by line angle
                        final double aAngle = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(
                                this.getLineSubset().getLine().getAngle());
                        final double bAngle = PlaneAngleRadians.normalizeBetweenMinusPiAndPi(
                                other.getLineSubset().getLine().getAngle());

                        cmp = Double.compare(aAngle, bAngle);
                    }
                }
            }
            return cmp;
        }

        /** {@inheritDoc} */
        @Override
        public int hashCode() {
            return Objects.hash(start, subset);
        }

        /** {@inheritDoc} */
        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || !this.getClass().equals(obj.getClass())) {
                return false;
            }

            final ConnectableLineSubset other = (ConnectableLineSubset) obj;
            return Objects.equals(this.start, other.start) &&
                    Objects.equals(this.subset, other.subset);
        }

        /** {@inheritDoc} */
        @Override
        protected ConnectableLineSubset getSelf() {
            return this;
        }
    }
}
