/*
 * Copyright 2016-present Open Networking Foundation
 *
 * 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
 *
 *     http://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 org.onosproject.provider.lisp.device.impl;

import com.google.common.base.Preconditions;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Deactivate;

import org.onlab.packet.ChassisId;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.lisp.ctl.LispController;
import org.onosproject.lisp.ctl.LispRouterId;
import org.onosproject.lisp.ctl.LispRouterListener;
import org.onosproject.net.AnnotationKeys;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.MastershipRole;
import org.onosproject.net.PortNumber;
import org.onosproject.net.SparseAnnotations;
import org.onosproject.net.device.DeviceProvider;
import org.onosproject.net.device.DeviceProviderRegistry;
import org.onosproject.net.device.DeviceProviderService;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.device.DeviceDescription;
import org.onosproject.net.device.DefaultDeviceDescription;
import org.onosproject.net.provider.AbstractProvider;
import org.onosproject.net.provider.ProviderId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;

/**
 * Provider which uses an LISP controller to detect device.
 */
@Component(immediate = true)
public class LispDeviceProvider extends AbstractProvider implements DeviceProvider {

    private static final Logger log = LoggerFactory.getLogger(LispDeviceProvider.class);

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected DeviceProviderRegistry providerRegistry;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected DeviceService deviceService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected CoreService coreService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected LispController controller;

    private static final String APP_NAME = "org.onosproject.lisp";
    private static final String SCHEME_NAME = "lisp";
    private static final String DEVICE_PROVIDER_PACKAGE = "org.onosproject.lisp.provider.device";

    private static final String MANUFACTURER = "IETF";
    private static final String HARDWARE_VERSION = "LISP Reference Router";
    private static final String SOFTWARE_VERSION = "1.0";
    private static final String SERIAL_NUMBER = "unknown";
    private static final String IS_NULL_MSG = "LISP device info is null";
    private static final String IPADDRESS = "ipaddress";
    private static final String LISP = "lisp";

    protected DeviceProviderService providerService;
    private InternalLispRouterListener routerListener = new InternalLispRouterListener();

    private ApplicationId appId;

    /**
     * Creates a LISP device provider.
     */
    public LispDeviceProvider() {
        super(new ProviderId(SCHEME_NAME, DEVICE_PROVIDER_PACKAGE));
    }

    @Activate
    public void activate() {
        providerService = providerRegistry.register(this);
        appId = coreService.registerApplication(APP_NAME);
        controller.addRouterListener(routerListener);
        log.info("Started");
    }

    @Deactivate
    public void deactivate() {
        controller.getRouters().forEach(router -> controller.disconnectRouter(
                            new LispRouterId(router.routerId()), true));
        controller.removeRouterListener(routerListener);
        providerRegistry.unregister(this);
        providerService = null;
        log.info("Stopped");
    }

    @Override
    public void triggerProbe(DeviceId deviceId) {
        log.info("Triggering probe on device {}", deviceId);
    }

    @Override
    public void roleChanged(DeviceId deviceId, MastershipRole newRole) {

    }

    @Override
    public boolean isReachable(DeviceId deviceId) {
        // TODO: need to provide a way to send probe message to LISP router,
        // to check the device reachability.
        return true;
    }

    @Override
    public void changePortState(DeviceId deviceId, PortNumber portNumber, boolean enable) {
        log.info("This operation is irrelevant for LISP router");
    }

    /**
     * Adds a LISP router into device store.
     */
    private void connectDevice(LispRouterId routerId) {
        DeviceId deviceId = getDeviceId(routerId.id().toString());
        Preconditions.checkNotNull(deviceId, IS_NULL_MSG);

        // formulate LISP router object
        ChassisId cid = new ChassisId();
        String ipAddress = routerId.id().toString();
        SparseAnnotations annotations = DefaultAnnotations.builder()
                .set(IPADDRESS, ipAddress)
                .set(AnnotationKeys.PROTOCOL, SCHEME_NAME.toUpperCase())
                .build();
        DeviceDescription deviceDescription = new DefaultDeviceDescription(
                deviceId.uri(),
                Device.Type.ROUTER,
                MANUFACTURER, HARDWARE_VERSION,
                SOFTWARE_VERSION, SERIAL_NUMBER,
                cid, false,
                annotations);
        if (deviceService.getDevice(deviceId) == null) {
            providerService.deviceConnected(deviceId, deviceDescription);
        }
        checkAndUpdateDevice(deviceId, deviceDescription);
    }

    /**
     * Checks whether a specified device is available.
     *
     * @param deviceId          device identifier
     * @param deviceDescription device description
     */
    private void checkAndUpdateDevice(DeviceId deviceId, DeviceDescription deviceDescription) {
        if (deviceService.getDevice(deviceId) == null) {
            log.warn("LISP router {} has not been added to store", deviceId);
        } else {
            boolean isReachable = isReachable(deviceId);
            if (isReachable && !deviceService.isAvailable(deviceId)) {
                // TODO: handle the mastership logic
            } else if (!isReachable && deviceService.isAvailable(deviceId)) {
                providerService.deviceDisconnected(deviceId);
            }
        }
    }

    /**
     * Listener for LISP router events.
     */
    private class InternalLispRouterListener implements LispRouterListener {

        @Override
        public void routerAdded(LispRouterId routerId) {
            connectDevice(routerId);
            log.debug("LISP router {} added to core.", routerId);
        }

        @Override
        public void routerRemoved(LispRouterId routerId) {
            Preconditions.checkNotNull(routerId, IS_NULL_MSG);

            DeviceId deviceId = getDeviceId(routerId.id().toString());
            if (deviceService.getDevice(deviceId) != null) {
                providerService.deviceDisconnected(deviceId);
                log.debug("LISP router {} removed from LISP controller", deviceId);
            } else {
                log.warn("LISP router {} does not exist in the store, " +
                         "or it may already have been removed", deviceId);
            }
        }

        @Override
        public void routerChanged(LispRouterId routerId) {

        }
    }

    /**
     * Obtains the DeviceId contains IP address of LISP router.
     *
     * @param ip IP address
     * @return DeviceId device identifier
     */
    private DeviceId getDeviceId(String ip) {
        try {
            return DeviceId.deviceId(new URI(LISP, ip, null));
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Unable to build deviceID for device "
                    + ip, e);
        }
    }
}