/*
 * Copyright 2015-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.nil;

import com.google.common.collect.Lists;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.packet.ChassisId;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.VlanId;
import org.onosproject.cluster.ClusterService;
import org.onosproject.cluster.NodeId;
import org.onosproject.mastership.MastershipAdminService;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.AnnotationKeys;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultAnnotations;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
import org.onosproject.net.HostLocation;
import org.onosproject.net.Link;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DefaultDeviceDescription;
import org.onosproject.net.device.DefaultPortDescription;
import org.onosproject.net.device.DeviceAdminService;
import org.onosproject.net.device.DeviceDescription;
import org.onosproject.net.device.DeviceEvent;
import org.onosproject.net.device.DeviceListener;
import org.onosproject.net.device.DeviceProviderService;
import org.onosproject.net.device.PortDescription;
import org.onosproject.net.host.DefaultHostDescription;
import org.onosproject.net.host.HostDescription;
import org.onosproject.net.host.HostProviderService;
import org.onosproject.net.host.HostService;
import org.onosproject.net.link.DefaultLinkDescription;
import org.onosproject.net.link.LinkProviderService;
import org.onosproject.net.link.LinkService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.onlab.util.Tools.toHex;
import static org.onosproject.net.HostId.hostId;
import static org.onosproject.net.Link.Type.DIRECT;
import static org.onosproject.net.MastershipRole.MASTER;
import static org.onosproject.net.PortNumber.portNumber;
import static org.onosproject.net.device.DeviceEvent.Type.*;
import static org.onosproject.provider.nil.NullProviders.SCHEME;

/**
 * Abstraction of a provider capable to simulate some network topology.
 */
public abstract class TopologySimulator {

    protected final Logger log = LoggerFactory.getLogger(getClass());

    protected String[] topoShape;
    protected int deviceCount;
    protected int hostCount;

    protected ServiceDirectory directory;
    protected NodeId localNode;

    protected ClusterService clusterService;
    protected MastershipService mastershipService;
    protected MastershipAdminService mastershipAdminService;

    protected DeviceAdminService deviceService;
    protected HostService hostService;
    protected LinkService linkService;

    protected DeviceProviderService deviceProviderService;
    protected HostProviderService hostProviderService;
    protected LinkProviderService linkProviderService;

    protected int maxWaitSeconds = 1;
    protected int infrastructurePorts = 2;
    protected CountDownLatch deviceLatch;

    protected final List<DeviceId> deviceIds = Lists.newArrayList();

    private final DeviceListener deviceEventCounter = new DeviceEventCounter();

    /**
     * Initializes a new topology simulator with access to the specified service
     * directory and various provider services.
     *
     * @param topoShape             topology shape specifier
     * @param deviceCount           number of devices in the topology
     * @param hostCount             number of hosts per device
     * @param directory             service directory
     * @param deviceProviderService device provider service
     * @param hostProviderService   host provider service
     * @param linkProviderService   link provider service
     */
    protected void init(String topoShape, int deviceCount, int hostCount,
                        ServiceDirectory directory,
                        DeviceProviderService deviceProviderService,
                        HostProviderService hostProviderService,
                        LinkProviderService linkProviderService) {
        this.deviceCount = deviceCount;
        this.hostCount = hostCount;
        this.directory = directory;

        this.clusterService = directory.get(ClusterService.class);
        this.mastershipService = directory.get(MastershipService.class);
        this.mastershipAdminService = directory.get(MastershipAdminService.class);

        this.deviceService = directory.get(DeviceAdminService.class);
        this.hostService = directory.get(HostService.class);
        this.linkService = directory.get(LinkService.class);
        this.deviceProviderService = deviceProviderService;
        this.hostProviderService = hostProviderService;
        this.linkProviderService = linkProviderService;

        localNode = clusterService.getLocalNode().id();

        processTopoShape(topoShape);
    }

    /**
     * Processes the topology shape specifier.
     *
     * @param shape topology shape specifier
     */
    protected void processTopoShape(String shape) {
        this.topoShape = shape.split(",");
    }

    /**
     * Sets up network topology simulation.
     */
    public void setUpTopology() {
        deviceIds.clear();
        prepareForDeviceEvents(deviceCount);
        createDevices();
        waitForDeviceEvents();

        createLinks();
        createHosts();
    }

    /**
     * Creates simulated devices.
     */
    protected void createDevices() {
        for (int i = 0; i < deviceCount; i++) {
            createDevice(i + 1);
        }
    }

