/*
 * 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.tl1.device.impl;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.onlab.packet.ChassisId;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
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.config.ConfigFactory;
import org.onosproject.net.config.NetworkConfigEvent;
import org.onosproject.net.config.NetworkConfigListener;
import org.onosproject.net.config.NetworkConfigRegistry;
import org.onosproject.net.config.basics.SubjectFactories;
import org.onosproject.net.device.DefaultDeviceDescription;
import org.onosproject.net.device.DeviceAdminService;
import org.onosproject.net.device.DeviceDescription;
import org.onosproject.net.device.DeviceDescriptionDiscovery;
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.provider.AbstractProvider;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.tl1.DefaultTl1Device;
import org.onosproject.tl1.Tl1Controller;
import org.onosproject.tl1.Tl1Device;
import org.onosproject.tl1.Tl1Listener;
import org.onosproject.tl1.device.Tl1DeviceConfig;
import org.slf4j.Logger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.NoSuchElementException;
import java.util.Set;

import static org.slf4j.LoggerFactory.getLogger;

/**
 * Device provider for TL1 devices.
 * <p>
 * Sits between ONOS provider service and the TL1 controller.
 * Relies on network config subsystem to know about devices.
 */
@Component(immediate = true)
public class Tl1DeviceProvider extends AbstractProvider implements DeviceProvider {
    private static final String APP_NAME = "org.onosproject.tl1";
    private static final String PROVIDER = "org.onosproject.provider.tl1.device";
    private static final String UNKNOWN = "unknown";
    private static final int REACHABILITY_TIMEOUT = 2000;      // in milliseconds

