package CH.ifa.draw.standard;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import CH.ifa.draw.framework.UndoableCommand;
import CH.ifa.draw.util.Geom;
import de.renew.draw.storables.ontology.ConnectionFigure;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.storables.ontology.FigureFilter;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.SpreadMode;


/**
 * Spread a selection of figures e.g. make their relative distances
 * equal. The outermost figures (according to the selected operation
 * mode) serve as anchors, all other figures are distributed evenly
 * while respecting their left-to-right or top-to-bottom order.
 * <p>
 * <code>ConnectionFigure</code> and its descendants are ignored
 * in the computation as they cannot be repositioned explicitly.
 * </p>
 * SpreadCommand.java
 * Created: Mon May 22 2000
 * @author Michael Duvigneau
 * @deprecated This class is only for internal usage and will later be made private.
 */
@Deprecated
public class SpreadCommand extends UndoableCommand {

    /**
     * Operation mode: equalize distance between left sides
     */
    public final static int LEFTS = 0;

    /**
     * Operation mode: equalize distance between centers (horizontally)
     */
    public final static int CENTERS = 1;

    /**
     * Operation mode: equalize distance between right sides
     */
    public final static int RIGHTS = 2;

    /**
     * Operation mode: equalize distance between tops
     */
    public final static int TOPS = 3;

    /**
     * Operation mode: equalize distance between middles (vertically)
     */
    public final static int MIDDLES = 4;

    /**
     * Operation mode: equalize distance between bottoms
     */
    public final static int BOTTOMS = 5;

    /**
     * Operation mode: equalize space between bounding rectangles (horizontally)
     **/
    public final static int HORIZONTAL_DISTANCE = 6;

    /**
     * Operation mode: equalize space between bounding rectangles (vertically)
     **/
    public final static int VERTICAL_DISTANCE = 7;

    /**
     * Operation mode: line up all figures along a straight line,
     * the line may be of any gradient. The center of each figure
     * is relevant.
     **/
    public final static int DIAGONAL_CENTERS = 8;

    /**
     * Operation mode: line up all figures along a straight line,
     * the line may be of any gradient. The distances between the
     * figures are relevant.
     * <p><b>Does this mode make any sense?
     *       The current implementation does not.
     * </b></p>
     **/
    private final static int DIAGONAL_DISTANCE = 9;
    private final int _op;

    /**
     * Constructs a spread command.
     *
     * @param name   the command name
     * @param op     kind of the spread operation (LEFTS, RIGHTS,
     *               CENTERS, etc.); determines the spread direction
     *               (horizontal or vertical) and also the location
     *               of the "hotspot" of each figure (the point from
     *               which the distances are calculated).
     * @deprecated This constructor is only for internal usage. Please use
     *             {@link de.renew.draw.ui.api.CommandApi#createSpreadCommand(String, SpreadMode)} instead.
     */
    @Deprecated
    public SpreadCommand(String name, int op) {
        super(name);
        _op = op;
    }

    /**
     * This command is executable when at least 3 figures are
     * selected in the current drawing view.
     **/
    @Override
    public boolean isExecutable() {
        if (!super.isExecutable()) {
            return false;
        }
        return getEditor().view().selectionCount() > 2;
    }

