/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.binaryschema;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser;
import org.eclipse.milo.opcua.binaryschema.parser.DictionaryDescription;
import org.eclipse.milo.opcua.stack.core.NamespaceTable;
import org.eclipse.milo.opcua.stack.core.UaSerializationException;
import org.eclipse.milo.opcua.stack.core.channel.EncodingLimits;
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder;
import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder;
import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext;
import org.eclipse.milo.opcua.stack.core.serialization.codecs.OpcUaBinaryDataTypeCodec;
import org.eclipse.milo.opcua.stack.core.types.DataTypeManager;
import org.eclipse.milo.opcua.stack.core.types.OpcUaBinaryDataTypeDictionary;
import org.eclipse.milo.opcua.stack.core.types.OpcUaDataTypeManager;
import org.eclipse.milo.opcua.stack.core.util.Namespaces;
import org.testng.annotations.BeforeSuite;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public abstract class BsdParserTest {

    private static final String BSD_PARSER_TEST_NAMESPACE_URI = "https://github.com/eclipse/milo";

    private final Table<String, String, OpcUaBinaryDataTypeCodec<?>> codecTable = HashBasedTable.create();

    private final SerializationContext context = new SerializationContext() {

        private final NamespaceTable namespaceTable = new NamespaceTable();

        @Override
        public DataTypeManager getDataTypeManager() {
            return OpcUaDataTypeManager.getInstance();
        }

        @Override
        public EncodingLimits getEncodingLimits() {
            return EncodingLimits.DEFAULT;
        }

        @Override
        public NamespaceTable getNamespaceTable() {
            return namespaceTable;
        }

        @Override
        public Object decode(
            String namespaceUri,
            String typeName,
            OpcUaBinaryStreamDecoder decoder
        ) throws UaSerializationException {

            return codecTable.get(namespaceUri, typeName).decode(context, decoder);
        }

        @Override
        public void encode(
            String namespaceUri,
            String typeName,
            Object encodable,
            OpcUaBinaryStreamEncoder encoder
        ) throws UaSerializationException {

            @SuppressWarnings("unchecked")
            OpcUaBinaryDataTypeCodec<Object> codec =
                (OpcUaBinaryDataTypeCodec<Object>) codecTable.get(namespaceUri, typeName);

            codec.encode(context, encoder, encodable);
        }

    };

    public BsdParserTest() {
        OpcUaDataTypeManager dataTypeManager = OpcUaDataTypeManager.getInstance();

        OpcUaBinaryDataTypeDictionary dictionary =
            (OpcUaBinaryDataTypeDictionary) dataTypeManager.getDataTypeDictionary(Namespaces.OPC_UA);

        assert dictionary != null;

        dictionary.getCodecsByDescription().forEach((d, c) ->
            codecTable.put(Namespaces.OPC_UA, d, c)
        );
    }

    /**
     * Create and return a {@link BsdParser} instance to test.
     *
     * @return a {@link BsdParser} instance to test.
     */
    protected abstract BsdParser createBsdParser();

    @BeforeSuite
    public void parseTypeDictionary() throws Exception {
        BsdParser parser = createBsdParser();

        DictionaryDescription dictionary = parser.parse(
            getClass().getClassLoader().getResourceAsStream(
                "dictionaries/BsdParserTest.bsd.xml")
        );

        dictionary.getStructCodecs().forEach(cd ->
            codecTable.put(
                dictionary.getNamespaceUri(), cd.getDescription(), cd.getCodec())
        );

        dictionary.getEnumCodecs().forEach(cd ->
            codecTable.put(
                dictionary.getNamespaceUri(), cd.getDescription(), cd.getCodec())
        );
    }

    @SuppressWarnings("unchecked")
    protected OpcUaBinaryDataTypeCodec<Object> getCodec(String name) {
        OpcUaBinaryDataTypeCodec<?> codec =
            codecTable.get(BSD_PARSER_TEST_NAMESPACE_URI, name);

        assertNotNull(codec);

        return (OpcUaBinaryDataTypeCodec<Object>) codec;
    }

    protected void assertRoundTrip(String type, Object originalValue, OpcUaBinaryDataTypeCodec<Object> codec) {
        System.out.printf("--- assertRoundTrip Type: %s ---\n", type);

        System.out.println("originalValue:\t" + originalValue);
        ByteBuf buffer = Unpooled.buffer();
        codec.encode(context, new OpcUaBinaryStreamEncoder(context).setBuffer(buffer), originalValue);

        ByteBuf encodedValue = buffer.copy();
        System.out.println("encodedValue:\t" + ByteBufUtil.hexDump(encodedValue));

        Object decodedValue = codec.decode(context, new OpcUaBinaryStreamDecoder(context).setBuffer(buffer));
        assertEquals(decodedValue, originalValue);
        System.out.println("decodedValue:\t" + decodedValue);
    }

    /**
     * A weaker version of {@link #assertRoundTrip(String, Object, OpcUaBinaryDataTypeCodec)} for values that don't
     * implement equals and hashcode or values that contain members not implementing equals and  hashcode.
     * <p>
     * Relies on toString() values to be implemented at all levels instead... not great, but since the built-in structs
     * don't implement equals/hashcode it's what we have.
     */
    protected void assertRoundTripUsingToString(
        String type,
        Object originalValue,
        OpcUaBinaryDataTypeCodec<Object> codec
    ) {

        System.out.printf("--- assertRoundTrip Type: %s ---\n", type);

        System.out.println("originalValue:\t" + originalValue);
        ByteBuf buffer = Unpooled.buffer();
        codec.encode(context, new OpcUaBinaryStreamEncoder(context).setBuffer(buffer), originalValue);

        ByteBuf encodedValue = buffer.copy();
        System.out.println("encodedValue:\t" + ByteBufUtil.hexDump(encodedValue));

        Object decodedValue = codec.decode(context, new OpcUaBinaryStreamDecoder(context).setBuffer(buffer));
        assertEquals(decodedValue.toString(), originalValue.toString());
        System.out.println("decodedValue:\t" + decodedValue);
    }

}
