/*
 *  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.causeway.commons.internal.assertions;

import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.commons.internal.primitives._Ints;
import org.apache.causeway.commons.internal.primitives._Longs;

import org.jspecify.annotations.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.log4j.Log4j2;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * A collection of commonly used constants.
 * </p>
 * <p>
 * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
 * These may be changed or removed without notice!
 * </p>
 *
 * @since 2.0
 */
@UtilityClass
@Log4j2
public final class _Assert {

    // -- TRUE

    public void assertTrue(final boolean condition) {
        assertTrue(condition, (String) null);
    }

    public void assertTrue(final boolean condition, final String message) {
        if (!condition) {
            fail(message, true, false);
        }
    }

    public void assertTrue(final boolean condition, final Supplier<String> lazyMessage) {
        if (!condition) {
            fail(lazyMessage.get(), true, false);
        }
    }

    // -- FALSE

    public void assertFalse(final boolean condition) {
        assertFalse(condition, (String) null);
    }

    public void assertFalse(final boolean condition, final String message) {
        if (condition) {
            fail(message, false, true);
        }
    }

    public void assertFalse(final boolean condition, final Supplier<String> lazyMessage) {
        if (condition) {
            fail(lazyMessage.get(), true, false);
        }
    }

    // -- NULL

    public void assertNull(final Object object) {
        assertNull(object, (String) null);
    }

    public void assertNull(final Object object, final String message) {
        if (object!=null) {
            fail(message, "null", "not null");
        }
    }

    public void assertNull(final Object object, final Supplier<String> lazyMessage) {
        if (object!=null) {
            fail(lazyMessage.get(), "null", "not null");
        }
    }

    // -- NOT NULL

    public void assertNotNull(final Object object) {
        assertNotNull(object, (String) null);
    }

    public void assertNotNull(final Object object, final String message) {
        if (object==null) {
            fail(message, "not null", "null");
        }
    }

    public void assertNotNull(final Object object, final Supplier<String> lazyMessage) {
        if (object==null) {
            fail(lazyMessage.get(), "not null", "null");
        }
    }

    // -- SAMENESS

    /**
     * <em>Assert</em> that {@code left} == {@code right}.
     * @return {@code left} if assertion holds
     */
    public <T> T assertSameObject(final T left, final Object right) {
        assertTrue(left == right);
        return left;
    }

    /**
     * <em>Assert</em> that {@code left} == {@code right}.
     * @return {@code left} if assertion holds
     */
    public <T> T assertSameObject(final T left, final Object right, final String msg) {
        assertTrue(left == right, msg);
        return left;
    }

    /**
     * <em>Assert</em> that {@code left} == {@code right}.
     * @return {@code left} if assertion holds
     */
    public <T> T assertSameObject(final T left, final Object right, final Supplier<String> lazyMessage) {
        assertTrue(left == right, lazyMessage);
        return left;
    }

    // -- EQUALITY

    /**
     * <em>Assert</em> that {@code expected} and {@code actual} are equal.
     * <p>If both are {@code null}, they are considered equal.
     *
     * @see Object#equals(Object)
     */
    public void assertEquals(final Object left, final Object right) {
        assertEquals(left, right, (String) null);
    }

    /**
     * <em>Assert</em> that {@code expected} and {@code actual} are equal.
     * <p>If both are {@code null}, they are considered equal.
     * <p>Fails with the supplied failure {@code message}.
     *
     * @see Object#equals(Object)
     */
    public void assertEquals(final Object left, final Object right, final String message) {
        if (!Objects.equals(left, right)) {
            fail(message, left, right);
        }
    }

    public void assertEquals(final Object left, final Object right, final Supplier<String> lazyMessage) {
        if (!Objects.equals(left, right)) {
            fail(lazyMessage.get(), left, right);
        }
    }

    // -- RANGE CHECKS

    public void assertRangeContains(final _Ints.Range range, final int value, final String message) {
        if(!range.contains(value)) {
            fail(message, range.toString(), value);
        }
    }

    public void assertRangeContains(final _Ints.Range range, final int value, final Supplier<String> lazyMessage) {
        if(!range.contains(value)) {
            fail(lazyMessage.get(), range.toString(), value);
        }
    }

    public void assertRangeContains(final _Longs.Range range, final long value, final String message) {
        if(!range.contains(value)) {
            fail(message, range.toString(), value);
        }
    }