    @Override
    public boolean executeUndoable() {
        // Check if execution conditions are still met.
        if (isExecutable()) {
            if (!super.isExecutable()) {
                return false;
            }
            DrawingView view = getEditor().view();
            FigureEnumeration selection = view.selectionElements();

            // Hide all connection figures in the enumeration.
            selection = new FilteredFigureEnumerator(selection, new ConnectionFilter());


            // But now, there may be the problem that there are
            // less than 3 figures remaining...
            // I will handle that after building up the sorted list.
            // First, copy the figures from the selection enumeration
            // into a list I can work with repeatedly.
            // While touching all figures, I can also figure out the
            // global bounding box surrounding all hotspots.
            List<SortPair> sortedList = new LinkedList<>();
            Iterator<SortPair> sortedIter;
            SortPair currentFigurePair;
            Rectangle bBox = null;

            while (selection.hasMoreElements()) {
                currentFigurePair = new SortPair(_op, selection.nextFigure());
                if (bBox == null) {
                    bBox = new Rectangle(currentFigurePair._hotspot);
                } else {
                    bBox.add(currentFigurePair._hotspot);
                }
                sortedList.add(0, currentFigurePair);
            }

            // Now the promised check whether there are enough figures
            // left after filtering out all connection figures:
            if (sortedList.size() < 3) {
                return false;
            }

            // Then, I need to sort the figures by their relevant
            // corner (the "hotspot" - it depends on the operation
            // mode selected on instantiation).
            if ((_op != DIAGONAL_CENTERS) && (_op != DIAGONAL_DISTANCE)) {
                sortedList.sort(new HotspotComparator(_op));
            } else {
                // In diagonal mode, there are some additional computations
                // needed to find out the outermost figures and how to order
                // all other figures.
                // The proceeding is:
                // 1. Sort all figures by their distance from the center
                //    of the bounding box (largest distance first).
                // 2. Find out the two outermost figures:
                //    The first figure is the first element of the sorted list.
                //    The second figure is not necessarily the second list
                //    element because that could lie near the first one.
                //    Instead, all figures down to a minimum distance to the
                //    center are candidates, the one with the largest distance
                //    to the first figure is chosen.
                //    The minimum distance is half the horizontal or vertical
                //    size of the bounding box (whichever is greater) because
                //    there must exist at least one figure that stretched the
                //    box to that size.
                // 3. Sort all figures by their projection point to the
                //    direct line between first and second outermost figure.
                // After these steps, the first and second outermost figures
                // are forgotten. But they should be found as first and last
                // element in the resulting sorted list.
                // This assumption holds only if there are no figures with
                // projection points outside the end points of the projection
                // line. But I think that such figures do not exist (unproven).
                // Step 1
                Point bBoxCenter = Geom.center(bBox);

                sortedList.sort(new DistanceComparator(bBoxCenter));

                sortedIter = sortedList.iterator();
                SortPair firstPair = sortedIter.next();

                SortPair secondPair = firstPair;
                SortPair currentPair = firstPair;
                int minDistance = (Math.max(bBox.width, bBox.height)) / 2;
                long minDistance2 = (long) minDistance * minDistance;

                long foundDistance2 = 0;
                long currentDistance2;
                while (sortedIter.hasNext()
                    && (currentPair.distance2(bBoxCenter) >= minDistance2)) {
                    currentPair = sortedIter.next();
                    currentDistance2 = currentPair.distance2(firstPair._hotspot);

                    if (foundDistance2 < currentDistance2) {
                        foundDistance2 = currentDistance2;
                        secondPair = currentPair;
                    }
                }

                sortedList.sort(new ProjectionComparator(firstPair._hotspot, secondPair._hotspot));
            }

            // Now compute the sum of the distances to distribute.
            // The sum may be negative, the figures will then overlap
            // after being repositioned.
            // I do not compute the should-distance between each 
            // pair of figures globally to avoid the round-off error
            // to be multiplied.
            int wholeLengthX = 0;
            int wholeLengthY = 0;
            Rectangle prevBox;
            Rectangle currentBox;
            int firstIndex = 0;
            int lastIndex = sortedList.size() - 1;
            sortedIter = sortedList.iterator();

            switch (_op) {
                case LEFTS:
                case RIGHTS:
                case CENTERS:
                case TOPS:
                case BOTTOMS:
                case MIDDLES:
                case DIAGONAL_CENTERS:
                    // Get the distance between the outermost figures.
                    wholeLengthX = (sortedList.get(lastIndex))._hotspot.x
                        - (sortedList.get(firstIndex))._hotspot.x;
                    wholeLengthY = (sortedList.get(lastIndex))._hotspot.y
                        - (sortedList.get(firstIndex))._hotspot.y;
                    break;
                case HORIZONTAL_DISTANCE:
                case VERTICAL_DISTANCE:
                    // Sum the distances between the right(lower) border of
                    // a figure and the left(upper) border of its successor.
                    prevBox = (sortedIter.next())._figure.displayBox();
                    while (sortedIter.hasNext()) {
                        currentBox = (sortedIter.next())._figure.displayBox();
                        wholeLengthX += currentBox.x - prevBox.x - prevBox.width;
                        wholeLengthY += currentBox.y - prevBox.y - prevBox.height;
                        prevBox = currentBox;
                    }
                    break;
                case DIAGONAL_DISTANCE:
                    // Get the distance between the outermost figures (as in classical modes)
                    // then subtract the size of all figures (except the last figure,
                    // which lies outside the bounding box due to hotspot computation).
                    wholeLengthX = (sortedList.get(lastIndex))._hotspot.x
                        - (sortedList.get(firstIndex))._hotspot.x;
                    wholeLengthY = (sortedList.get(lastIndex))._hotspot.y
                        - (sortedList.get(firstIndex))._hotspot.y;
                    prevBox = (sortedIter.next())._figure.displayBox();
                    while (sortedIter.hasNext()) {
                        currentBox = (sortedIter.next())._figure.displayBox();
                        wholeLengthX -= prevBox.width;
                        wholeLengthY -= prevBox.height;
                        prevBox = currentBox;
                    }
                    break;
            }

            // Finally move the hotspot of the figures to 
            // the new "shouldSpots" computed according to
            // the operation mode.
            Point shouldSpot = null;
            Point firstSpot = (sortedList.get(0))._hotspot;
            int nodeCount = 0;
            int intervals = sortedList.size() - 1;
            SortPair prevFigurePair = null; // will contain hotspot *AFTER* move!
            sortedIter = sortedList.iterator();

            while (sortedIter.hasNext()) {
                currentFigurePair = sortedIter.next();
                switch (_op) {
                    case LEFTS:
                    case CENTERS:
                    case RIGHTS:
                        shouldSpot = new Point(
                            firstSpot.x + wholeLengthX * nodeCount / intervals,
                            currentFigurePair._hotspot.y);
                        break;
                    case TOPS:
                    case BOTTOMS:
                    case MIDDLES:
                        shouldSpot = new Point(
                            currentFigurePair._hotspot.x,
                            firstSpot.y + wholeLengthY * nodeCount / intervals);
                        break;
                    case DIAGONAL_CENTERS:
                        shouldSpot = new Point(
                            firstSpot.x + wholeLengthX * nodeCount / intervals,
                            firstSpot.y + wholeLengthY * nodeCount / intervals);
                        break;
                    case HORIZONTAL_DISTANCE:
                    case VERTICAL_DISTANCE:
                    case DIAGONAL_DISTANCE:
                        // The following calculation is dependent on the chosen
                        // hotspot location relative to the bounding rectangle!
                        if (prevFigurePair != null) {
                            // The distance to add is recomputed each round
                            // to get different rounding results.
                            int distanceToAddX = (wholeLengthX * nodeCount / intervals)
                                - (wholeLengthX * (nodeCount - 1) / intervals);
                            int distanceToAddY = (wholeLengthY * nodeCount / intervals)
                                - (wholeLengthY * (nodeCount - 1) / intervals);

                            shouldSpot = switch (_op) {
                                case HORIZONTAL_DISTANCE -> new Point(
                                    prevFigurePair._hotspot.x
                                        + prevFigurePair._figure.displayBox().width
                                        + distanceToAddX,
                                    currentFigurePair._hotspot.y);
                                case VERTICAL_DISTANCE -> new Point(
                                    currentFigurePair._hotspot.x,
                                    prevFigurePair._hotspot.y
                                        + prevFigurePair._figure.displayBox().height
                                        + distanceToAddY);
                                case DIAGONAL_DISTANCE ->
                                    // The figure to use the width/height from depends on
                                    // the direction we iterate through the figures:
                                    // from left(top) to right(bottom) or opposite?
                                    new Point(
                                        prevFigurePair._hotspot.x
                                            + ((wholeLengthX >= 0)
                                                ? prevFigurePair._figure.displayBox().width
                                                : currentFigurePair._figure.displayBox().width)
                                            + distanceToAddX,
                                        prevFigurePair._hotspot.y
                                            + ((wholeLengthY >= 0)
                                                ? prevFigurePair._figure.displayBox().height
                                                : currentFigurePair._figure.displayBox().height)
                                            + distanceToAddY);
                                default -> shouldSpot;
                            };
                        } else {
                            // The first figure should stay at its place.
                            shouldSpot = currentFigurePair._hotspot;
                        }
                        break;
                }
                if (shouldSpot != null) {
                    currentFigurePair._figure.moveBy(
                        shouldSpot.x - currentFigurePair._hotspot.x,
                        shouldSpot.y - currentFigurePair._hotspot.y);
                    prevFigurePair = new SortPair(_op, currentFigurePair._figure);
                }
                nodeCount++;
            }

            view.checkDamage();
            return true;
        }
        return false;
    }

