/*
 * Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/**
 * @test
 * @bug 4987374
 * @summary Unit test for inversion methods:
 *
 *          AffineTransform.createInverse();
 *          AffineTransform.invert();
 *
 * @author flar
 * @run main TestInvertMethods
 */

import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;

/*
 * Instances of the inner class Tester are "nodes" which take an input
 * AffineTransform (AT), modify it in some way and pass the modified
 * AT onto another Tester node.
 *
 * There is one particular Tester node of note called theVerifier.
 * This is a leaf node which takes the input AT and tests the various
 * inversion methods on that matrix.
 *
 * Most of the other Tester nodes will perform a single affine operation
 * on their input, such as a rotate by various angles, or a scale by
 * various predefined scale  values, and then pass the modified AT on
 * to the next node in the chain which may be a verifier or another
 * modifier.
 *
 * The Tester instances can also be chained together using the chain
 * method so that we can test not only matrices modified by some single
 * affine operation (scale, rotate, etc.) but also composite matrices
 * that represent multiple operations concatenated together.
 */
public class TestInvertMethods {
    public static boolean verbose;

    public static final double MAX_ULPS = 2.0;
    public static double MAX_TX_ULPS = MAX_ULPS;
    public static double maxulps = 0.0;
    public static double maxtxulps = 0.0;
    public static int numtests = 0;

    public static void main(String argv[]) {
        Tester rotate = new Tester.Rotate();
        Tester scale = new Tester.Scale();
        Tester shear = new Tester.Shear();
        Tester translate = new Tester.Translate();

        if (argv.length > 1) {
            // This next line verifies that chaining works correctly...
            scale.chain(translate.chain(new Tester.Debug())).test(false);
            return;
        }

        verbose = (argv.length > 0);

        new Tester.Identity().test(true);
        translate.test(true);
        scale.test(true);
        rotate.test(true);
        shear.test(true);
        scale.chain(translate).test(true);
        rotate.chain(translate).test(true);
        shear.chain(translate).test(true);
        translate.chain(scale).test(true);
        translate.chain(rotate).test(true);
        translate.chain(shear).test(true);
        translate.chain(scale.chain(rotate.chain(shear))).test(false);
        shear.chain(rotate.chain(scale.chain(translate))).test(false);

        System.out.println(numtests+" tests performed");
        System.out.println("Max scale and shear difference: "+maxulps+" ulps");
        System.out.println("Max translate difference: "+maxtxulps+" ulps");
    }

    public abstract static class Tester {
        public static AffineTransform IdentityTx = new AffineTransform();

        /*
         * This is the leaf node that performs inversion testing
         * on the incoming AffineTransform.
         */
        public static final Tester theVerifier = new Tester() {
            public void test(AffineTransform at, boolean full) {
                numtests++;
                AffineTransform inv1, inv2;
                boolean isinvertible =
                    (Math.abs(at.getDeterminant()) >= Double.MIN_VALUE);
                try {
                    inv1 = at.createInverse();
                    if (!isinvertible) missingNTE("createInverse", at);
                } catch (NoninvertibleTransformException e) {
                    inv1 = null;
                    if (isinvertible) extraNTE("createInverse", at);
                }
                inv2 = new AffineTransform(at);
                try {
                    inv2.invert();
                    if (!isinvertible) missingNTE("invert", at);
                } catch (NoninvertibleTransformException e) {
                    if (isinvertible) extraNTE("invert", at);
                }
                if (verbose) System.out.println("at = "+at);
                if (isinvertible) {
                    if (verbose) System.out.println(" inv1 = "+inv1);
                    if (verbose) System.out.println(" inv2 = "+inv2);
                    if (!inv1.equals(inv2)) {
                        report(at, inv1, inv2,
                               "invert methods do not agree");
                    }
                    inv1.concatenate(at);
                    inv2.concatenate(at);
                    // "Fix" some values that don't always behave
                    // well with all the math that we've done up
                    // to this point.
                    // See the note on the concatfix method below.
                    concatfix(inv1);
                    concatfix(inv2);
                    if (verbose) System.out.println("  at*inv1 = "+inv1);
                    if (verbose) System.out.println("  at*inv2 = "+inv2);
                    if (!compare(inv1, IdentityTx)) {
                        report(at, inv1, IdentityTx,
                               "createInverse() check failed");
                    }
                    if (!compare(inv2, IdentityTx)) {
                        report(at, inv2, IdentityTx,
                               "invert() check failed");
                    }
                } else {
                    if (verbose) System.out.println(" is not invertible");
                }
                if (verbose) System.out.println();
            }

            void missingNTE(String methodname, AffineTransform at) {
                throw new RuntimeException("Noninvertible was not "+
                                           "thrown from "+methodname+
                                           " for: "+at);
            }

            void extraNTE(String methodname, AffineTransform at) {
                throw new RuntimeException("Unexpected Noninvertible "+
                                           "thrown from "+methodname+
                                           " for: "+at);
            }
        };