    private final Logger log = getLogger(getClass());

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected NetworkConfigRegistry cfgRegistry;

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

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY)
    protected DeviceAdminService deviceAdminService;

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

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

    private ApplicationId appId;
    private NetworkConfigListener cfgListener = new InnerConfigListener();
    private Tl1Listener tl1Listener = new InnerTl1Listener();
    private DeviceProviderService providerService;

    private final ConfigFactory factory =
            new ConfigFactory<DeviceId, Tl1DeviceConfig>(SubjectFactories.DEVICE_SUBJECT_FACTORY,
                                                         Tl1DeviceConfig.class,
                                                         Tl1DeviceConfig.TL1) {
                @Override
                public Tl1DeviceConfig createConfig() {
                    return new Tl1DeviceConfig();
                }
            };

    @Activate
    public void activate() {
        appId = coreService.registerApplication(APP_NAME);
        providerService = providerRegistry.register(this);
        cfgRegistry.addListener(cfgListener);
        controller.addListener(tl1Listener);
        cfgRegistry.registerConfigFactory(factory);
        connectDevices();
        log.info("Started");
    }

    @Deactivate
    public void deactivate() {
        controller.removeListener(tl1Listener);
        cfgRegistry.removeListener(cfgListener);
        controller.getDeviceIds().forEach(deviceId -> {
            controller.removeDevice(deviceId);
            deviceAdminService.removeDevice(deviceId);
        });
        providerRegistry.unregister(this);
        cfgRegistry.unregisterConfigFactory(factory);
        providerService = null;
        log.info("Stopped");
    }

    public Tl1DeviceProvider() {
        super(new ProviderId(Tl1DeviceConfig.TL1, PROVIDER));
    }

    @Override
    public void triggerProbe(DeviceId deviceId) {
        // TODO
    }

    @Override
    public void roleChanged(DeviceId deviceId, MastershipRole newRole) {
        switch (newRole) {
            case MASTER:
                controller.connectDevice(deviceId);
                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.MASTER);
                log.debug("Accepting mastership role change to {} for device {}", newRole, deviceId);
                break;
            case STANDBY:
                controller.disconnectDevice(deviceId);
                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.STANDBY);
                break;
            case NONE:
                controller.disconnectDevice(deviceId);
                providerService.receivedRoleReply(deviceId, newRole, MastershipRole.NONE);
                break;
            default:
                log.error("Invalid mastership state: {}", newRole);
        }
    }

    // Assumes device is registered in TL1 controller.
    @Override
    public boolean isReachable(DeviceId deviceId) {
        try {
            // First check if device is already connected.
            // If not, try to open a socket.
            Tl1Device device = controller.getDevice(deviceId).get();
            if (device.isConnected()) {
                return true;
            }

            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(device.ip().toInetAddress(), device.port()), REACHABILITY_TIMEOUT);
            socket.close();
            return true;
        } catch (NoSuchElementException | IOException | IllegalArgumentException e) {
            log.error("Cannot reach device {}", deviceId, e);
            return false;
        }
    }

    @Override
    public void changePortState(DeviceId deviceId, PortNumber portNumber, boolean enable) {
        // TODO
    }

    //Method to register devices provided via net-cfg under devices/ tree
    private void connectDevices() {
        Set<DeviceId> deviceSubjects =
                cfgRegistry.getSubjects(DeviceId.class, Tl1DeviceConfig.class);
        deviceSubjects.forEach(deviceId -> {
            Tl1DeviceConfig config =
                    cfgRegistry.getConfig(deviceId, Tl1DeviceConfig.class);
            connectDevice(new DefaultTl1Device(config.ip(), config.port(), config.username(),
                                               config.password()));
        });
    }

    // Register a device in the core and in the TL1 controller.
    private void connectDevice(Tl1Device device) {
        try {
            // Add device to TL1 controller
            DeviceId deviceId = DeviceId.deviceId(
                    new URI(Tl1DeviceConfig.TL1, device.ip() + ":" + device.port(), null));

            if (controller.addDevice(deviceId, device)) {
                SparseAnnotations ann = DefaultAnnotations.builder()
                        .set(AnnotationKeys.PROTOCOL, Tl1DeviceConfig.TL1.toUpperCase())
                        .build();
                // Register device in the core with default parameters and mark it as unavailable
                DeviceDescription dd = new DefaultDeviceDescription(deviceId.uri(),
                                                                    Device.Type.SWITCH,
                                                                    UNKNOWN, UNKNOWN,
                                                                    UNKNOWN, UNKNOWN,
                                                                    new ChassisId(),
                                                                    false, ann);
                providerService.deviceConnected(deviceId, dd);
            }
        } catch (URISyntaxException e) {
            log.error("Skipping device {}", device, e);
        }
    }

    /**
     * Tries to update the device and port descriptions through the {@code DeviceDescriptionDiscovery} behaviour.
     *
     * @param deviceId the device
     */
    void updateDevice(DeviceId deviceId) {
        Device device = deviceService.getDevice(deviceId);

        if (!device.is(DeviceDescriptionDiscovery.class)) {
            return;
        }

        try {
            // Update device description
            DeviceDescriptionDiscovery discovery = device.as(DeviceDescriptionDiscovery.class);
            DeviceDescription dd = discovery.discoverDeviceDetails();
            if (dd == null) {
                return;
            }
            providerService.deviceConnected(deviceId,
                                            new DefaultDeviceDescription(dd, true, dd.annotations()));
            // Update ports
            providerService.updatePorts(deviceId, discovery.discoverPortDetails());
        } catch (IllegalStateException | IllegalArgumentException e) {
            log.error("Cannot update device description {}", deviceId, e);
        }
    }

    /**
     * Listener for network configuration events.
     */
    private class InnerConfigListener implements NetworkConfigListener {
        @Override
        public void event(NetworkConfigEvent event) {
            if (event.type() == NetworkConfigEvent.Type.CONFIG_ADDED) {
                if (event.configClass().equals(Tl1DeviceConfig.class)) {
                    connectDevices();
                } else {
                    log.warn("Injecting device via this Json is deprecated, " +
                                     "please put configuration under devices/");
                }
            } else if (event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED) {
                // TODO: calculate delta
                if (event.configClass().equals(Tl1DeviceConfig.class)) {
                    connectDevices();
                } else {
                    log.warn("Injecting device via this Json is deprecated, " +
                                     "please put configuration under devices/");
                }
            } else if (event.type() == NetworkConfigEvent.Type.CONFIG_REMOVED) {
                controller.getDeviceIds().forEach(deviceId -> {
                    controller.removeDevice(deviceId);
                    deviceAdminService.removeDevice(deviceId);
                });
            }
        }

        @Override
        public boolean isRelevant(NetworkConfigEvent event) {
            return (event.configClass().equals(Tl1DeviceConfig.class)) &&
                    (event.type() == NetworkConfigEvent.Type.CONFIG_ADDED ||
                            event.type() == NetworkConfigEvent.Type.CONFIG_UPDATED ||
                            event.type() == NetworkConfigEvent.Type.CONFIG_REMOVED);
        }
    }

    /**
     * Listener for TL1 events.
     */
    private class InnerTl1Listener implements Tl1Listener {
        @Override
        public void deviceConnected(DeviceId deviceId) {
            updateDevice(deviceId);
        }

        @Override
        public void deviceDisconnected(DeviceId deviceId) {
            providerService.deviceDisconnected(deviceId);
        }
    }
}