    /**
     * Used by <code>execute()</code> to store the "hotspot"
     * along with the figures in a sorted list.
     **/
    private static class SortPair {

        /**
         * A figure.
         **/
        public final Figure _figure;

        /**
         * The relevant corner of the bounding rectangle of the
         * figure, may also be its center in some operation modes.
         **/
        public final Point _hotspot;

        /**
         * Groups the figure together with its "hotspot" which
         * is computed according to the specified operation mode.
         * @param op operation mode, influences the hotspot
         *           computation (one of the SpreadCommand
         *           constants LEFTS, CENTERS, RIGHTS, ...).
         * @param figure is stored unchanged as SortPair.figure,
         *               the hotspot is calculated from it.
         **/
        public SortPair(int op, Figure figure) {
            // In horizontal or vertical distance modes the figures
            // are sorted with regard to their upper left corners
            // (same computation as in LEFTS or TOPS mode).
            // Note that in these modes there are two relevant points
            // per figure (the two opposite sides), but I had to choose
            // one of them.
            // I did not choose the center as hot spot because this
            // leads to many divisions with truncation discrepancies.
            // However, the distribution algorithm depends on the
            // chosen hotspot location, so do not change it without 
            // adapting the computation.
            Point newHotspot = switch (op) {
                case LEFTS, TOPS, HORIZONTAL_DISTANCE, VERTICAL_DISTANCE ->
                    //case DIAGONAL_DISTANCE:
                    figure.displayBox().getLocation();
                case RIGHTS,
                    BOTTOMS -> new Point(
                        figure.displayBox().x + figure.displayBox().width,
                        figure.displayBox().y + figure.displayBox().height);
                case MIDDLES, CENTERS, DIAGONAL_CENTERS -> Geom.center(figure.displayBox());
                default -> null;
            };

            this._figure = figure;
            this._hotspot = newHotspot;
        }