        /*
         * The inversion math may work out fairly exactly, but when
         * we concatenate the inversions back with the original matrix
         * in an attempt to restore them to the identity matrix,
         * then we can end up compounding errors to a fairly high
         * level, particularly if the component values had mantissas
         * that were repeating fractions.  This function therefore
         * "fixes" the results of concatenating the inversions back
         * with their original matrices to get rid of small variations
         * in the values that should have ended up being 0.0.
         */
        public void concatfix(AffineTransform at) {
            double m00 = at.getScaleX();
            double m10 = at.getShearY();
            double m01 = at.getShearX();
            double m11 = at.getScaleY();
            double m02 = at.getTranslateX();
            double m12 = at.getTranslateY();
            if (Math.abs(m02) < 1E-10) m02 = 0.0;
            if (Math.abs(m12) < 1E-10) m12 = 0.0;
            if (Math.abs(m01) < 1E-15) m01 = 0.0;
            if (Math.abs(m10) < 1E-15) m10 = 0.0;
            at.setTransform(m00, m10,
                            m01, m11,
                            m02, m12);
        }

        public void test(boolean full) {
            test(IdentityTx, full);
        }

        public void test(AffineTransform init, boolean full) {
            test(init, theVerifier, full);
        }

        public void test(AffineTransform init, Tester next, boolean full) {
            next.test(init, full);
        }

        public Tester chain(Tester next) {
            return new Chain(this, next);
        }

        /*
         * Utility node used to chain together two other nodes for
         * implementing the "chain" method.
         */
        public static class Chain extends Tester {
            Tester prev;
            Tester next;

            public Chain(Tester prev, Tester next) {
                this.prev = prev;
                this.next = next;
            }

            public void test(AffineTransform init, boolean full) {
                prev.test(init, next, full);
            }

            public Tester chain(Tester next) {
                this.next = this.next.chain(next);
                return this;
            }
        }

        /*
         * Utility node for testing.
         */
        public static class Fail extends Tester {
            public void test(AffineTransform init, Tester next, boolean full) {
                throw new RuntimeException("Debug: Forcing failure");
            }
        }

        /*
         * Utility node for testing that chaining works.
         */
        public static class Debug extends Tester {
            public void test(AffineTransform init, Tester next, boolean full) {
                new Throwable().printStackTrace();
                next.test(init, full);
            }
        }

        /*
         * NOP node.
         */
        public static class Identity extends Tester {
            public void test(AffineTransform init, Tester next, boolean full) {
                if (verbose) System.out.println("*Identity = "+init);
                next.test(init, full);
            }
        }

        /*
         * Affine rotation node.
         */
        public static class Rotate extends Tester {
            public void test(AffineTransform init, Tester next, boolean full) {
                int inc = full ? 10 : 45;
                for (int i = -720; i <= 720; i += inc) {
                    AffineTransform at2 = new AffineTransform(init);
                    at2.rotate(Math.toRadians(i));
                    if (verbose) System.out.println("*Rotate("+i+") = "+at2);
                    next.test(at2, full);
                }
            }
        }

        public static final double SMALL_VALUE = .0001;
        public static final double LARGE_VALUE = 10000;

        /*
         * Affine scale node.
         */
        public static class Scale extends Tester {
            public double fullvals[] = {
                // Noninvertibles
                0.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,

                // Invertibles
                SMALL_VALUE, SMALL_VALUE,
                SMALL_VALUE, 1.0,
                1.0, SMALL_VALUE,

                SMALL_VALUE, LARGE_VALUE,
                LARGE_VALUE, SMALL_VALUE,

                LARGE_VALUE, LARGE_VALUE,
                LARGE_VALUE, 1.0,
                1.0, LARGE_VALUE,

                0.5, 0.5,
                1.0, 1.0,
                2.0, 2.0,
                Math.PI, Math.E,
            };
            public double abbrevvals[] = {
                0.0, 0.0,
                1.0, 1.0,
                2.0, 3.0,
            };

            public void test(AffineTransform init, Tester next, boolean full) {
                double scales[] = (full ? fullvals : abbrevvals);
                for (int i = 0; i < scales.length; i += 2) {
                    AffineTransform at2 = new AffineTransform(init);
                    at2.scale(scales[i], scales[i+1]);
                    if (verbose) System.out.println("*Scale("+scales[i]+", "+
                                                    scales[i+1]+") = "+at2);
                    next.test(at2, full);
                }
            }
        }

        /*
         * Affine shear node.
         */
        public static class Shear extends Tester {
            public double fullvals[] = {
                0.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,

                // Noninvertible
                1.0, 1.0,

                SMALL_VALUE, SMALL_VALUE,
                SMALL_VALUE, LARGE_VALUE,
                LARGE_VALUE, SMALL_VALUE,
                LARGE_VALUE, LARGE_VALUE,

                Math.PI, Math.E,
            };
            public double abbrevvals[] = {
                0.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,

                // Noninvertible
                1.0, 1.0,
            };

