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

import org.apache.commons.geometry.core.Transform;

/** Implementation of the {@link Transform} interface for spherical 1D points.
 *
 * <p>Similar to the Euclidean 1D
 * {@link org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D AffineTransformMatrix1D},
 * this class performs transformations using an internal 1D affine transformation matrix. In the
 * Euclidean case, the matrix contains a scale factor and a translation. Here, the matrix contains
 * a scale/negation factor that takes the values -1 or +1, and a rotation value. This restriction on
 * the allowed values in the matrix is required in order to fulfill the geometric requirements
 * of the {@link Transform} interface. For example, if arbitrary scaling is allowed, the point {@code 0.5pi}
 * could be scaled by 4 to {@code 2pi}, which is equivalent to {@code 0pi}. However, if the inverse scaling
 * of {@code 1/4} is applied to {@code 0pi}, the result is {@code 0pi} and not {@code 0.5pi}. This breaks
 * the {@link Transform} requirement that transforms be inversible.
 * </p>
 *
 * <p>Instances of this class are guaranteed to be immutable.</p>
 */
public final class Transform1S implements Transform<Point1S> {
    /** Static instance representing the identity transform. */
    private static final Transform1S IDENTITY = new Transform1S(1, 0);

    /** Static instance that negates azimuth values. */
    private static final Transform1S NEGATION = new Transform1S(-1, 0);

    /** Value to scale the point azimuth by. This will only be +1/-1. */
    private final double scale;

    /** Value to rotate the point azimuth by. */
    private final double rotate;

    /** Construct a new instance from its transform components.
     * @param scale scale value for the transform; must only be +1 or -1
     * @param rotate rotation value
     */
    private Transform1S(final double scale, final double rotate) {
        this.scale = scale;
        this.rotate = rotate;
    }

    /** Return true if the transform negates the azimuth values of transformed
     * points, regardless of any rotation applied subsequently.
     * @return true if the transform negates the azimuth values of transformed
     *      points
     * @see #preservesOrientation()
     */
    public boolean isNegation() {
        return scale <= 0;
    }

    /** Get the rotation value applied by this instance, in radians.
     * @return the rotation value applied by this instance, in radians.
     */
    public double getRotation() {
        return rotate;
    }

    /** {@inheritDoc} */
    @Override
    public Point1S apply(final Point1S pt) {
        final double az = pt.getAzimuth();
        final double resultAz = (az * scale) + rotate;

        return Point1S.of(resultAz);
    }

    /** {@inheritDoc} */
    @Override
    public boolean preservesOrientation() {
        return !isNegation();
    }

    /** Return a new transform created by pre-multiplying this instance by a transform
     * producing a rotation with the given angle.
     * @param angle angle to rotate, in radians
     * @return a new transform created by pre-multiplying this instance by a transform
     *      producing a rotation with the given angle
     * @see #createRotation(double)
     */
    public Transform1S rotate(final double angle) {
        return premultiply(createRotation(angle));
    }

    /** Return a new transform created by pre-multiplying this instance by a transform
     * that negates azimuth values.
     * @return a new transform created by pre-multiplying this instance by a transform
     *      that negates azimuth values
     */
    public Transform1S negate() {
        return premultiply(createNegation());
    }

    /** Multiply the underlying matrix 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
     */
    public Transform1S multiply(final Transform1S other) {
        return multiply(this, other);
    }

    /** Multiply the underlying 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
     */
    public Transform1S premultiply(final Transform1S other) {
        return multiply(other, this);
    }

    /** {@inheritDoc} */
    @Override
    public Transform1S inverse() {
        final double invScale = 1.0 / scale;

        final double resultRotate = -(rotate * invScale);

        return new Transform1S(invScale, resultRotate);
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;

        result = (result * prime) + Double.hashCode(scale);
        result = (result * prime) + Double.hashCode(rotate);

        return result;
    }

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

        return Double.compare(scale, other.scale) == 0 &&
                Double.compare(rotate, other.rotate) == 0;
    }

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

        sb.append(this.getClass().getSimpleName())
            .append("[negate= ")
            .append(isNegation())
            .append(", rotate= ")
            .append(getRotation())
            .append("]");

        return sb.toString();
    }

    /** Return a transform instance representing the identity transform.
     * @return a transform instance representing the identity transform
     */
    public static Transform1S identity() {
        return IDENTITY;
    }

    /** Return a transform instance that negates azimuth values.
     * @return a transform instance that negates azimuth values.
     */
    public static Transform1S createNegation() {
        return NEGATION;
    }

    /** Return a transform instance that performs a rotation with the given
     * angle.
     * @param angle angle of the rotation, in radians
     * @return a transform instance that performs a rotation with the given
     *      angle
     */
    public static Transform1S createRotation(final double angle) {
        return new Transform1S(1, angle);
    }

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

        // calculate the matrix elements
        final double resultScale = a.scale * b.scale;
        final double resultRotate = (a.scale * b.rotate) + a.rotate;

        return new Transform1S(resultScale, resultRotate);
    }
}