        /**
         * Returns the square distance from the given point to my
         * "hotspot".
         **/
        public long distance2(Point origin) {
            // Perhaps it makes sense to cache popular results?
            // When sorting a list, the same distance may be needed
            // several times...
            return Geom.length2(origin.x, origin.y, _hotspot.x, _hotspot.y);
        }
    }

    /**
     * Used by <code>execute()</code> to order the figures
     * in a sorted list by their "hotspot" location.
     **/
    private static class HotspotComparator implements Comparator<SortPair> {

        /**
         * The mode to use (the horizontal/vertical
         * distinction is of relevance).
         **/
        private final int _op;

        /**
         * Creates a comparator which can compare two
         * <code>SortPair</code> objects with regard to
         * their "hotspots".
         *
         * @param op operation mode, the comparison depends
         *           on its horizontal/vertical information
         *           (one of the SpreadCommand constants
         *           LEFTS, CENTERS, RIGHTS, ...).
         **/
        public HotspotComparator(int op) {
            this._op = op;
        }

        /**
         * @return <0, if the second SortPair's hotspot is more left or above <br>
         *         =0, if both hotspots have the same x or y coordinate <br>
         *         >0, if the first SortPair's hotspot is more left or above <br>
         *         Horizontal or vertical mode is selected via the <code>op</code>
         *         parameter of the constructor.
         **/
        @Override
        public int compare(SortPair fst, SortPair snd) {
            return switch (_op) {
                case HORIZONTAL_DISTANCE, LEFTS, RIGHTS, CENTERS -> fst._hotspot.x - snd._hotspot.x;
                case VERTICAL_DISTANCE, TOPS, BOTTOMS, MIDDLES -> fst._hotspot.y - snd._hotspot.y;
                default -> throw new RuntimeException(
                    "SpreadCommand: Comparator: Illegal operation mode " + _op + "!");
            };

        }
    }

    /**
     * Used by <code>execute()</code> to order the figures
     * in a sorted list by their geometrical distance from
     * a given location.
     **/
    private static class DistanceComparator implements Comparator<SortPair> {

        /**
         * The point to measure all distances from.
         **/
        private final Point _origin;

        /**
         * Creates a comparator which can compare two
         * <code>SortPair</code> objects with regard to
         * their distance from a given point.
         *
         * @param origin point to measure the distance from
         **/
        public DistanceComparator(Point origin) {
            this._origin = origin;
        }

