/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2011 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.jersey.spi.inject;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * TODO do not use static thread local?
 *
 * @author Paul.Sandoz@Sun.Com
 */
public final class Errors {

    public static class ErrorMessagesException extends RuntimeException {
        public final List<ErrorMessage> messages;
        
        private ErrorMessagesException(List<ErrorMessage> messages) {
            this.messages = messages;
        }
    }

    public static class ErrorMessage {
        
        final String message;

        final boolean isFatal;

        private ErrorMessage(String message, boolean isFatal) {
            this.message = message;
            this.isFatal = isFatal;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 37 * hash + (this.message != null ? this.message.hashCode() : 0);
            hash = 37 * hash + (this.isFatal ? 1 : 0);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final ErrorMessage other = (ErrorMessage) obj;
            if ((this.message == null) ? (other.message != null) : !this.message.equals(other.message)) {
                return false;
            }
            if (this.isFatal != other.isFatal) {
                return false;
            }
            return true;
        }
    }

    private final ArrayList<ErrorMessage> messages = new ArrayList<ErrorMessage>(0);

    private int mark = -1;

    private int stack = 0;

    private boolean fieldReporting = true;

    private void _mark() {
        mark = messages.size();
    }

    private void _unmark() {
        mark = -1;
    }

    private void _reset() {
        if (mark >= 0 && mark < messages.size()) {
            messages.subList(mark, messages.size()).clear();
            _unmark();
        }
    }

    private void preProcess() {
        stack++;
    }

    private void postProcess(boolean throwException) {
        stack--;
        fieldReporting = true;

        if (stack == 0) {
            try {
                if (!messages.isEmpty()) {
                    processErrorMessages(throwException, messages);
                }
            } finally {
                errors.remove();
            }
        }
    }

    private static final Logger LOGGER = Logger.getLogger(Errors.class.getName());

    private static void processErrorMessages(boolean throwException, List<ErrorMessage> messages) {
        final StringBuilder sb = new StringBuilder();
        boolean isFatal = false;
        for (ErrorMessage em : messages) {
            if (sb.length() > 0) {
                sb.append("\n");
            }

            sb.append("  ");

            if (em.isFatal) {
                sb.append("SEVERE: ");
            } else {
                sb.append("WARNING: ");
            }
            isFatal |= em.isFatal;

            sb.append(em.message);
        }

        final String message = sb.toString();
        if (isFatal) {
            LOGGER.severe("The following errors and warnings have been detected with resource and/or provider classes:\n" + message);
            if (throwException) {
                throw new ErrorMessagesException(new ArrayList<ErrorMessage>(messages));
            }
        } else {
            LOGGER.warning("The following warnings have been detected with resource and/or provider classes:\n" + message);
        }
    }

    private static ThreadLocal<Errors> errors = new ThreadLocal<Errors>();

    public static interface Closure<T> {
        public T f();
    }

    public static <T> T processWithErrors(Closure<T> c) {
        Errors e = errors.get();
        if (e == null) {
            e = new Errors();
            errors.set(e);
        }
        e.preProcess();

        RuntimeException caught = null;
        try {
            return c.f();
        } catch (RuntimeException re) {
            // If a runtime exception is caught then report errors and
            // rethrow
            caught = re;
        } finally {
            e.postProcess(caught == null);
        }

        throw caught;
    }

    private static Errors getInstance() {
        Errors e = errors.get();
        // No error processing in scope
        if (e == null) {
            throw new IllegalStateException("There is no error processing in scope");
        }
        // The following should not be necessary but given the fragile nature of
        // static thread local probably best to add it in case some internals of
        // this class change
        if (e.stack == 0) {
            errors.remove();
            throw new IllegalStateException("There is no error processing in scope");
        }
        return e;
    }

    public static void mark() {
        getInstance()._mark();
    }

    public static  void unmark() {
        getInstance()._unmark();
    }

    public static void reset() {
        getInstance()._reset();
    }

    public static void error(String message) {
        error(message, true);
    }

    public static void error(String message, boolean isFatal) {
        final ErrorMessage em = new ErrorMessage(message, isFatal);
        getInstance().messages.add(em);
    }

    public int numberOfErrors() {
        return getInstance().messages.size();
    }
    
    public static void innerClass(Class c) {
        error("The inner class " + c.getName() + " is not a static inner class and cannot be instantiated.");
    }

    public static void nonPublicClass(Class c) {
        error("The class " + c.getName() + " is a not a public class and cannot be instantiated.");
    }

    public static void nonPublicConstructor(Class c) {
        error("The class " + c.getName() + " does not have a public constructor and cannot be instantiated.");
    }

    public static void abstractClass(Class c) {
        error("The class " + c.getName() + " is an abstract class and cannot be instantiated.");
    }

    public static void interfaceClass(Class c) {
        error("The class " + c.getName() + " is an interface and cannot be instantiated.");
    }

    public static void missingDependency(Constructor ctor, int i) {
        error("Missing dependency for constructor " + ctor + " at parameter index " + i);
    }

    public static void setReportMissingDependentFieldOrMethod(boolean fieldReporting) {
        getInstance().fieldReporting = fieldReporting;
    }

    public static boolean getReportMissingDependentFieldOrMethod() {
        return getInstance().fieldReporting;
    }

    public static void missingDependency(Field f) {
        if (getReportMissingDependentFieldOrMethod()) {
            error("Missing dependency for field: " + f.toGenericString());
        }
    }
    
    public static void missingDependency(Method m, int i) {
        if (getReportMissingDependentFieldOrMethod()) {
            error("Missing dependency for method " + m + " at parameter at index " + i);
        }
    }
}
