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

import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;

/** Implementation of the {@link Transform} interface for spherical 2D points.
 *
 * <p>This class uses an {@link AffineTransformMatrix3D} to perform spherical point transforms
 * in Euclidean 3D space.</p>
 *
 * <p>Instances of this class are guaranteed to be immutable.</p>
 */
public final class Transform2S implements Transform<Point2S> {
    /** Static instance representing the identity transform. */
    private static final Transform2S IDENTITY = new Transform2S(AffineTransformMatrix3D.identity());

    /** Static transform instance that reflects across the x-y plane. */
    private static final AffineTransformMatrix3D XY_PLANE_REFLECTION = AffineTransformMatrix3D.createScale(1, 1, -1);

    /** Euclidean transform matrix underlying the spherical transform. */
    private final AffineTransformMatrix3D euclideanTransform;

    /** Construct a new instance from its underlying Euclidean transform.
     * @param euclideanTransform underlying Euclidean transform
     */
    private Transform2S(final AffineTransformMatrix3D euclideanTransform) {
        this.euclideanTransform = euclideanTransform;
    }

    /** Get the Euclidean transform matrix underlying the spherical transform.
     * @return the Euclidean transform matrix underlying the spherical transform
     */
    public AffineTransformMatrix3D getEuclideanTransform() {
        return euclideanTransform;
    }

    /** {@inheritDoc} */
    @Override
    public Point2S apply(final Point2S pt) {
        final Vector3D vec = pt.getVector();
        return Point2S.from(euclideanTransform.apply(vec));
    }

    /** {@inheritDoc} */
    @Override
    public boolean preservesOrientation() {
        return euclideanTransform.preservesOrientation();
    }

    /** {@inheritDoc} */
    @Override
    public Transform2S inverse() {
        return new Transform2S(euclideanTransform.inverse());
    }

    /** Apply a rotation of {@code angle} radians around the given point to this instance.
     * @param pt point to rotate around
     * @param angle rotation angle in radians
     * @return transform resulting from applying the specified rotation to this instance
     */
    public Transform2S rotate(final Point2S pt, final double angle) {
        return premultiply(createRotation(pt, angle));
    }

    /** Apply a rotation of {@code angle} radians around the given 3D axis to this instance.
     * @param axis 3D axis of rotation
     * @param angle rotation angle in radians
     * @return transform resulting from applying the specified rotation to this instance
     */
    public Transform2S rotate(final Vector3D axis, final double angle) {
        return premultiply(createRotation(axis, angle));
    }

    /** Apply the given quaternion rotation to this instance.
     * @param quaternion quaternion rotation to apply
     * @return transform resulting from applying the specified rotation to this instance
     */
    public Transform2S rotate(final QuaternionRotation quaternion) {
        return premultiply(createRotation(quaternion));
    }

    /** Apply a reflection across the equatorial plane defined by the given pole point
     * to this instance.
     * @param pole pole point defining the equatorial reflection plane
     * @return transform resulting from applying the specified reflection to this instance
     */
    public Transform2S reflect(final Point2S pole) {
        return premultiply(createReflection(pole));
    }

    /** Apply a reflection across the equatorial plane defined by the given pole vector
     * to this instance.
     * @param poleVector pole vector defining the equatorial reflection plane
     * @return transform resulting from applying the specified reflection to this instance
     */
    public Transform2S reflect(final Vector3D poleVector) {
        return premultiply(createReflection(poleVector));
    }

    /** Multiply the underlying Euclidean transform of this instance by that of the argument, eg,
     * {@code other * this}. The returned transform performs the equivalent of
     * {@code other} followed by {@code this}.
     * @param other transform to multiply with
     * @return a new transform computed by multiplying the matrix of this
     *      instance by that of the argument
     * @see AffineTransformMatrix3D#multiply(AffineTransformMatrix3D)
     */
    public Transform2S multiply(final Transform2S other) {
        return multiply(this, other);
    }

