/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.io.neeo.internal.serialization;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.type.ThingType;
import org.openhab.io.neeo.NeeoService;
import org.openhab.io.neeo.internal.NeeoBrainServlet;
import org.openhab.io.neeo.internal.NeeoConstants;
import org.openhab.io.neeo.internal.NeeoDeviceKeys;
import org.openhab.io.neeo.internal.NeeoUtil;
import org.openhab.io.neeo.internal.ServiceContext;
import org.openhab.io.neeo.internal.models.NeeoDevice;
import org.openhab.io.neeo.internal.models.NeeoDeviceChannel;
import org.openhab.io.neeo.internal.models.NeeoDeviceTiming;
import org.openhab.io.neeo.internal.models.NeeoDeviceType;
import org.openhab.io.neeo.internal.models.NeeoThingUID;

import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
 * Implementation of {@link JsonSerializer} and {@link JsonDeserializer} to serialize/deserial
 * {@link NeeoDevice}. This implementation should NOT be used in communications with the NEEO brain (use
 * {@link NeeoBrainDeviceSerializer} instead)
 *
 * @author Tim Roberts - Initial Contribution
 */
@NonNullByDefault
public class NeeoDeviceSerializer implements JsonSerializer<NeeoDevice>, JsonDeserializer<NeeoDevice> {

    /** The service */
    @Nullable
    private final NeeoService service;

    /** The service context */
    @Nullable
    private final ServiceContext context;

    /**
     * Constructs the object with no service or context
     */
    public NeeoDeviceSerializer() {
        this(null, null);
    }

    /**
     * Constructs the object from the service and context. A null service or context will suppress certain values on the
     * returned json object
     *
     * @param service the possibly null service
     * @param context the possibly null context
     */
    public NeeoDeviceSerializer(@Nullable NeeoService service, @Nullable ServiceContext context) {
        this.service = service;
        this.context = context;
    }

    @Override
    public NeeoDevice deserialize(@Nullable JsonElement elm, @Nullable Type type,
            @Nullable JsonDeserializationContext jsonContext) throws JsonParseException {
        Objects.requireNonNull(elm, "elm cannot be null");
        Objects.requireNonNull(type, "type cannot be null");
        Objects.requireNonNull(jsonContext, "jsonContext cannot be null");

        if (!(elm instanceof JsonObject)) {
            throw new JsonParseException("Element not an instance of JsonObject: " + elm);
        }

        final JsonObject jo = (JsonObject) elm;
        final NeeoThingUID uid = jsonContext.deserialize(jo.get("uid"), NeeoThingUID.class);
        final NeeoDeviceType devType = jsonContext.deserialize(jo.get("type"), NeeoDeviceType.class);
        final String manufacturer = NeeoUtil.getString(jo, "manufacturer");
        final String name = NeeoUtil.getString(jo, "name");
        final NeeoDeviceChannel[] channels = jsonContext.deserialize(jo.get("channels"), NeeoDeviceChannel[].class);
        final NeeoDeviceTiming timing = jo.has("timing")
                ? jsonContext.deserialize(jo.get("timing"), NeeoDeviceTiming.class)
                : null;

        final String[] deviceCapabilities = jo.has("deviceCapabilities")
                ? jsonContext.deserialize(jo.get("deviceCapabilities"), String[].class)
                : null;

        final String specificName = jo.has("specificName") ? jo.get("specificName").getAsString() : null;

        final String iconName = jo.has("iconName") ? jo.get("iconName").getAsString() : null;
        final int driverVersion = jo.has("driverVersion") ? jo.get("driverVersion").getAsInt() : 0;

        try {
            return new NeeoDevice(uid, driverVersion, devType,
                    manufacturer == null || StringUtils.isEmpty(manufacturer) ? NeeoUtil.NOTAVAILABLE : manufacturer,
                    name, Arrays.asList(channels), timing,
                    deviceCapabilities == null ? null : Arrays.asList(deviceCapabilities), specificName, iconName);
        } catch (NullPointerException | IllegalArgumentException e) {
            throw new JsonParseException(e);
        }
    }

