/*******************************************************************************
 * Copyright (c) 2011, 2016 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *      tware - initial
 ******************************************************************************/
package org.eclipse.persistence.jpa.rs.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.logging.Level;

import javax.ws.rs.core.MediaType;

import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.weaving.PersistenceWeavedRest;
import org.eclipse.persistence.jpa.rs.DataStorage;
import org.eclipse.persistence.jpa.rs.PersistenceContext;
import org.eclipse.persistence.jpa.rs.logging.LoggingLocalization;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.logging.SessionLogEntry;
import org.eclipse.persistence.sessions.Session;

/**
 * Logger for EclipseLink JPA-RS related functionality.
 * Publishes messages under the {@link SessionLog#JPARS} category.
 */
public class JPARSLogger {

    private static final SessionLog defaultLog = AbstractSessionLog.getLog();

    /**
     * Entering
     *
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param params parameters
     */
    public static void entering(String sourceClass, String sourceMethod, Object[] params) {
        entering(defaultLog, sourceClass, sourceMethod, params);
    }

    /**
     * Entering
     *
     * @param sessionLog the log
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param params parameters
     */
    public static void entering(SessionLog sessionLog, String sourceClass, String sourceMethod, Object[] params) {
        // Logger logs entering logs when log level <= FINER. But, we want to get these logs created only when the log level is FINEST.
        if (isLoggableFinest(sessionLog)) {
            SessionLogEntry sle = newLogEntry(sessionLog.getSession());
            sle.setSourceClassName(sourceClass);
            sle.setSourceMethodName(sourceMethod);
            sle.setMessage("ENTRY {0}");
            sle.setParameters(getParamsWithAdditionalInfo(params));
            sessionLog.log(sle);
        }
    }

    /**
     * Entering
     *
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param in the input stream
     */
    public static void entering(String sourceClass, String sourceMethod, InputStream in) {
        entering(defaultLog, sourceClass, sourceMethod, in);
    }

    /**
     * Entering
     *
     * @param sessionLog log receiving the message
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param in the input stream
     */
    public static void entering(SessionLog sessionLog, String sourceClass, String sourceMethod, InputStream in) {
        // Logger logs entering logs when log level <= FINER. But, we want to get these logs created only when the log level is FINEST.

        // make sure input stream supports mark so that the or create a new BufferedInputStream which supports mark.
        // when mark is supported, the stream remembers all the bytes read after the call to mark and
        // stands ready to supply those same bytes again if and whenever the method reset is called.
        if (isLoggableFinest(sessionLog) && (in.markSupported())) {
            try {
                String data = readData(in);
                in.reset();
                if (data != null) {
                    SessionLogEntry sle = newLogEntry(sessionLog.getSession());
                    sle.setSourceClassName(sourceClass);
                    sle.setSourceMethodName(sourceMethod);
                    sle.setMessage("ENTRY {0}");
                    sle.setParameters(getParamsWithAdditionalInfo(new Object[] { data }));
                    sessionLog.log(sle);
                }
            } catch (Throwable throwable) {
                exception(throwable.getMessage(), new Object[] {}, throwable);
            }
        }
    }

    /**
     * Exiting
     *
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param params parameters
     */
    public static void exiting(String sourceClass, String sourceMethod, Object[] params) {
        exiting(defaultLog, sourceClass, sourceMethod, params);
    }

    /**
     * Exiting
     *
     * @param sessionLog the log
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param params parameters
     */
    public static void exiting(SessionLog sessionLog, String sourceClass, String sourceMethod, Object[] params) {
        // Logger logs exiting logs when log level <= FINER. But, we want to get these logs created only when the log level is FINEST.
        if (isLoggableFinest()) {
            try {
                SessionLogEntry sle = newLogEntry(sessionLog.getSession());
                sle.setSourceClassName(sourceClass);
                sle.setSourceMethodName(sourceMethod);
                sle.setMessage("RETURN {0}");
                sle.setParameters(new Object[] {new MethodExitLogData(getParamsWithAdditionalInfo(params))});
                sessionLog.log(sle);
            } catch (Throwable throwable) {
                exception(throwable.getMessage(), new Object[] {}, throwable);
            }
        }
    }

    /**
     * Exiting
     *
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param context the context
     * @param object the object
     * @param mediaType the media type
     */
    public static void exiting(String sourceClass, String sourceMethod, PersistenceContext context, Object object, MediaType mediaType) {
        exiting(defaultLog, sourceClass, sourceMethod, context, object, mediaType);
    }