    /** Multiply the underlying Euclidean transform matrix of the argument by that of this instance, eg,
     * {@code this * other}. The returned transform performs the equivalent of {@code this}
     * followed by {@code other}.
     * @param other transform to multiply with
     * @return a new transform computed by multiplying the matrix of the
     *      argument by that of this instance
     * @see AffineTransformMatrix3D#premultiply(AffineTransformMatrix3D)
     */
    public Transform2S premultiply(final Transform2S other) {
        return multiply(other, this);
    }

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

    /**
     * Return true if the given object is an instance of {@link Transform2S}
     * and the underlying Euclidean transform matrices are exactly equal.
     * @param obj object to test for equality with the current instance
     * @return true if the underlying transform matrices are exactly equal
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Transform2S)) {
            return false;
        }
        final Transform2S other = (Transform2S) obj;

        return euclideanTransform.equals(other.euclideanTransform);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();

        sb.append(this.getClass().getSimpleName())
            .append("[euclideanTransform= ")
            .append(getEuclideanTransform())
            .append("]");

        return sb.toString();
    }

    /** Return an instance representing the identity transform. This transform is guaranteed
     * to return an <em>equivalent</em> (ie, co-located) point for any input point. However, the
     * points are not guaranteed to contain exactly equal coordinates. For example, at the poles, an
     * infinite number of points exist that vary only in the azimuth coordinate. When one of these
     * points is transformed by this identity transform, the returned point may contain a different
     * azimuth value from the input, but it will still represent the same location in space.
     * @return an instance representing the identity transform
     */
    public static Transform2S identity() {
        return IDENTITY;
    }

    /** Create a transform that rotates the given angle around {@code pt}.
     * @param pt point to rotate around
     * @param angle angle of rotation in radians
     * @return a transform that rotates the given angle around {@code pt}
     */
    public static Transform2S createRotation(final Point2S pt, final double angle) {
        return createRotation(pt.getVector(), angle);
    }

    /** Create a transform that rotates the given angle around {@code axis}.
     * @param axis 3D axis of rotation
     * @param angle angle of rotation in radians
     * @return a transform that rotates the given angle {@code axis}
     */
    public static Transform2S createRotation(final Vector3D axis, final double angle) {
        return createRotation(QuaternionRotation.fromAxisAngle(axis, angle));
    }

    /** Create a transform that performs the given 3D rotation.
     * @param quaternion quaternion instance representing the 3D rotation
     * @return a transform that performs the given 3D rotation
     */
    public static Transform2S createRotation(final QuaternionRotation quaternion) {
        return new Transform2S(quaternion.toMatrix());
    }

    /** Create a transform that performs a reflection across the equatorial plane
     * defined by the given pole point.
     * @param pole pole point defining the equatorial reflection plane
     * @return a transform that performs a reflection across the equatorial plane
     *      defined by the given pole point
     */
    public static Transform2S createReflection(final Point2S pole) {
        return createReflection(pole.getVector());
    }

    /** Create a transform that performs a reflection across the equatorial plane
     * defined by the given pole point.
     * @param poleVector pole vector defining the equatorial reflection plane
     * @return a transform that performs a reflection across the equatorial plane
     *      defined by the given pole point
     */
    public static Transform2S createReflection(final Vector3D poleVector) {
        final QuaternionRotation quat = QuaternionRotation.createVectorRotation(poleVector, Vector3D.Unit.PLUS_Z);

        final AffineTransformMatrix3D matrix = quat.toMatrix()
                .premultiply(XY_PLANE_REFLECTION)
                .premultiply(quat.inverse().toMatrix());

        return new Transform2S(matrix);
    }

    /** Multiply the Euclidean transform matrices of the arguments together.
     * @param a first transform
     * @param b second transform
     * @return the transform computed as {@code a x b}
     */
    private static Transform2S multiply(final Transform2S a, final Transform2S b) {

        return new Transform2S(a.euclideanTransform.multiply(b.euclideanTransform));
    }
}