    @Override
    public JsonElement serialize(NeeoDevice device, @Nullable Type deviceType,
            @Nullable JsonSerializationContext jsonContext) {
        Objects.requireNonNull(device, "device cannot be null");
        Objects.requireNonNull(deviceType, "deviceType cannot be null");
        Objects.requireNonNull(jsonContext, "jsonContext cannot be null");

        final JsonObject jsonObject = new JsonObject();

        final NeeoThingUID uid = device.getUid();
        jsonObject.add("uid", jsonContext.serialize(uid));
        jsonObject.add("type", jsonContext.serialize(device.getType()));
        jsonObject.addProperty("manufacturer", device.getManufacturer());
        jsonObject.addProperty("name", device.getName());
        jsonObject.addProperty("specificName", device.getSpecificName());
        jsonObject.addProperty("iconName", device.getIconName());
        jsonObject.addProperty("driverVersion", device.getDriverVersion());

        final JsonArray channels = (JsonArray) jsonContext.serialize(device.getChannels());

        final NeeoDeviceTiming timing = device.getDeviceTiming();
        jsonObject.add("timing", jsonContext.serialize(timing == null ? new NeeoDeviceTiming() : timing));

        jsonObject.add("deviceCapabilities", jsonContext.serialize(device.getDeviceCapabilities()));

        jsonObject.addProperty("thingType", uid.getThingType());

        if (StringUtils.equalsIgnoreCase(NeeoConstants.NEEOIO_BINDING_ID, uid.getBindingId())) {
            jsonObject.addProperty("thingStatus", uid.getThingType().toUpperCase());
        }

        final ServiceContext localContext = context;
        if (localContext != null) {
            if (!StringUtils.equalsIgnoreCase(NeeoConstants.NEEOIO_BINDING_ID, uid.getBindingId())) {
                final Thing thing = localContext.getThingRegistry().get(device.getUid().asThingUID());
                jsonObject.addProperty("thingStatus",
                        thing == null ? ThingStatus.UNKNOWN.name() : thing.getStatus().name());

                if (thing != null) {
                    final ThingType thingType = localContext.getThingTypeRegistry()
                            .getThingType(thing.getThingTypeUID());

                    if (thingType != null) {
                        for (JsonElement chnl : channels) {
                            JsonObject jo = (JsonObject) chnl;
                            if (jo.has("groupId") && jo.has("itemLabel")) {
                                final String groupId = jo.get("groupId").getAsString();
                                final String groupLabel = NeeoUtil.getGroupLabel(thingType, groupId);
                                if (StringUtils.isNotEmpty(groupLabel)) {
                                    final JsonElement itemLabel = jo.remove("itemLabel");
                                    jo.addProperty("itemLabel", groupLabel + "#" + itemLabel.getAsString());
                                } else if (StringUtils.isNotEmpty("groupId")) {
                                    // have a groupid but no group definition found (usually error on binding)
                                    // just default to "Others" like the Paperui does.
                                    final JsonElement itemLabel = jo.remove("itemLabel");
                                    jo.addProperty("itemLabel", "Others#" + itemLabel.getAsString());
                                }
                            }
                        }
                    }
                }
            }
        }

        jsonObject.add("channels", channels);

        final NeeoService localService = service;
        if (localService != null) {
            List<String> foundKeys = new ArrayList<>();
            for (final NeeoBrainServlet servlet : localService.getServlets()) {
                final NeeoDeviceKeys servletKeys = servlet.getDeviceKeys();
                final Set<String> keys = servletKeys.get(device.getUid());
                foundKeys.addAll(keys);
            }
            jsonObject.add("keys", jsonContext.serialize(foundKeys));
        }

        return jsonObject;
    }
}