            public void test(AffineTransform init, Tester next, boolean full) {
                double shears[] = (full ? fullvals : abbrevvals);
                for (int i = 0; i < shears.length; i += 2) {
                    AffineTransform at2 = new AffineTransform(init);
                    at2.shear(shears[i], shears[i+1]);
                    if (verbose) System.out.println("*Shear("+shears[i]+", "+
                                                    shears[i+1]+") = "+at2);
                    next.test(at2, full);
                }
            }
        }

        /*
         * Affine translate node.
         */
        public static class Translate extends Tester {
            public double fullvals[] = {
                0.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,

                SMALL_VALUE, SMALL_VALUE,
                SMALL_VALUE, LARGE_VALUE,
                LARGE_VALUE, SMALL_VALUE,
                LARGE_VALUE, LARGE_VALUE,

                Math.PI, Math.E,
            };
            public double abbrevvals[] = {
                0.0, 0.0,
                0.0, 1.0,
                1.0, 0.0,
                Math.PI, Math.E,
            };

            public void test(AffineTransform init, Tester next, boolean full) {
                double translates[] = (full ? fullvals : abbrevvals);
                for (int i = 0; i < translates.length; i += 2) {
                    AffineTransform at2 = new AffineTransform(init);
                    at2.translate(translates[i], translates[i+1]);
                    if (verbose) System.out.println("*Translate("+
                                                    translates[i]+", "+
                                                    translates[i+1]+") = "+at2);
                    next.test(at2, full);
                }
            }
        }
    }

    public static void report(AffineTransform orig,
                              AffineTransform at1, AffineTransform at2,
                              String message)
    {
        System.out.println(orig+", type = "+orig.getType());
        System.out.println(at1+", type = "+at1.getType());
        System.out.println(at2+", type = "+at2.getType());
        System.out.println("ScaleX values differ by "+
                           ulps(at1.getScaleX(),
                                at2.getScaleX())+" ulps");
        System.out.println("ScaleY values differ by "+
                           ulps(at1.getScaleY(),
                                at2.getScaleY())+" ulps");
        System.out.println("ShearX values differ by "+
                           ulps(at1.getShearX(),
                                at2.getShearX())+" ulps");
        System.out.println("ShearY values differ by "+
                           ulps(at1.getShearY(),
                                at2.getShearY())+" ulps");
        System.out.println("TranslateX values differ by "+
                           ulps(at1.getTranslateX(),
                                at2.getTranslateX())+" ulps");
        System.out.println("TranslateY values differ by "+
                           ulps(at1.getTranslateY(),
                                at2.getTranslateY())+" ulps");
        throw new RuntimeException(message);
    }

    public static boolean compare(AffineTransform at1, AffineTransform at2) {
        maxulps = Math.max(maxulps, ulps(at1.getScaleX(), at2.getScaleX()));
        maxulps = Math.max(maxulps, ulps(at1.getScaleY(), at2.getScaleY()));
        maxulps = Math.max(maxulps, ulps(at1.getShearX(), at2.getShearX()));
        maxulps = Math.max(maxulps, ulps(at1.getShearY(), at2.getShearY()));
        maxtxulps = Math.max(maxtxulps,
                             ulps(at1.getTranslateX(), at2.getTranslateX()));
        maxtxulps = Math.max(maxtxulps,
                             ulps(at1.getTranslateY(), at2.getTranslateY()));
        return (getModifiedType(at1) == getModifiedType(at2) &&
                (compare(at1.getScaleX(), at2.getScaleX(), MAX_ULPS)) &&
                (compare(at1.getScaleY(), at2.getScaleY(), MAX_ULPS)) &&
                (compare(at1.getShearX(), at2.getShearX(), MAX_ULPS)) &&
                (compare(at1.getShearY(), at2.getShearY(), MAX_ULPS)) &&
                (compare(at1.getTranslateX(),
                         at2.getTranslateX(), MAX_TX_ULPS)) &&
                (compare(at1.getTranslateY(),
                         at2.getTranslateY(), MAX_TX_ULPS)));
    }

    public static final int ANY_SCALE_MASK =
        (AffineTransform.TYPE_UNIFORM_SCALE |
         AffineTransform.TYPE_GENERAL_SCALE);
    public static int getModifiedType(AffineTransform at) {
        int type = at.getType();
        // Some of the vector methods can introduce a tiny uniform scale
        // at some angles...
        if ((type & ANY_SCALE_MASK) != 0) {
            maxulps = Math.max(maxulps, ulps(at.getDeterminant(), 1.0));
            if (ulps(at.getDeterminant(), 1.0) <= MAX_ULPS) {
                // Really tiny - we will ignore it
                type &= ~ ANY_SCALE_MASK;
            }
        }
        return type;
    }

    public static boolean compare(double val1, double val2, double maxulps) {
        if (Math.abs(val1 - val2) < 1E-15) return true;
        return (ulps(val1, val2) <= maxulps);
    }

    public static double ulps(double val1, double val2) {
        double diff = Math.abs(val1 - val2);
        double ulpmax = Math.min(Math.ulp(val1), Math.ulp(val2));
        return (diff / ulpmax);
    }
}
