/*
 * Copyright (c) 2015, 2017 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 v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.http2;


import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.http2.frames.ContinuationFrame;
import org.glassfish.grizzly.http2.frames.DataFrame;
import org.glassfish.grizzly.http2.frames.GoAwayFrame;
import org.glassfish.grizzly.http2.frames.HeadersFrame;
import org.glassfish.grizzly.http2.frames.Http2Frame;
import org.glassfish.grizzly.http2.frames.PingFrame;
import org.glassfish.grizzly.http2.frames.PriorityFrame;
import org.glassfish.grizzly.http2.frames.PushPromiseFrame;
import org.glassfish.grizzly.http2.frames.RstStreamFrame;
import org.glassfish.grizzly.http2.frames.SettingsFrame;
import org.glassfish.grizzly.http2.frames.UnknownFrame;
import org.glassfish.grizzly.http2.frames.WindowUpdateFrame;

import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

final class NetLogger {

    private static final Logger LOGGER = Grizzly.logger(NetLogger.class);
    private static final Level LEVEL = Level.FINE;

    private static final String CLOSE_FMT           = "'{' \"session\":\"{0}\", \"event\":\"SESSION_CLOSE\" '}'";
    private static final String DATA                = "DATA";
    private static final String DATA_FMT            = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"fin\":\"{3}\", \"len\":\"{4}\" '}'";
    private static final String CONTINUATION        = "CONTINUATION";
    private static final String CONTINUATION_FMT    = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"len\":\"{3}\" '}'";
    private static final String GOAWAY              = "GOAWAY";
    private static final String GOAWAY_FMT          = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"last-stream\":\"{3}\", \"error-code\":\"{4}\", \"detail\":\"{5}\" '}'";
    private static final String HEADERS             = "HEADERS";
    private static final String HEADERS_FMT         = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"parent-stream\":\"{3}\", \"prioritized\":\"{4}\", \"exclusive\":\"{5}\", \"weight\":\"{6}\", \"fin\":\"{7}\", \"len\":\"{8}\", \"headers\":{9} '}'";
    private static final String OPEN_FMT            = "'{' \"session\":\"{0}\", \"event\":\"SESSION_OPEN\" '}'";
    private static final String PING                = "PING";
    private static final String PING_FMT            = "'{' session=\"{0}\", event=\"{1}\", is-ack=\"{2}\", opaque-data=\"{3}\" '}'";
    private static final String PRIORITY            = "PRIORITY";
    private static final String PRIORITY_FMT        = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"parent-stream\":\"{3}\", \"exclusive\":\"{4}\", \"weight\":\"{5}\" '}'";
    private static final String PUSH_PROMISE        = "PUSH_PROMISE";
    private static final String PUSH_PROMISE_FMT    = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"promised-stream\":\"{3}\", \"len\":\"{4}\", \"headers\":{5} '}'";
    private static final String RST                 = "RST";
    private static final String RST_FMT             = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"stream\":\"{2}\", \"error-code\":\"{3}\" '}'";
    private static final String SETTINGS            = "SETTINGS";
    private static final String SETTINGS_FMT        = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"settings\":'{'{2}'}' '}'";
    private static final String UNKNOWN             = "UNKNOWN";
    private static final String UNKNOWN_FMT         = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"frame-type\":\"{2}\", \"len\":\"{3}\" '}'";
    private static final String WINDOW_UPDATE       = "WINDOW_UPDATE";
    private static final String WINDOW_UPDATE_FMT   = "'{' \"session\":\"{0}\", \"event\":\"{1}\", \"delta\":\"{2}\" '}'";

    private static final String NOT_AVAILABLE = "None Available";


    enum Context {
        TX("SEND_"),
        RX("RECV_");

        final String prefix;
        Context(final String prefix) {
            this.prefix = prefix;
        }

        String getPrefix() {
            return prefix;
        }
    }

    static boolean isActive() {
        return LOGGER.isLoggable(LEVEL);
    }

    static void log(final Context ctx, final Http2Session c, final Http2Frame frame) {
        switch (frame.getType()) {
            case ContinuationFrame.TYPE:
                log(ctx, c, (ContinuationFrame) frame);
                break;
            case DataFrame.TYPE:
                log(ctx, c, (DataFrame) frame);
                break;
            case GoAwayFrame.TYPE:
                log(ctx, c, (GoAwayFrame) frame);
                break;
            case HeadersFrame.TYPE:
                break;
            case PingFrame.TYPE:
                log(ctx, c, (PingFrame) frame);
                break;
            case PriorityFrame.TYPE:
                log(ctx, c, (PriorityFrame) frame);
                break;
            case PushPromiseFrame.TYPE:
                break;
            case RstStreamFrame.TYPE:
                log(ctx, c, (RstStreamFrame) frame);
                break;
            case SettingsFrame.TYPE:
                log(ctx, c, (SettingsFrame) frame);
                break;
            case WindowUpdateFrame.TYPE:
                log(ctx, c, (WindowUpdateFrame) frame);
                break;
            default:
                log(ctx, c, (UnknownFrame) frame);

        }
    }

    static void log(final Context ctx, final Http2Session c, final ContinuationFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, CONTINUATION_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + CONTINUATION,
                    frame.getStreamId(),
                    frame.getLength()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final DataFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, DATA_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + DATA,
                    frame.getStreamId(),
                    frame.isEndStream(),
                    frame.getData().remaining()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final GoAwayFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            final Buffer b = frame.getAdditionalDebugData();
            final String details = ((b != null) ? b.toStringContent() : NOT_AVAILABLE);
            LOGGER.log(LEVEL, GOAWAY_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + GOAWAY,
                    frame.getStreamId(),
                    frame.getLastStreamId(),
                    frame.getErrorCode().getCode(),
                    escape(details)});
        }
    }

    static void log(final Context ctx,
                    final Http2Session c,
                    final HeadersFrame frame,
                    final Map<String,String> headers) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, HEADERS_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + HEADERS,
                    frame.getStreamId(),
                    frame.getStreamDependency(),
                    frame.isPrioritized(),
                    frame.isExclusive(),
                    frame.getWeight(),
                    frame.isEndStream(),
                    frame.getLength(),
                    toJSON(headers)});


        }
    }



    static void log(final Context ctx, final Http2Session c, final PingFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, PING_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + PING,
                    frame.isAckSet(),
                    frame.getOpaqueData()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final PriorityFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, PRIORITY_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + PRIORITY,
                    frame.getStreamId(),
                    frame.getStreamDependency(),
                    frame.isExclusive(),
                    frame.getWeight()});
        }
    }

    static void log(final Context ctx,
                    final Http2Session c,
                    final PushPromiseFrame frame,
                    final Map<String,String> headers) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, PUSH_PROMISE_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + PUSH_PROMISE,
                    frame.getStreamId(),
                    frame.getPromisedStreamId(),
                    frame.getLength(),
                    toJSON(headers)});
        }
    }

    static void log(final Context ctx, final Http2Session c, final RstStreamFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, RST_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + RST,
                    frame.getStreamId(),
                    frame.getErrorCode().getCode()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final SettingsFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            final int numSettings = frame.getNumberOfSettings();
            final StringBuilder sb = new StringBuilder();
            if (numSettings > 0) {
                for (int i = 0; i < numSettings; i++) {
                    final SettingsFrame.Setting setting = frame.getSettingByIndex(i);
                    sb.append('"').append(frame.getSettingNameById(setting.getId())).append('"');
                    sb.append(": ");
                    sb.append('"').append(setting.getValue()).append('"');
                    if (i + 1 < numSettings) {
                        sb.append(", ");
                    }
                }
            }
            LOGGER.log(LEVEL, SETTINGS_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + SETTINGS,
                    sb.toString()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final WindowUpdateFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, WINDOW_UPDATE_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + WINDOW_UPDATE,
                    frame.getWindowSizeIncrement()});
        }
    }

    static void log(final Context ctx, final Http2Session c, final UnknownFrame frame) {
        validateParams(ctx, c, frame);
        if (isActive()) {
            LOGGER.log(LEVEL, UNKNOWN_FMT, new Object[]{
                    escape(c.getConnection().toString()),
                    ctx.getPrefix() + UNKNOWN,
                    frame.getType(),
                    frame.getLength()});
        }
    }

    static void logClose(final Http2Session c) {
        logSessionEvent(CLOSE_FMT, c);
    }

    static void logOpen(final Http2Session c) {
        logSessionEvent(OPEN_FMT, c);
    }


    // --------------------------------------------------------- Private Methods


    private static void logSessionEvent(final String msg, final Http2Session c) {
        if (c == null) {
            throw new NullPointerException("Http2Session cannot be null");
        }
        if (isActive()) {
            LOGGER.log(LEVEL, msg, new Object[]{escape(c.getConnection().toString())});
        }
    }

    private static StringBuilder toJSON(final Map<String, String> headers) {
        final StringBuilder result = new StringBuilder(64);
        result.append("{ ");
        for (Iterator<Map.Entry<String,String>> i = headers.entrySet().iterator(); i.hasNext();) {
            Map.Entry<String,String> entry = i.next();
            result.append('"').append(entry.getKey()).append("\":\"").append(entry.getValue()).append('"');
            if (i.hasNext()) {
                result.append(", ");
            }
        }
        result.append(" }");
        return result;
    }

    private static void validateParams(final Context ctx,
                                       final Http2Session c,
                                       final Http2Frame frame) {
        if (ctx == null) {
            throw new NullPointerException("Context cannot be null.");
        }
        if (c == null) {
            throw new NullPointerException("Http2Session cannot be null.");
        }
        if (frame == null) {
            throw new NullPointerException("Http2Frame cannot be null.");
        }
    }

    private static String escape(final String s) {
        final StringBuilder sb = new StringBuilder(s.length() + 20);
        for (int i = 0, len = s.length(); i < len; i++) {
            final char c = s.charAt(i);
            switch (c) {
                case '\'':
                    sb.append("\'");
                    break;
                case '"' :
                    sb.append("\"");
                    break;
                case '\\':
                    sb.append("\\");
                    break;
                default:
                    sb.append(c);
            }
        }
        return sb.toString();
    }

}