/*
 * Copyright 2013-2020 Real Logic Limited.
 *
 * 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
 *
 * https://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 uk.co.real_logic.sbe.examples;

import org.agrona.DirectBuffer;
import uk.co.real_logic.sbe.PrimitiveValue;
import uk.co.real_logic.sbe.ir.Encoding;
import uk.co.real_logic.sbe.ir.Token;
import uk.co.real_logic.sbe.otf.TokenListener;
import uk.co.real_logic.sbe.otf.Types;

import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

/**
 * Example of a {@link TokenListener} implementation which prints a decoded message to a {@link PrintWriter}.
 */
public class ExampleTokenListener implements TokenListener
{
    private int compositeLevel = 0;
    private final PrintWriter out;
    private final Deque<String> namedScope = new ArrayDeque<>();
    private final byte[] tempBuffer = new byte[1024];

    public ExampleTokenListener(final PrintWriter out)
    {
        this.out = out;
    }

    public void onBeginMessage(final Token token)
    {
        namedScope.push(token.name() + ".");
    }

    public void onEndMessage(final Token token)
    {
        namedScope.pop();
    }

    public void onEncoding(
        final Token fieldToken,
        final DirectBuffer buffer,
        final int index,
        final Token typeToken,
        final int actingVersion)
    {
        final CharSequence value = readEncodingAsString(buffer, index, typeToken, actingVersion);

        printScope();
        out.append(compositeLevel > 0 ? typeToken.name() : fieldToken.name())
            .append('=')
            .append(value)
            .println();
    }

    public void onEnum(
        final Token fieldToken,
        final DirectBuffer buffer,
        final int bufferIndex,
        final List<Token> tokens,
        final int beginIndex,
        final int endIndex,
        final int actingVersion)
    {
        final Token typeToken = tokens.get(beginIndex + 1);
        final long encodedValue = readEncodingAsLong(buffer, bufferIndex, typeToken, actingVersion);

        String value = null;
        if (fieldToken.isConstantEncoding())
        {
            final String refValue = fieldToken.encoding().constValue().toString();
            final int indexOfDot = refValue.indexOf('.');
            value = -1 == indexOfDot ? refValue : refValue.substring(indexOfDot + 1);
        }
        else
        {
            for (int i = beginIndex + 1; i < endIndex; i++)
            {
                if (encodedValue == tokens.get(i).encoding().constValue().longValue())
                {
                    value = tokens.get(i).name();
                    break;
                }
            }
        }

        printScope();
        out.append(determineName(0, fieldToken, tokens, beginIndex))
            .append('=')
            .append(value)
            .println();
    }

    public void onBitSet(
        final Token fieldToken,
        final DirectBuffer buffer,
        final int bufferIndex,
        final List<Token> tokens,
        final int beginIndex,
        final int endIndex,
        final int actingVersion)
    {
        final Token typeToken = tokens.get(beginIndex + 1);
        final long encodedValue = readEncodingAsLong(buffer, bufferIndex, typeToken, actingVersion);

        printScope();
        out.append(determineName(0, fieldToken, tokens, beginIndex)).append(':');

        for (int i = beginIndex + 1; i < endIndex; i++)
        {
            out.append(' ').append(tokens.get(i).name()).append('=');

            final long bitPosition = tokens.get(i).encoding().constValue().longValue();
            final boolean flag = (encodedValue & (1L << bitPosition)) != 0;

            out.append(Boolean.toString(flag));
        }

        out.println();
    }

    public void onBeginComposite(
        final Token fieldToken, final List<Token> tokens, final int fromIndex, final int toIndex)
    {
        ++compositeLevel;

        namedScope.push(determineName(1, fieldToken, tokens, fromIndex) + ".");
    }

    public void onEndComposite(final Token fieldToken, final List<Token> tokens, final int fromIndex, final int toIndex)
    {
        --compositeLevel;

        namedScope.pop();
    }

    public void onGroupHeader(final Token token, final int numInGroup)
    {
        printScope();
        out.append(token.name())
            .append(" Group Header : numInGroup=")
            .append(Integer.toString(numInGroup))
            .println();
    }

    public void onBeginGroup(final Token token, final int groupIndex, final int numInGroup)
    {
        namedScope.push(token.name() + ".");
    }

    public void onEndGroup(final Token token, final int groupIndex, final int numInGroup)
    {
        namedScope.pop();
    }

    public void onVarData(
        final Token fieldToken,
        final DirectBuffer buffer,
        final int bufferIndex,
        final int length,
        final Token typeToken)
    {
        final String value;
        try
        {
            final String characterEncoding = typeToken.encoding().characterEncoding();
            if (null == characterEncoding)
            {
                value = length + " bytes of raw data";
            }
            else
            {
                buffer.getBytes(bufferIndex, tempBuffer, 0, length);
                value = new String(tempBuffer, 0, length, characterEncoding);
            }
        }
        catch (final UnsupportedEncodingException ex)
        {
            ex.printStackTrace();
            return;
        }

        printScope();
        out.append(fieldToken.name())
            .append('=')
            .append(value)
            .println();
    }

    private String determineName(
        final int thresholdLevel, final Token fieldToken, final List<Token> tokens, final int fromIndex)
    {
        if (compositeLevel > thresholdLevel)
        {
            return tokens.get(fromIndex).name();
        }
        else
        {
            return fieldToken.name();
        }
    }

    private static CharSequence readEncodingAsString(
        final DirectBuffer buffer, final int index, final Token typeToken, final int actingVersion)
    {
        final PrimitiveValue constOrNotPresentValue = constOrNotPresentValue(typeToken, actingVersion);
        if (null != constOrNotPresentValue)
        {
            if (constOrNotPresentValue.size() == 1)
            {
                try
                {
                    final byte[] bytes = { (byte)constOrNotPresentValue.longValue() };
                    return new String(bytes, constOrNotPresentValue.characterEncoding());
                }
                catch (final UnsupportedEncodingException ex)
                {
                    throw new RuntimeException(ex);
                }
            }
            else
            {
                return constOrNotPresentValue.toString();
            }
        }

        final StringBuilder sb = new StringBuilder();
        final Encoding encoding = typeToken.encoding();
        final int elementSize = encoding.primitiveType().size();

        for (int i = 0, size = typeToken.arrayLength(); i < size; i++)
        {
            Types.appendAsString(sb, buffer, index + (i * elementSize), encoding);
            sb.append(", ");
        }

        sb.setLength(sb.length() - 2);

        return sb;
    }

    private static long readEncodingAsLong(
        final DirectBuffer buffer, final int bufferIndex, final Token typeToken, final int actingVersion)
    {
        final PrimitiveValue constOrNotPresentValue = constOrNotPresentValue(typeToken, actingVersion);
        if (null != constOrNotPresentValue)
        {
            return constOrNotPresentValue.longValue();
        }

        return Types.getLong(buffer, bufferIndex, typeToken.encoding());
    }

    private static PrimitiveValue constOrNotPresentValue(final Token token, final int actingVersion)
    {
        if (token.isConstantEncoding())
        {
            return token.encoding().constValue();
        }
        else if (token.isOptionalEncoding() && actingVersion < token.version())
        {
            return token.encoding().applicableNullValue();
        }

        return null;
    }

    private void printScope()
    {
        final Iterator<String> i = namedScope.descendingIterator();
        while (i.hasNext())
        {
            out.print(i.next());
        }
    }
}