        /**
         * @return -1, if the second SortPair's hotspot is nearer to origin. <br>
         *         =0, if both SortPairs' hotspots are within equal distance. <br>
         *         >0, if the first SortPair's hotspot is nearer to origin.
         **/
        @Override
        public int compare(SortPair fst, SortPair snd) {
            long difference = snd.distance2(_origin) - fst.distance2(_origin);
            if (difference < 0) {
                return -1;
            } else if (difference > 0) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    /**
     * Used by <code>execute()</code> to order the figures
     * in a sorted list by the distance of their projection
     * on a line to the starting point of the line.
     *
     * <p>
     * The computation works as follows:
     * Let vl be the normalized gradient vector of the projection
     * line, consisting of (xl, yl), and let vn be a normalized
     * vector orthogonal to vl (this can be achieved by choosing
     * (xn=yl, yn=-xl) as components).
     * </p>
     * <p>
     * The vector vf from the origin of the line to the hotspot
     * of a figure to compare can be expressed as linear
     * combination of vl and vn.
     * <pre>
     * (eq.1)         vf = i * vl + j * vn
     * </pre>
     * The value i is a measure for the distance from the projection
     * point of the hotspot on the line to the origin of the line.
     * </p>
     * <p>
     * Eq.1 can be transformed into an equation system and resolved
     * to i, which leads to the following formula:
     * <pre>
     *                     xf*xl + yf*yl
     * (eq.2)          i = -------------
     *                      xl^2 + yl^2
     * </pre>
     * This formula is not usable if xl=yl=0, this case has to
     * be rejected when the comparator is constructed.
     * <p>
     * </p>
     * In the derivation of the formula, also yl=0 had to be excluded.
     * However, the formula produces usable results in that case.
     * So that case is not handled in any special way.
     * </p>
     **/
    private static class ProjectionComparator implements Comparator<SortPair> {

        /**
         * x component of the gradient triangle of the line.
         **/
        private final float _xL;

        /**
         * y component of the gradient triangle of the line.
         **/
        private final float _yL;

        /**
         * _xL^2 + _yL^2 (just to save computation time)
         **/
        private final float _denominator;

        /**
         * Creates a comparator which can compare two
         * <code>SortPair</code> objects with regard to
         * their projected distance from a given point.
         *
         * @param origin starting point of the projection line,
         *               also the point to measure the distances.
         * @param second end point of the projection line.
         *
         * @exception ArithmeticException
         *            if the given end points are too close,
         *            i.e. a projection line could not be computed.
         **/
        public ProjectionComparator(Point origin, Point second) throws ArithmeticException {
            float distance =
                (float) Math.sqrt(Geom.length2(origin.x, origin.y, second.x, second.y));
            if (distance == 0) {
                throw new ArithmeticException("Projection line end points are too near!");
            }

            this._xL = (second.x - origin.x) / distance;
            this._yL = (second.y - origin.y) / distance;
            this._denominator = _xL * _xL + _yL * _yL;
        }

        /**
         * @return -1, if i1 <  i2. <br>
         *          0, if i1 == i2. <br>
         *          1, if i1  > i2.
         *
         * @see SpreadCommand.ProjectionComparator Overview (description of i)
         **/
        @Override
        public int compare(SortPair fst, SortPair snd) {
            // As this computation will be repeated several times for
            // each SortPair, it could make sense to delegate the
            // computation to the SortPair and cache the result there.
            float i1 = fst._hotspot.x * _xL + fst._hotspot.y * _yL / _denominator;
            float i2 = snd._hotspot.x * _xL + snd._hotspot.y * _yL / _denominator;
            return Float.compare(i1, i2);
        }
    }

    /**
     * This filter lets pass all types of figures
     * except instances of <code>ConnectionFigure</code>.
     * This makes sense for the spread command because
     * connections produce contra-intuitive spread results.
     * And they cannot be moved on their own, anyway...
     **/
    private static class ConnectionFilter implements FigureFilter {

        /**
         * @return <code>false</code>, if the figure is a ConnectionFigure
         *       - <code>true</code>, otherwise.
         **/
        @Override
        public boolean isAcceptedFigure(Figure figure) {
            return !(figure instanceof ConnectionFigure);
        }
    }
}