/*
 * 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.rotation;

import org.apache.commons.geometry.euclidean.EuclideanTransform;
import org.apache.commons.geometry.euclidean.internal.Vectors;
import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;

/** Class representing a rotation in 2 dimensional Euclidean space. Positive
 * rotations are in a <em>counter-clockwise</em> direction.
 */
public final class Rotation2D implements EuclideanTransform<Vector2D> {

    /** Instance representing a rotation of zero radians. */
    private static final Rotation2D IDENTITY = new Rotation2D(0);

    /** The angle of the rotation in radians. */
    private final double angle;

    /** The cosine of the angle of rotation, cached to avoid repeated computation. */
    private final double cosAngle;

    /** The sine of the angle of rotation, cached to avoid repeated computation. */
    private final double sinAngle;

    /** Create a new instance representing the given angle.
     * @param angle the angle of rotation, in radians
     */
    private Rotation2D(final double angle) {
        this.angle = angle;
        this.cosAngle = Math.cos(angle);
        this.sinAngle = Math.sin(angle);
    }

    /** Get the angle of rotation in radians.
     * @return the angle of rotation in radians
     */
    public double getAngle() {
        return angle;
    }

    /** {@inheritDoc} */
    @Override
    public Rotation2D inverse() {
        return new Rotation2D(-angle);
    }

    /** {@inheritDoc}
     *
     * <p>This method simply returns true since rotations always preserve the orientation
     * of the space.</p>
     */
    @Override
    public boolean preservesOrientation() {
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public Vector2D apply(final Vector2D pt) {
        final double x = pt.getX();
        final double y = pt.getY();

        return Vector2D.of(
                    (x * cosAngle) - (y * sinAngle),
                    (x * sinAngle) + (y * cosAngle)
                );
    }

    /** {@inheritDoc}
     *
     * <p>This method simply calls {@code apply(vec)} since rotations treat
     * points and vectors similarly.</p>
     * */
    @Override
    public Vector2D applyVector(final Vector2D vec) {
        return apply(vec);
    }

    /** Return an {@link AffineTransformMatrix2D} representing the same rotation
     * as this instance.
     * @return a transform matrix representing the same rotation
     */
    public AffineTransformMatrix2D toMatrix() {
        return AffineTransformMatrix2D.of(
                    cosAngle, -sinAngle, 0.0,
                    sinAngle, cosAngle, 0.0
                );
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return Double.hashCode(angle);
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Rotation2D)) {
            return false;
        }

        final Rotation2D other = (Rotation2D) obj;

        return Double.compare(this.angle, other.angle) == 0;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append(this.getClass().getSimpleName())
            .append("[angle=")
            .append(angle)
            .append(']');

        return sb.toString();
    }

    /** Create a new instance with the given angle of rotation.
     * @param angle the angle of rotation in radians
     * @return a new instance with the given angle of rotation
     */
    public static Rotation2D of(final double angle) {
        return new Rotation2D(angle);
    }

    /** Return an instance representing the identity rotation, ie a rotation
     * of zero radians.
     * @return an instance representing a rotation of zero radians
     */
    public static Rotation2D identity() {
        return IDENTITY;
    }

    /** Create a rotation instance that rotates the vector {@code u} to point in the direction of
     * vector {@code v}.
     * @param u input vector
     * @param v target vector
     * @return a rotation instance that rotates {@code u} to point in the direction of {@code v}
     * @throws IllegalArgumentException if either vector cannot be normalized
     */
    public static Rotation2D createVectorRotation(final Vector2D u, final Vector2D v) {
        // make sure that the vectors are real-valued and of non-zero length; we don't
        // actually need to use the norm value; we just need to check its properties
        Vectors.checkedNorm(u);
        Vectors.checkedNorm(v);

        final double uAzimuth = Math.atan2(u.getY(), u.getX());
        final double vAzimuth = Math.atan2(v.getY(), v.getX());

        return of(vAzimuth - uAzimuth);
    }
}
