package net.onrc.onos.core.intent.runtime;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.floodlightcontroller.util.MACAddress;
import net.onrc.onos.core.intent.FlowEntry;
import net.onrc.onos.core.intent.Intent;
import net.onrc.onos.core.intent.IntentOperation;
import net.onrc.onos.core.intent.IntentOperation.Operator;
import net.onrc.onos.core.intent.IntentOperationList;
import net.onrc.onos.core.intent.PathIntent;
import net.onrc.onos.core.intent.ShortestPathIntent;
import net.onrc.onos.core.topology.LinkData;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The PlanCalcRuntime class receives a list of intents and operations (IntentOperationList),
 * and does the following:
 * <ul>
 * <li> Convert the Intent into FlowEntries.
 * <li> Determine the dependency between FlowEntries.
 * <li> Create sets of FlowEntries to be installed by the OpenFlow protocol.
 * </ul>
 */

public class PlanCalcRuntime {

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

    /**
     * The method produces a plan from a list of IntentOperations.
     * <p>
     * In this context, a plan is a list of sets of flow entries. Flow entries within
     * a set can be installed in parallel. Each set of flow entries is dependent on
     * all previous sets in the list.
     *
     * @param intentOps list of IntentOperations (Intent + Operation) for plan computation
     * @return plan (list of set of flow entries)
     */
    public List<Set<FlowEntry>> computePlan(IntentOperationList intentOps) {
        long start = System.nanoTime();
        List<Collection<FlowEntry>> flowEntries = computeFlowEntries(intentOps);
        long step1 = System.nanoTime();
        List<Set<FlowEntry>> plan = buildPhases(flowEntries);
        long step2 = System.nanoTime();
        log.debug("MEASUREMENT: Compute flow entries: {} ns, Build phases: {} ns",
                (step1 - start), (step2 - step1));
        return plan;
    }

    /**
     * Converts PathIntents to FlowEntries so they can be installed.
     * <p>
     * Note: This method only supports ShortestPathIntents at the moment.
     *
     * @param intentOps list of IntentOperations
     * @return a list of FlowEntry objects for each Intent in intentOps
     */
    private List<Collection<FlowEntry>> computeFlowEntries(IntentOperationList intentOps) {
        List<Collection<FlowEntry>> flowEntries = new LinkedList<>();
        for (IntentOperation i : intentOps) {
            if (!(i.intent instanceof PathIntent)) {
                log.warn("Not a path intent: {}", i);
                continue;
            }
            PathIntent intent = (PathIntent) i.intent;
            Intent parent = intent.getParentIntent();
            long srcPort, dstPort;
            long lastDstSw = -1, lastDstPort = -1, firstSrcSw = -1;
            MACAddress srcMac, dstMac;
            int idleTimeout = 0, hardTimeout = 0, firstSwitchIdleTimeout = 0, firstSwitchHardTimeout = 0;
            Long cookieId = null;
            int srcIP, dstIP;
            if (parent instanceof ShortestPathIntent) {
                ShortestPathIntent pathIntent = (ShortestPathIntent) parent;
                srcPort = pathIntent.getSrcPortNumber();

                if (pathIntent.getSrcMac() != ShortestPathIntent.EMPTYMACADDRESS) {
                    srcMac = MACAddress.valueOf(pathIntent.getSrcMac());
                } else {
                    srcMac = null;
                }

                if (pathIntent.getDstMac() != ShortestPathIntent.EMPTYMACADDRESS) {
                    dstMac = MACAddress.valueOf(pathIntent.getDstMac());
                } else {
                    dstMac = null;
                }

                srcIP = pathIntent.getSrcIp();
                dstIP = pathIntent.getDstIp();

                lastDstSw = pathIntent.getDstSwitchDpid();
                firstSrcSw = pathIntent.getSrcSwitchDpid();
                lastDstPort = pathIntent.getDstPortNumber();
                idleTimeout = pathIntent.getIdleTimeout();
                hardTimeout = pathIntent.getHardTimeout();
                firstSwitchIdleTimeout = pathIntent.getFirstSwitchIdleTimeout();
                firstSwitchHardTimeout = pathIntent.getFirstSwitchHardTimeout();
                try {
                    cookieId = Long.valueOf(pathIntent.getId());
                } catch (NumberFormatException e) {
                    log.trace("NumberFormatException : ", e);
                }
            } else {
                log.warn("Unsupported Intent: {}", parent);
                continue;
            }
            List<FlowEntry> entries = new ArrayList<>();
            for (LinkData linkData : intent.getPath()) {
                long sw = linkData.getSrc().getDpid().value();
                dstPort = linkData.getSrc().getPortNumber().value();
                FlowEntry fe = new FlowEntry(sw, srcPort, dstPort, srcMac, dstMac,
                                             srcIP, dstIP, i.operator);
                if (sw != firstSrcSw) {
                    fe.setIdleTimeout(idleTimeout);
                    fe.setHardTimeout(hardTimeout);
                } else {
                    fe.setIdleTimeout(firstSwitchIdleTimeout);
                    fe.setHardTimeout(firstSwitchHardTimeout);
                }
                if (cookieId != null) {
                    log.trace("cookieId is set: {}", cookieId);
                    fe.setFlowEntryId(cookieId);
                }
                entries.add(fe);
                srcPort = linkData.getDst().getPortNumber().value();
            }
            if (lastDstSw >= 0 && lastDstPort >= 0) {
                long sw = lastDstSw;
                dstPort = lastDstPort;
                FlowEntry fe = new FlowEntry(sw, srcPort, dstPort, srcMac, dstMac,
                                             srcIP, dstIP, i.operator);
                if (cookieId != null) {
                    log.trace("cookieId is set: {}", cookieId);
                    fe.setFlowEntryId(cookieId);
                }
                if (sw != firstSrcSw) {
                    fe.setIdleTimeout(idleTimeout);
                    fe.setHardTimeout(hardTimeout);
                } else {
                    fe.setIdleTimeout(firstSwitchIdleTimeout);
                    fe.setHardTimeout(firstSwitchHardTimeout);
                }
                entries.add(fe);
            }
            // reverse order of flow entries so they are installed backwards
            Collections.reverse(entries);
            flowEntries.add(entries);
        }
        return flowEntries;
    }

