/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed 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 rx.exceptions;

import java.io.*;
import java.util.*;

import rx.plugins.*;

/**
 * Represents a {@code Throwable} that an {@code Observable} might notify its subscribers of, but that then can
 * be handled by an operator that is designed to recover from or react appropriately to such an error. You can
 * recover more information from an {@code OnErrorThrowable} than is found in a typical {@code Throwable}, such
 * as the item the {@code Observable} was trying to emit at the time the error was encountered.
 */
public final class OnErrorThrowable extends RuntimeException {

    private static final long serialVersionUID = -569558213262703934L;

    private final boolean hasValue;
    private final Object value;

    private OnErrorThrowable(Throwable exception) {
        super(exception);
        hasValue = false;
        this.value = null;
    }

    private OnErrorThrowable(Throwable exception, Object value) {
        super(exception);
        hasValue = true;
        Object v;
        if (value instanceof Serializable) {
            v = value;
        } else {
            try {
                v = String.valueOf(value);
            } catch (Throwable ex) {
                v = ex.getMessage();
            }
        }
        this.value = v;
    }

    /**
     * Get the value associated with this {@code OnErrorThrowable}
     *
     * @return the value associated with this {@code OnErrorThrowable} (or {@code null} if there is none)
     */
    public Object getValue() {
        return value;
    }

    /**
     * Indicates whether or not there is a value associated with this {@code OnErrorThrowable}
     *
     * @return {@code true} if there is a value associated with this {@code OnErrorThrowable}, otherwise
     *         {@code false}
     */
    public boolean isValueNull() {
        return hasValue;
    }

    /**
     * Converts a {@link Throwable} into an {@link OnErrorThrowable}.
     *
     * @param t
     *          the {@code Throwable} to convert; if null, a NullPointerException is constructed
     * @return an {@code OnErrorThrowable} representation of {@code t}
     */
    public static OnErrorThrowable from(Throwable t) {
        if (t == null) {
            t = new NullPointerException();
        }
        Throwable cause = Exceptions.getFinalCause(t);
        if (cause instanceof OnErrorThrowable.OnNextValue) {
            return new OnErrorThrowable(t, ((OnNextValue) cause).getValue());
        }
        return new OnErrorThrowable(t);
    }

    /**
     * Adds the given item as the final cause of the given {@code Throwable}, wrapped in {@code OnNextValue}
     * (which extends {@code RuntimeException}).
     *
     * @param e
     *          the {@link Throwable} to which you want to add a cause
     * @param value
     *          the item you want to add to {@code e} as the cause of the {@code Throwable}
     * @return the same {@code Throwable} ({@code e}) that was passed in, with {@code value} added to it as a
     *         cause
     */
    public static Throwable addValueAsLastCause(Throwable e, Object value) {
        if (e == null) {
            e = new NullPointerException();
        }
        Throwable lastCause = Exceptions.getFinalCause(e);
        if (lastCause instanceof OnNextValue) {
            // purposefully using == for object reference check
            if (((OnNextValue) lastCause).getValue() == value) {
                // don't add another
                return e;
            }
        }
        Exceptions.addCause(e, new OnNextValue(value));
        return e;
    }

    /**
     * Represents an exception that was encountered while trying to emit an item from an Observable, and
     * tries to preserve that item for future use and/or reporting.
     */
    public static class OnNextValue extends RuntimeException {

        private static final long serialVersionUID = -3454462756050397899L;

        private final Object value;

        // Lazy loaded singleton
        static final class Primitives {

            static final Set<Class<?>> INSTANCE = create();

            private static Set<Class<?>> create() {
                Set<Class<?>> set = new HashSet<Class<?>>();
                set.add(Boolean.class);
                set.add(Character.class);
                set.add(Byte.class);
                set.add(Short.class);
                set.add(Integer.class);
                set.add(Long.class);
                set.add(Float.class);
                set.add(Double.class);
                // Void is another primitive but cannot be instantiated
                // and is caught by the null check in renderValue
                return set;
            }
        }

        /**
         * Create an {@code OnNextValue} exception and include in its error message a string representation of
         * the item that was intended to be emitted at the time the exception was handled.
         *
         * @param value
         *         the item that the Observable was trying to emit at the time of the exception
         */
        public OnNextValue(Object value) {
            super("OnError while emitting onNext value: " + renderValue(value));
            Object v;
            if (value instanceof Serializable) {
                v = value;
            } else {
                try {
                    v = String.valueOf(value);
                } catch (Throwable ex) {
                    v = ex.getMessage();
                }
            }
            this.value = v;
        }

        /**
         * Retrieve the item that the Observable was trying to emit at the time this exception occurred.
         *
         * @return the item that the Observable was trying to emit at the time of the exception
         */
        public Object getValue() {
            return value;
        }

        /**
         * Render the object if it is a basic type. This avoids the library making potentially expensive
         * or calls to toString() which may throw exceptions.
         *
         * If a specific behavior has been defined in the {@link RxJavaErrorHandler} plugin, some types
         * may also have a specific rendering. Non-primitive types not managed by the plugin are rendered
         * as the class name of the object.
         * <p>
         * See PR #1401 and Issue #2468 for details.
         *
         * @param value
         *        the item that the Observable was trying to emit at the time of the exception
         * @return a string version of the object if primitive or managed through error plugin,
         *        otherwise the class name of the object
         */
        static String renderValue(Object value) {
            if (value == null) {
                return "null";
            }
            if (Primitives.INSTANCE.contains(value.getClass())) {
                return value.toString();
            }
            if (value instanceof String) {
                return (String) value;
            }
            if (value instanceof Enum) {
                return ((Enum<?>) value).name();
            }

            @SuppressWarnings("deprecation")
            String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value);
            if (pluggedRendering != null) {
                return pluggedRendering;
            }

            return value.getClass().getName() + ".class";
        }
    }
}