    /**
     * Creates simulated links.
     */
    protected abstract void createLinks();

    /**
     * Creates simulated hosts.
     */
    protected abstract void createHosts();

    /**
     * Creates simulated device and adds its id to the list of devices ids.
     *
     * @param i index of the device id in the list.
     */
    protected void createDevice(int i) {
        createDevice(DeviceId.deviceId(SCHEME + ":" + toHex(i)), i);
    }

    /**
     * Creates simulated device.
     *
     * @param id        device identifier
     * @param chassisId chassis identifier number
     */
    public void createDevice(DeviceId id, int chassisId) {
        createDevice(id, chassisId, Device.Type.SWITCH, hostCount + infrastructurePorts);
    }

    /**
     * Creates simulated device.
     *
     * @param id        device identifier
     * @param chassisId chassis identifier number
     * @param type      device type
     * @param portCount number of device ports
     */
    public void createDevice(DeviceId id, int chassisId, Device.Type type, int portCount) {
        createDevice(id, chassisId, type, "0.1", "0.1.2", portCount);
    }

    /**
     * Creates simulated device.
     *
     * @param id        device identifier
     * @param chassisId chassis identifier number
     * @param type      device type
     * @param hw        hardware revision
     * @param sw        software revision
     * @param portCount number of device ports
     */
    public void createDevice(DeviceId id, int chassisId, Device.Type type,
                             String hw, String sw, int portCount) {
        DeviceDescription desc =
                new DefaultDeviceDescription(id.uri(), type, "ONF", hw, sw, "1234",
                                             new ChassisId(chassisId),
                                             DefaultAnnotations.builder()
                                                     .set(AnnotationKeys.NAME, "Switch " + chassisId)
                                                     .build());
        deviceIds.add(id);
        mastershipAdminService.setRoleSync(localNode, id, MASTER);
        deviceProviderService.deviceConnected(id, desc);
        deviceProviderService.updatePorts(id, buildPorts(portCount));
    }

    /**
     * Creates simulated link between two devices.
     *
     * @param i  index of one simulated device
     * @param j  index of another simulated device
     * @param pi port number of i-th device
     * @param pj port number of j-th device
     */
    public void createLink(int i, int j, int pi, int pj) {
        ConnectPoint one = new ConnectPoint(deviceIds.get(i), PortNumber.portNumber(pi));
        ConnectPoint two = new ConnectPoint(deviceIds.get(j), PortNumber.portNumber(pj));
        createLink(one, two);
    }

    /**
     * Creates simulated link between two connection points.
     *
     * @param one one connection point
     * @param two another connection point
     */
    public void createLink(ConnectPoint one, ConnectPoint two) {
        createLink(one, two, DIRECT, true);
    }

    /**
     * Creates simulated link between two connection points.
     *
     * @param one             one connection point
     * @param two             another connection point
     * @param type            link type
     * @param isBidirectional true if link is bidirectional
     */
    public void createLink(ConnectPoint one, ConnectPoint two, Link.Type type, boolean isBidirectional) {
        linkProviderService.linkDetected(new DefaultLinkDescription(one, two, type));
        if (isBidirectional) {
            linkProviderService.linkDetected(new DefaultLinkDescription(two, one, type));
        }
    }

    /**
     * Creates simularted hosts for the specified device.
     *
     * @param deviceId   device identifier
     * @param portOffset port offset where to start attaching hosts
     */
    public void createHosts(DeviceId deviceId, int portOffset) {
        String s = deviceId.toString();
        byte dByte = Byte.parseByte(s.substring(s.length() - 2), 16);
        // TODO: this limits the simulation to 256 devices & 256 hosts/device.
        byte[] macBytes = new byte[]{0, 0, 0, 0, dByte, 0};
        byte[] ipBytes = new byte[]{(byte) 192, (byte) 168, dByte, 0};

        for (int i = 0; i < hostCount; i++) {
            int port = portOffset + i + 1;
            macBytes[5] = (byte) (i + 1);
            ipBytes[3] = (byte) (i + 1);
            HostId id = hostId(MacAddress.valueOf(macBytes), VlanId.NONE);
            IpAddress ip = IpAddress.valueOf(IpAddress.Version.INET, ipBytes);
            hostProviderService.hostDetected(id, description(i, id, ip, deviceId, port), false);
        }
    }

