/*
 * Copyright 2019-2020 Jan Kundrát, CESNET, <jan.kundrat@cesnet.cz> and 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.drivers.czechlight;

import com.google.common.collect.Range;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.onosproject.net.PortNumber;
import org.onosproject.net.OchSignal;
import org.onosproject.net.behaviour.PowerConfig;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.driver.AbstractHandlerBehaviour;

import java.util.Arrays;
import java.util.Optional;

import org.onosproject.netconf.DatastoreId;
import org.onosproject.netconf.NetconfController;
import org.onosproject.netconf.NetconfException;
import org.onosproject.netconf.NetconfSession;
import org.slf4j.Logger;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.slf4j.LoggerFactory.getLogger;

public class CzechLightPowerConfig<T> extends AbstractHandlerBehaviour
        implements PowerConfig<T> {

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


    @Override
    public Optional<Double> getTargetPower(PortNumber port, T component) {
        return Optional.empty();
    }

    //Used by the ROADM app to set the "attenuation" parameter
    @Override
    public void setTargetPower(PortNumber port, T component, double power) {
        switch (deviceType()) {
            case LINE_DEGREE:
            case ADD_DROP_FLEX:
                if (!(component instanceof OchSignal)) {
                    log.error("Cannot set target power or anything but a Media Channel");
                    return;
                }
                HierarchicalConfiguration xml;
                try {
                    xml = doGetSubtree(CzechLightDiscovery.CHANNEL_DEFS_FILTER + CzechLightDiscovery.MC_ROUTING_FILTER);
                } catch (NetconfException e) {
                    log.error("Cannot read data from NETCONF: {}", e);
                    return;
                }
                final var allChannels = MediaChannelDefinition.parseChannelDefinitions(xml);
                final var och = ((OchSignal) component);
                final var channel = allChannels.entrySet().stream()
                        .filter(entry -> MediaChannelDefinition.mcMatches(entry, och))
                        .findAny()
                        .orElse(null);
                if (channel == null) {
                    log.error("Cannot map OCh definition {} to a channel from the channel plan", och);
                    return;
                }
                final String element = port.toLong() == CzechLightDiscovery.PORT_COMMON ? "add" : "drop";
                log.debug("{}: {} power for {} to {}", data().deviceId(), channel.getKey(), power);
                var sb = new StringBuilder();
                sb.append(CzechLightDiscovery.XML_MC_OPEN);
                sb.append("<channel>");
                sb.append(channel.getKey());
                sb.append("</channel>");
                sb.append("<");
                sb.append(element);
                sb.append("><power>");
                sb.append(power);
                sb.append("</power></");
                sb.append(element);
                sb.append(">");
                sb.append(CzechLightDiscovery.XML_MC_CLOSE);
                doEditConfig("merge", sb.toString());
                return;
            default:
                log.error("Target power is only supported on WSS-based devices");
                return;
        }
    }

    @Override
    public Optional<Double> currentPower(PortNumber port, T component) {
        if (component instanceof OchSignal) {
            // FIXME: this should be actually very easy for MCs that are routed...
            log.debug("per-MC power not implemented yet");
            return Optional.empty();
        }
        switch (deviceType()) {
            case LINE_DEGREE:
            case ADD_DROP_FLEX:
                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
                            "aggregate-power/common-out"));
                } else {
                    return Optional.ofNullable(fetchLeafSum(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
                            "media-channels[drop/port = '" +
                                    CzechLightDiscovery.leafPortName(deviceType(), port.toLong()) +
                                    "']/power/leaf-out"));
                }
            case COHERENT_ADD_DROP:
                if (component instanceof OchSignal) {
                    log.debug("Coherent Add/Drop: cannot query per-MC channel power");
                    return Optional.empty();
                }
                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
                            "aggregate-power/express-out"));
                } else {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
                            "aggregate-power/drop"));
                }
            case INLINE_AMP:
                return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_INLINE_AMP,
                        inlineAmpStageNameFor(port) + "/optical-power/output"));
            default:
                assert false : "unhandled device type";
        }
        return Optional.empty();
    }

    @Override
    public Optional<Double> currentInputPower(PortNumber port, T component)  {
        if (component instanceof OchSignal) {
            log.debug("per-MC power not implemented yet");
            return Optional.empty();
        }
        switch (deviceType()) {
            case LINE_DEGREE:
            case ADD_DROP_FLEX:
                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
                            "aggregate-power/common-in"));
                } else {
                    return Optional.ofNullable(fetchLeafSum(CzechLightDiscovery.NS_CZECHLIGHT_ROADM_DEVICE,
                            "media-channels[add/port = '" +
                                    CzechLightDiscovery.leafPortName(deviceType(), port.toLong()) +
                                    "']/power/leaf-in"));
                }
            case COHERENT_ADD_DROP:
                if (component instanceof OchSignal) {
                    log.debug("Coherent Add/Drop: cannot query per-MC channel power");
                    return Optional.empty();
                }
                if (port.toLong() == CzechLightDiscovery.PORT_COMMON) {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
                            "aggregate-power/express-in"));
                } else {
                    return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_COHERENT_A_D,
                            "client-ports[port='" + Long.toString(port.toLong()) + "']/input-power"));
                }
            case INLINE_AMP:
                return Optional.ofNullable(fetchLeafDouble(CzechLightDiscovery.NS_CZECHLIGHT_INLINE_AMP,
                        inlineAmpStageNameFor(port) + "/optical-power/input"));
            default:
                assert false : "unhandled device type";
        }
        return Optional.empty();
    }

    @Override
    public Optional<Range<Double>> getTargetPowerRange(PortNumber portNumber, T component) {
        switch (deviceType()) {
            case LINE_DEGREE:
            case ADD_DROP_FLEX:
                if (component instanceof OchSignal) {
                    // not all values might be actually set, it's complicated, so at least return some limit
                    return Optional.ofNullable(Range.closed(-25.0, 5.0));
                }
            default:
                // pass
        }
        return Optional.empty();
    }

    @Override
    public Optional<Range<Double>> getInputPowerRange(PortNumber portNumber, T component) {
        switch (deviceType()) {
            case LINE_DEGREE:
            case ADD_DROP_FLEX:
                if (component instanceof OchSignal) {
                    // not all values might be actually set, it's complicated, so at least return some limit
                    return Optional.ofNullable(Range.closed(-30.0, +10.0));
                }
            default:
                // pass
        }
        return Optional.empty();
    }

    private CzechLightDiscovery.DeviceType deviceType() {
        var annotations = this.handler().get(DeviceService.class).getDevice(handler().data().deviceId()).annotations();
        return CzechLightDiscovery.DeviceType.valueOf(annotations.value(CzechLightDiscovery.DEVICE_TYPE_ANNOTATION));
    }

    private Double fetchLeafDouble(final String namespace, final String xpath) {
        try {
            final var res = doGetXPath("M", namespace, "/M:" + xpath);
            final var key = CzechLightDiscovery.xpathToXmlKey(xpath);
            if (!res.containsKey(key)) {
                log.error("<get> reply does not contain data for key '{}'", key);
                return null;
            }
            return res.getDouble(key);
        } catch (NetconfException e) {
            log.error("Cannot read data from NETCONF: {}", e);
            return null;
        }
    }

    private Double fetchLeafSum(final String namespace, final String xpath) {
        try {
            final var data = doGetXPath("M", namespace, "/M:" + xpath);
            final var key = CzechLightDiscovery.xpathToXmlKey(xpath);
            final var power = Arrays.stream(data.getStringArray(key))
                    .map(s -> Double.valueOf(s))
                    .map(dBm -> CzechLightDiscovery.dbmToMilliwatts(dBm))
                    .reduce(0.0, Double::sum);
            log.debug(" -> power lin {}, dBm: {}", power, CzechLightDiscovery.milliwattsToDbm(power));
            return CzechLightDiscovery.milliwattsToDbm(power);
        } catch (NetconfException e) {
            log.error("Cannot read data from NETCONF: {}", e);
            return null;
        }
    }

    private String inlineAmpStageNameFor(final PortNumber port) {
        return port.toLong() == CzechLightDiscovery.PORT_INLINE_WEST ? "west-to-east" : "east-to-west";
    }

    private HierarchicalConfiguration doGetXPath(final String prefix, final String namespace, final String xpathFilter)
            throws NetconfException {
        NetconfSession session = getNetconfSession();
        if (session == null) {
            log.error("Cannot request NETCONF session for {}", data().deviceId());
            return null;
        }
        return CzechLightDiscovery.doGetXPath(session, prefix, namespace, xpathFilter);
    }

    private HierarchicalConfiguration doGetSubtree(final String subtreeXml) throws NetconfException {
        NetconfSession session = getNetconfSession();
        if (session == null) {
            log.error("Cannot request NETCONF session for {}", data().deviceId());
            return null;
        }
        return CzechLightDiscovery.doGetSubtree(session, subtreeXml);
    }

    public boolean doEditConfig(String mode, String cfg) {
        NetconfSession session = getNetconfSession();
        if (session == null) {
            log.error("Cannot request NETCONF session for {}", data().deviceId());
            return false;
        }

        try {
            return session.editConfig(DatastoreId.RUNNING, mode, cfg);
        } catch (NetconfException e) {
            throw new IllegalStateException(new NetconfException("Failed to edit configuration.", e));
        }
    }

    private NetconfSession getNetconfSession() {
        NetconfController controller =
                checkNotNull(handler().get(NetconfController.class));
        return controller.getNetconfDevice(data().deviceId()).getSession();
    }
}