    public void assertRangeContains(final _Longs.Range range, final long value, final Supplier<String> lazyMessage) {
        if(!range.contains(value)) {
            fail(lazyMessage.get(), range.toString(), value);
        }
    }

    // -- STRING CHECKS

    public void assertNotEmpty(final String s) {
        assertNotEmpty(s, (String) null);
    }

    public void assertNotEmpty(final String s, final String message) {
        if(_NullSafe.isEmpty(s)) {
            fail(message, "not empty", "empty");
        }
    }

    public void assertNotEmpty(final String s, final Supplier<String> lazyMessage) {
        if(_NullSafe.isEmpty(s)) {
            fail(lazyMessage.get(), "not empty", "empty");
        }
    }

    // -- TYPE INSTANCE OF

    /**
     * Asserts, that given {@code type} is an instance of {@code requiredType}.
     */
    public void assertTypeIsInstanceOf(final @NonNull Class<?> type, final @NonNull Class<?> requiredType) {
        if(!requiredType.isAssignableFrom(type))
			throw _Exceptions.assertionError(
                    "unexpected type: '%s' is not an instance of '%s' ", ""+type, ""+requiredType);
    }

    /**
     * Asserts, that given null-able {@code pojo} is an instance of {@code requiredType}.
     * <p>
     * If {@code pojo} is {@code null}, the assertion is always satisfied.
     */
    public void assertNullableObjectIsInstanceOf(final @Nullable Object pojo, final @NonNull Class<?> requiredType) {
        if(pojo==null) return; // skip check
        assertTypeIsInstanceOf(pojo.getClass(), requiredType);
    }

    // -- NUMBERS

    /**
     * BigDecimal equality by value.
     */
    public void assertNumberEquals(final @Nullable BigDecimal a, final @Nullable BigDecimal b) {
        assertNumberEquals(a, b, ()->"");
    }

    /**
     * BigDecimal equality by value.
     */
    public void assertNumberEquals(final @Nullable BigDecimal a, final @Nullable BigDecimal b,
            final Supplier<String> lazyMessage) {
        if(a==null || b==null) {
            assertEquals(a, b, lazyMessage);
            return;
        }
        var maxScale = Math.max(a.scale(), b.scale());
        assertEquals(
                a.setScale(maxScale),
                b.setScale(maxScale),
                lazyMessage);
    }

    /**
     * double equality by value relaxed by delta
     */
    public void assertNumberEquals(final double a, final double b, final double delta) {
        assertNumberEquals(a, b, delta, ()->"");
    }

    /**
     * double equality by value relaxed by delta
     */
    public void assertNumberEquals(final double a, final double b, final double delta,
            final Supplier<String> lazyMessage) {
        if (Double.isNaN(delta) || delta < 0.0)
			throw _Exceptions.assertionError(
                    "%sinvalid delta: '%s'",
                    _Strings.nonEmpty(lazyMessage.get()).map(msg->msg + ": ").orElse(""),
                    String.valueOf(delta));
        if((Double.doubleToLongBits(a) != Double.doubleToLongBits(b))
                && Math.abs(a - b) > delta)
			throw _Exceptions.assertionError(
                    "%snumbers %s and %s are not equal within delta %s",
                    _Strings.nonEmpty(lazyMessage.get()).map(msg->msg + ": ").orElse(""),
                    String.valueOf(a), String.valueOf(b), String.valueOf(delta));
    }

    /**
     * Generic number equality check, use with caution!
     */
    public void assertNumberEqualsUnsafe(final @Nullable Number a, final @Nullable Number b, final double delta) {
        assertNumberEqualsUnsafe(a, b, delta, ()->"");
    }

    /**
     * Generic number equality check, use with caution!
     */
    public void assertNumberEqualsUnsafe(final @Nullable Number a, final @Nullable Number b, final double delta,
            final Supplier<String> lazyMessage) {
        if(a==null || b==null) {
            assertEquals(a, b, lazyMessage);
            return;
        }
        assertNumberEquals(a.doubleValue(), b.doubleValue(), delta, lazyMessage);
    }

    // -- HELPER

    private static void fail(final String message, final Object expected, final Object actual) {
        var error = _Exceptions.assertionError(
                buildPrefix(message)
                + String.format("expected: <%s> but was: <%s>", ""+expected, ""+actual));
        log.error(error); // in case exceptions get swallowed, make sure errors at least get logged
        throw error;
    }
    private static String buildPrefix(final String message) {
        return _Strings.isNotEmpty(message) ? message + " ==> " : "";
    }

}
