/*
 * 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.gson;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.eclipse.milo.opcua.binaryschema.AbstractCodec;
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.opcfoundation.opcua.binaryschema.StructuredType;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;

public class JsonStructureCodec extends AbstractCodec<JsonObject, JsonElement> {

    private final Gson gson;

    JsonStructureCodec(StructuredType structuredType) {
        super(structuredType);

        gson = new GsonBuilder().setPrettyPrinting().create();
    }

    @Override
    public Class<JsonObject> getType() {
        return JsonObject.class;
    }

    @Override
    protected JsonObject createStructure(String name, LinkedHashMap<String, JsonElement> members) {
        JsonObject object = new JsonObject();

        members.forEach(object::add);

        return object;
    }

    @Override
    protected JsonElement opcUaToMemberTypeScalar(String name, Object value, String typeName) {
        if (value == null) {
            return JsonNull.INSTANCE;
        } else if (value instanceof Number) {
            if (value instanceof UByte) {
                return new JsonPrimitive(((UByte) value).shortValue());
            } else if (value instanceof UShort) {
                return new JsonPrimitive(((UShort) value).intValue());
            } else if (value instanceof UInteger) {
                return new JsonPrimitive(((UInteger) value).longValue());
            } else if (value instanceof ULong) {
                return new JsonPrimitive(((ULong) value).toBigInteger());
            } else {
                return new JsonPrimitive((Number) value);
            }
        } else if (value instanceof Boolean) {
            return new JsonPrimitive((Boolean) value);
        } else if (value instanceof String) {
            return new JsonPrimitive((String) value);
        } else if (value instanceof Character) {
            return new JsonPrimitive((Character) value);
        } else if (value instanceof JsonElement) {
            return (JsonElement) value;
        } else if (value instanceof DateTime) {
            return new JsonPrimitive(((DateTime) value).getUtcTime());
        } else if (value instanceof UUID) {
            return new JsonPrimitive(value.toString());
        } else if (value instanceof LocalizedText) {
            return gson.toJsonTree(value);
        } else if (value instanceof QualifiedName) {
            return gson.toJsonTree(value);
        } else if (value instanceof ByteString) {
            ByteString byteString = (ByteString) value;
            byte[] bs = byteString.bytesOrEmpty();
            JsonArray array = new JsonArray();
            for (Byte b : bs) {
                array.add(new JsonPrimitive(b));
            }
            return array;
        } else if (value instanceof XmlElement) {
            String fragment = ((XmlElement) value).getFragment();
            return fragment != null ? new JsonPrimitive(fragment) : JsonNull.INSTANCE;
        } else if (value instanceof NodeId) {
            String nodeId = ((NodeId) value).toParseableString();
            return new JsonPrimitive(nodeId);
        } else if (value instanceof ExpandedNodeId) {
            String xNodeId = ((ExpandedNodeId) value).toParseableString();
            return new JsonPrimitive(xNodeId);
        } else if (value instanceof StatusCode) {
            long code = ((StatusCode) value).getValue();
            return new JsonPrimitive(code);
        } else {
            throw new RuntimeException("could not create JsonElement for value: " + value);
        }
    }

    @Override
    protected JsonElement opcUaToMemberTypeArray(String name, Object values, String typeName) {
        if (values == null) {
            return JsonNull.INSTANCE;
        } else {
            JsonArray array = new JsonArray();

            if (values instanceof Object[]) {
                Object[] objects = (Object[]) values;

                for (Object value : objects) {
                    array.add(opcUaToMemberTypeScalar(name, value, typeName));
                }
            } else if (values instanceof Number) {
                // This is a bit array...
                Number number = (Number) values;
                return new JsonPrimitive(number);
            }

            return array;
        }
    }

    @Override
    protected Object memberTypeToOpcUaScalar(JsonElement member, String typeName) {
        if (member == null || member.isJsonNull()) {
            return null;
        } else if (member.isJsonArray()) {
            JsonArray array = member.getAsJsonArray();

            switch (typeName) {
                case "ByteString": {
                    byte[] bs = new byte[array.size()];

                    for (int i = 0; i < array.size(); i++) {
                        bs[i] = array.get(i).getAsByte();
                    }

                    return ByteString.of(bs);
                }

                default:
                    return array;
            }
        } else if (member.isJsonObject()) {
            JsonObject jsonObject = member.getAsJsonObject();

            switch (typeName) {
                case "QualifiedName": {
                    return new QualifiedName(
                        jsonObject.get("namespaceIndex").getAsInt(),
                        jsonObject.get("name").getAsString()
                    );
                }
                case "LocalizedText": {
                    return new LocalizedText(
                        jsonObject.get("locale").getAsString(),
                        jsonObject.get("text").getAsString()
                    );
                }

                default:
                    return jsonObject;
            }
        } else if (member.isJsonPrimitive()) {
            JsonPrimitive primitive = member.getAsJsonPrimitive();

            if (primitive.isBoolean()) {
                return primitive.getAsBoolean();
            } else if (primitive.isString()) {
                switch (typeName) {
                    case "Guid":
                        return UUID.fromString(primitive.getAsString());

                    case "NodeId":
                        return NodeId.parseSafe(primitive.getAsString()).orElse(NodeId.NULL_VALUE);

                    case "ExpandedNodeId":
                        return ExpandedNodeId.parse(primitive.getAsString());

                    case "XmlElement":
                        return new XmlElement(primitive.getAsString());

                    default:
                        return primitive.getAsString();
                }
            } else if (primitive.isNumber()) {
                switch (typeName) {
                    case "Bit":
                        return primitive.getAsInt();

                    case "SByte":
                        return primitive.getAsByte();
                    case "Int16":
                        return primitive.getAsShort();
                    case "Int32":
                        return primitive.getAsInt();
                    case "Int64":
                        return primitive.getAsLong();

                    case "Byte":
                        return ubyte(primitive.getAsShort());
                    case "UInt16":
                        return ushort(primitive.getAsInt());
                    case "UInt32":
                        return uint(primitive.getAsLong());
                    case "UInt64":
                        return ulong(primitive.getAsBigInteger());

                    case "Float":
                        return primitive.getAsFloat();
                    case "Double":
                        return primitive.getAsDouble();

                    case "DateTime":
                        return new DateTime(primitive.getAsLong());

                    case "StatusCode":
                        return new StatusCode(primitive.getAsLong());

                    default:
                        return primitive.getAsNumber();
                }
            }
        }

        return null;
    }

    @Override
    protected Object memberTypeToOpcUaArray(JsonElement member, String typeName) {
        if ("Bit".equals(typeName)) {
            return member.getAsJsonPrimitive().getAsInt();
        } else {
            JsonArray array = member.getAsJsonArray();

            Object[] values = new Object[array.size()];

            for (int i = 0; i < array.size(); i++) {
                JsonElement element = array.get(i);
                values[i] = memberTypeToOpcUaScalar(element, typeName);
            }

            return values;
        }
    }

    @Override
    protected Map<String, JsonElement> getMembers(JsonObject value) {
        LinkedHashMap<String, JsonElement> members = new LinkedHashMap<>();

        value.entrySet().forEach(e -> members.put(e.getKey(), e.getValue()));

        return members;
    }

}