    /**
     * TODO
     *
     * Note: This method is for a testing purpose. Please leave it right now.
     *
     * @param flowEntries
     * @return a list of phases, where each phase is a set of FlowEntries
     */
    @SuppressWarnings("unused")
    private List<Set<FlowEntry>> simpleBuildPhases(List<Collection<FlowEntry>> flowEntries) {
        List<Set<FlowEntry>> plan = new ArrayList<>();
        Set<FlowEntry> phase = new HashSet<>();
        for (Collection<FlowEntry> c : flowEntries) {
            phase.addAll(c);
        }
        plan.add(phase);

        return plan;
    }

    /**
     * Merges the lists generated by computeFlowEntries() into install phases.
     * <p>
     * This function will also remove duplicate entries.
     *
     * @param flowEntries list of lists of flowEntries
     * @return a list of sets of FlowEntries to be installed
     */
    private List<Set<FlowEntry>> buildPhases(List<Collection<FlowEntry>> flowEntries) {
        Map<FlowEntry, Integer> map = new HashMap<>();
        List<Set<FlowEntry>> plan = new ArrayList<>();
        // merge equal FlowEntries
        //TODO: explore ways to merge FlowEntries that contain the same match condition
        for (Collection<FlowEntry> c : flowEntries) {
            for (FlowEntry e : c) {
                Integer i = map.get(e);
                if (i == null) {
                    i = Integer.valueOf(0);
                }
                switch (e.getOperator()) {
                    case ADD:
                        i += 1;
                        break;
                    case REMOVE:
                        i -= 1;
                        break;
                    default:
                        break;
                }
                map.put(e, i);
            }
        }

        // really simple first iteration of plan
        //TODO: optimize the map in phases
        Set<FlowEntry> phase = new HashSet<>();
        for (Map.Entry<FlowEntry, Integer> entry : map.entrySet()) {
            FlowEntry e = entry.getKey();
            Integer i = entry.getValue();
            if (i == 0) {
                continue;
            } else if (i > 0) {
                e.setOperator(Operator.ADD);
            } else if (i < 0) {
                e.setOperator(Operator.REMOVE);
            }
            phase.add(e);
        }
        plan.add(phase);

        return plan;
    }
}