    /**
     * Exiting
     *
     * @param sessionLog the log
     * @param sourceClass the source class
     * @param sourceMethod the source method
     * @param context the context
     * @param object the object
     * @param mediaType the media type
     */
    public static void exiting(SessionLog sessionLog, String sourceClass, String sourceMethod, PersistenceContext context, Object object, MediaType mediaType) {
        // Log marshaled object only when the log level is FINEST
        if (isLoggableFinest(sessionLog) && (context != null) && (object != null) && (mediaType != null)) {
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                context.marshall(object, mediaType, outputStream, true);
                if (object instanceof PersistenceWeavedRest) {
                    exiting(sessionLog, sourceClass, sourceMethod, new Object[] { object.getClass().getName(), outputStream.toString(StandardCharsets.UTF_8.name())});
                } else {
                    exiting(sessionLog, sourceClass, sourceMethod, new Object[] { outputStream.toString(StandardCharsets.UTF_8.name()) });
                }
            } catch (Throwable throwable) {
                exception(throwable.getMessage(), new Object[] {}, throwable);
            }
        }
    }

    /**
     * Finest
     *
     * @param message the message
     * @param params parameters
     */
    public static void finest(String message, Object[] params) {
        finest(defaultLog, message, params);
    }

    /**
     * Finest
     *
     * @param sessionLog the log
     * @param message the message
     * @param params parameters
     */
    public static void finest(SessionLog sessionLog, String message, Object[] params) {
        log(sessionLog, SessionLog.FINEST, message, getParamsWithAdditionalInfo(params));
    }

    /**
     * Fine
     *
     * @param message the message
     * @param params parameters
     */
    public static void fine(String message, Object[] params) {
        fine(defaultLog, message, params);
    }

    /**
     * Fine
     *
     * @param sessionLog the log
     * @param message the message
     * @param params parameters
     */
    public static void fine(SessionLog sessionLog, String message, Object[] params) {
        log(sessionLog, SessionLog.FINE, message, getParamsWithAdditionalInfo(params));
    }

    /**
     * Warning
     *
     * @param message the message
     * @param params parameters
     */
    public static void warning(String message, Object[] params) {
        warning(defaultLog, message, params);
    }

    /**
     * Warning
     *
     * @param sessionLog the log
     * @param message the message
     * @param params parameters
     */
    public static void warning(SessionLog sessionLog, String message, Object[] params) {
        log(sessionLog, SessionLog.WARNING, message, getParamsWithAdditionalInfo(params));
    }

    /**
     * Error
     *
     * @param message the message
     * @param params parameters
     */
    public static void error(String message, Object[] params) {
        error(defaultLog, message, params);
    }

    /**
     * Error
     *
     * @param sessionLog the log
     * @param message the message
     * @param params parameters
     */
    public static void error(SessionLog sessionLog, String message, Object[] params) {
        log(sessionLog, SessionLog.SEVERE, message, getParamsWithAdditionalInfo(params));
    }

    /**
     * Exception
     *
     * @param message the message
     * @param params parameters
     * @param exc the throwable
     */
    public static void exception(String message, Object[] params, Throwable exc) {
        exception(defaultLog, message, params, exc);
    }

    /**
     * Exception
     *
     * @param sessionLog the log
     * @param message the message
     * @param params parameters
     * @param exc the throwable
     */
    public static void exception(SessionLog sessionLog, String message, Object[] params, Throwable exc) {
        log(sessionLog, SessionLog.SEVERE, message, getParamsWithAdditionalInfo(params), exc);
    }

    /**
     * Sets the log level
     *
     * @param level the new log level
     */
    public static void setLogLevel(Level level) {
        setLogLevel(defaultLog, AbstractSessionLog.translateStringToLoggingLevel(level.getName()));
    }

    /**
     * Sets the log level
     *
     * @param sessionLog the log
     * @param level the new log level
     */
    public static void setLogLevel(SessionLog sessionLog, int level) {
        sessionLog.setLevel(level, SessionLog.JPARS);
    }

    /**
     * @return true if log level is set to {@link SessionLog#FINEST}
     */
    public static boolean isLoggableFinest() {
        return isLoggableFinest(defaultLog);
    }

    /**
     * @param sessionLog the log
     *
     * @return true if log level is set to {@link SessionLog#FINEST}
     */
    public static boolean isLoggableFinest(SessionLog sessionLog) {
        return sessionLog.shouldLog(SessionLog.FINEST, SessionLog.JPARS);
    }

    private static Object[] getParamsWithAdditionalInfo(Object[] params) {
        String requestId = (String) DataStorage.get(DataStorage.REQUEST_ID);
        if (params != null) {
            Object[] paramsWithRequestId = new Object[params.length + 1];
            paramsWithRequestId[0] = requestId;
            System.arraycopy(params, 0, paramsWithRequestId, 1, params.length);
            return paramsWithRequestId;
        }
        return new Object[] { requestId };
    }

    private static void log(SessionLog sessionLog, int level, String message, Object[] params) {
        log(sessionLog, level, message, params, null);
    }

    private static void log(SessionLog sessionLog, int level, String message, Object[] params, Throwable t) {
        Objects.requireNonNull(sessionLog);
        if (sessionLog.shouldLog(level, SessionLog.JPARS)) {
            SessionLogEntry sle = newLogEntry(sessionLog.getSession());
            sle.setLevel(level);
            sle.setMessage(LoggingLocalization.buildMessage(message, params));
            sle.setParameters(params);
            sle.setException(t);
            sessionLog.log(sle);
        }
    }

    private static SessionLogEntry newLogEntry(Session session) {
        SessionLogEntry entry = session instanceof AbstractSession
                ? new SessionLogEntry((AbstractSession) session)
                : new SessionLogEntry(null);
        entry.setLevel(SessionLog.FINEST);
        entry.setNameSpace(SessionLog.JPARS);
        entry.setShouldTranslate(false);
        return entry;
    }

    private static String readData(InputStream is) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        ByteArrayInputStream bais = null;
        int nRead;
        byte[] data = new byte[16384];
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }
        buffer.flush();
        byte[] bytes = buffer.toByteArray();
        bais = new ByteArrayInputStream(bytes);
        return getDataFromInputStream(bais);
    }

    private static String getDataFromInputStream(InputStream is) {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            // ignore
        }
        return sb.toString();
    }
}