    /**
     * Prepares to count device added/available/removed events.
     *
     * @param count number of events to count
     */
    protected void prepareForDeviceEvents(int count) {
        deviceLatch = new CountDownLatch(count);
        deviceService.addListener(deviceEventCounter);
    }

    /**
     * Waits for all expected device added/available/removed events.
     */
    protected void waitForDeviceEvents() {
        try {
            deviceLatch.await(maxWaitSeconds, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.warn("Device events did not arrive in time");
            Thread.currentThread().interrupt();
        }
        deviceService.removeListener(deviceEventCounter);
    }


    /**
     * Tears down network topology simulation.
     */
    public void tearDownTopology() {
        removeHosts();
        removeLinks();
        removeDevices();
    }

    /**
     * Removes any hosts previously advertised by this provider.
     */
    protected void removeHosts() {
        hostService.getHosts()
                .forEach(host -> hostProviderService.hostVanished(host.id()));
    }

    /**
     * Removes any links previously advertised by this provider.
     */
    protected void removeLinks() {
        linkService.getLinks()
                .forEach(link -> linkProviderService.linkVanished(description(link)));
    }

    /**
     * Removes any devices previously advertised by this provider.
     */
    protected void removeDevices() {
        prepareForDeviceEvents(deviceIds.size());
        deviceIds.forEach(deviceProviderService::deviceDisconnected);
        waitForDeviceEvents();
        mastershipService.getDevicesOf(localNode).forEach(mastershipService::relinquishMastership);
        deviceIds.forEach(mastershipService::relinquishMastership);
        deviceIds.clear();
    }


    /**
     * Produces a device description from the given device.
     *
     * @param device device to copy
     * @return device description
     */
    static DeviceDescription description(Device device) {
        return new DefaultDeviceDescription(device.id().uri(), device.type(),
                                            device.manufacturer(),
                                            device.hwVersion(), device.swVersion(),
                                            device.serialNumber(), device.chassisId(),
                                            DefaultAnnotations.builder()
                                                    .putAll(device.annotations())
                                                    .build());
    }

    /**
     * Produces a link description from the given link.
     *
     * @param link link to copy
     * @return link description
     */
    static DefaultLinkDescription description(Link link) {
        return new DefaultLinkDescription(link.src(), link.dst(), link.type());
    }

    /**
     * Produces a host description from the given host.
     *
     * @param host host to copy
     * @return host description
     */
    static DefaultHostDescription description(Host host) {
        return new DefaultHostDescription(host.mac(), host.vlan(), host.location(),
                                          host.ipAddresses(),
                                          DefaultAnnotations.builder()
                                                  .putAll(host.annotations())
                                                  .build());
    }

    /**
     * Generates a host description from the given id and location information.
     *
     * @param index    host index for friendly name
     * @param hostId   host identifier
     * @param ip       host IP
     * @param deviceId edge device
     * @param port     edge port
     * @return host description
     */
    static HostDescription description(int index, HostId hostId, IpAddress ip,
                                       DeviceId deviceId, int port) {
        HostLocation location = new HostLocation(deviceId, portNumber(port), 0L);
        return new DefaultHostDescription(hostId.mac(), hostId.vlanId(), location, ip,
                                          DefaultAnnotations.builder()
                                                  .set(AnnotationKeys.NAME, "Host " + index)
                                                  .build());
    }

    /**
     * Generates a list of a configured number of ports.
     *
     * @param portCount number of ports
     * @return list of ports
     */
    protected List<PortDescription> buildPorts(int portCount) {
        List<PortDescription> ports = Lists.newArrayList();
        for (int i = 1; i <= portCount; i++) {
            ports.add(DefaultPortDescription.builder()
                    .withPortNumber(PortNumber.portNumber(i))
                    .isEnabled(true)
                    .type(Port.Type.COPPER)
                    .portSpeed(0)
                    .build());
        }
        return ports;
    }

    /**
     * Indicates whether or not the simulation deeps the device as available.
     *
     * @param deviceId device identifier
     * @return true if device is known
     */
    public boolean contains(DeviceId deviceId) {
        return deviceIds.contains(deviceId);
    }

    // Counts down number of device added/available/removed events.
    private class DeviceEventCounter implements DeviceListener {
        @Override
        public void event(DeviceEvent event) {
            DeviceEvent.Type type = event.type();
            if (type == DEVICE_ADDED || type == DEVICE_REMOVED ||
                    type == DEVICE_AVAILABILITY_CHANGED) {
                deviceLatch.countDown();
            }
        }
    }
}
