/*
 * Copyright 2017-present Open Networking Laboratory
 *
 * 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.opencord.ce.global.virtualdomain;

import com.google.common.collect.Maps;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.onosproject.core.CoreService;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Link;
import org.onosproject.net.MastershipRole;
import org.onosproject.net.Port;
import org.onosproject.net.SparseAnnotations;
import org.onosproject.net.device.DeviceEvent;
import org.onosproject.net.device.DeviceListener;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.link.DefaultLinkDescription;
import org.onosproject.net.link.LinkDescription;
import org.onosproject.net.link.LinkEvent;
import org.onosproject.net.link.LinkListener;
import org.onosproject.net.link.LinkProvider;
import org.onosproject.net.link.LinkProviderRegistry;
import org.onosproject.net.link.LinkProviderService;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.provider.ProviderId;
import org.opencord.ce.api.services.virtualprovider.LinkId;
import org.slf4j.Logger;

import java.util.Map;

import static org.onosproject.net.Link.Type.INDIRECT;
import static org.slf4j.LoggerFactory.getLogger;
import static org.opencord.ce.global.virtualdomain.VirtualDomainDeviceProvider.PROVIDER_NAME;

/**
 * Domain interlinks provider.
 */
@Component(immediate = true)
public class VirtualLinkProvider implements LinkProvider {
    public static final ProviderId PROVIDER_ID = new ProviderId("domain", PROVIDER_NAME);
    public static final String INTERLINK_ID = "interlinkId";
    private final Logger log = getLogger(getClass());

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected LinkProviderRegistry linkProviderRegistry;

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

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected LinkService linkService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected MastershipService mastershipService;

    private final Map<LinkId, ConnectPoint> stagedInterlinks = Maps.newConcurrentMap();
    private final Map<LinkId, Pair<ConnectPoint, ConnectPoint>>
            committedInterlinks = Maps.newConcurrentMap();
    private final DeviceListener deviceListener = new InternalDeviceListener();
    private final LinkListener linkListener = new InternalLinkListener();

    protected LinkProviderService linkProviderService;

    @Activate
    public void activate() {
        coreService.registerApplication(PROVIDER_NAME);
        linkProviderService = linkProviderRegistry.register(this);
        deviceService.addListener(deviceListener);
        linkService.addListener(linkListener);
        log.info("Started");

    }

    @Deactivate
    public void deactivate() {
        linkProviderRegistry.unregister(this);
        deviceService.removeListener(deviceListener);
        linkService.removeListener(linkListener);
        log.info("Stopped");
    }


    @Override
    public ProviderId id() {
        return PROVIDER_ID;
    }

    private void addInterLink(ConnectPoint point1, ConnectPoint point2, LinkId linkId) {
        SparseAnnotations annotations = DefaultAnnotations.builder()
                .set(INTERLINK_ID, linkId.id())
                .build();
        LinkDescription linkDescription = new DefaultLinkDescription(point1, point2,
                INDIRECT, true, annotations);
        linkProviderService.linkDetected(linkDescription);
    }

    private class InternalLinkListener implements LinkListener {

        @Override
        public void event(LinkEvent event) {
            Link link = event.subject();
            switch (event.type()) {
                case LINK_REMOVED:
                   if (link.annotations().keys().contains(INTERLINK_ID)) {
                       committedInterlinks.remove(LinkId.linkId(
                               link.annotations().value(INTERLINK_ID)
                       ));
                   }
                case LINK_ADDED:
                    // not relevant for now
                default:
            }
        }
    }


    private class InternalDeviceListener implements DeviceListener {
        @Override
        public boolean isRelevant(DeviceEvent event) {
            return (event.type().equals(DeviceEvent.Type.PORT_ADDED) ||
                    event.type().equals(DeviceEvent.Type.PORT_REMOVED) ||
                    event.type().equals(DeviceEvent.Type.PORT_UPDATED)) &&
                    event.port().annotations().keys().contains(INTERLINK_ID);
        }

        @Override
        public void event(DeviceEvent event) {

            Port port = event.port();
            DeviceId subjectDeviceId = event.subject().id();
            LinkId linkId = LinkId.linkId(port.annotations().value(INTERLINK_ID));

            switch (event.type()) {
                case PORT_ADDED:
                case PORT_UPDATED:
                    if (committedInterlinks.containsKey(linkId)) {
                        // !!!! ===== !!!! //
                        // committed interlinks cannot be subject to changes
                        // unless a port is removed.
                        // Possibly this is an error condition
                        log.info("notified interlinks cannot be subject to changes," +
                                "unless a port of the link is removed. Possibly this is" +
                                "an error condition");
                        return;
                    } else {
                        log.info("Adding virtual inter-links...");
                        if (stagedInterlinks.containsKey(linkId)) {
                            ConnectPoint point1 =
                                    stagedInterlinks.get(linkId);
                            ConnectPoint point2 =
                                    new ConnectPoint(subjectDeviceId, port.number());
                            if (subjectDeviceId.equals(point1.deviceId())) {
                                return;
                            }

                            committedInterlinks.put(linkId, Pair.of(point1, point2));
                            stagedInterlinks.remove(linkId);

                            // only the master instance of the destination link can notify the link
                            if (mastershipService.getLocalRole(point2.deviceId())
                                    .equals(MastershipRole.MASTER)) {
                                addInterLink(point1, point2, linkId);
                            }
                            if (mastershipService.getLocalRole(point1.deviceId())
                                    .equals(MastershipRole.MASTER)) {
                                addInterLink(point2, point1, linkId);
                            }
                        } else {
                            stagedInterlinks.put(linkId,
                                    new ConnectPoint(subjectDeviceId, port.number()));
                        }
                    }
                    break;
                case PORT_REMOVED:
                    if (committedInterlinks.containsKey(linkId)) {
                        Pair<ConnectPoint, ConnectPoint> points =
                                committedInterlinks.get(linkId);
                        committedInterlinks.remove(linkId);
                        if (points.getLeft().deviceId().equals(subjectDeviceId)) {
                            linkProviderService.linksVanished(points.getLeft());
                            stagedInterlinks.put(linkId, points.getRight());
                        } else {
                            linkProviderService.linksVanished(points.getRight());
                            stagedInterlinks.put(linkId, points.getLeft());
                        }
                    } else if (stagedInterlinks.containsKey(linkId)) {
                        stagedInterlinks.remove(linkId);
                    }
                    break;
                default:
            }
        }
    }
}
