/*
 * 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.fnl.base;

import org.onosproject.fnl.intf.NetworkAnomaly;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Link;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.criteria.Criterion;
import org.onosproject.net.flow.criteria.EthCriterion;
import org.onosproject.net.flow.criteria.EthTypeCriterion;
import org.onosproject.net.flow.criteria.IPCriterion;
import org.onosproject.net.flow.criteria.IPDscpCriterion;
import org.onosproject.net.flow.criteria.IPEcnCriterion;
import org.onosproject.net.flow.criteria.IPProtocolCriterion;
import org.onosproject.net.flow.criteria.PortCriterion;
import org.onosproject.net.flow.criteria.TcpPortCriterion;
import org.onosproject.net.flow.criteria.UdpPortCriterion;
import org.onosproject.net.flow.criteria.VlanIdCriterion;
import org.onosproject.net.flow.criteria.VlanPcpCriterion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Collections;

import static org.onosproject.fnl.intf.NetworkAnomaly.Type.LOOP;
import static org.onosproject.fnl.base.TsLoopPacket.SetHeaderResult.SETHEADER_FAILURE_NULL;
import static org.onosproject.fnl.base.TsLoopPacket.SetHeaderResult.SETHEADER_OVERRIDE;
import static org.onosproject.fnl.base.TsLoopPacket.SetHeaderResult.SETHEADER_SUCCESS;
import static org.onosproject.net.flow.criteria.Criteria.*;

/**
 * Virtual packet for Default Loop Checking.
 */
public final class TsLoopPacket implements NetworkAnomaly {

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

    private static final String EOL = String.format("%n");
    private static final String LINE =
            EOL + "====================================================" + EOL;
    private static final String HDR_FMT = EOL + "---------- %s ----------" + EOL;
    private static final String LOOP_HEADER = makeHeader("Loop Header");
    private static final String LOOP_FLOW_ENTRIES = makeHeader("Loop Flow Entries");
    private static final String LOOP_LINKS = makeHeader("Loop Links");

    private Map<Criterion.Type, Criterion> match;
    private Stack<FlowEntry> pathFlow;
    // when Upgrade, check to MAKE SURE it include just Link but not EdgeLink
    private Stack<Link> pathLink;

    /**
     * Create an initial virtual packet inside for Loop Checking.
     */
    private TsLoopPacket() {
        match = new HashMap<>();
        pathFlow = new Stack<>();
        pathLink = new Stack<>();
    }

    @Override
    public Type type() {
        return LOOP;
    }

    /**
     * Represents the result of setting a header to virtual packet.
     */
    public enum SetHeaderResult {
        /**
         * Set header successfully.
         */
        SETHEADER_SUCCESS,

        /**
         * Set header successfully but override old value.
         */
        SETHEADER_OVERRIDE,

        /**
         * Fail to set Header because NULL value.
         */
        SETHEADER_FAILURE_NULL,

        /**
         * Fail to set Header, but reason is not defined, defined in advance.
         */
        SETHEADER_FAILURE
    }

    /**
     * Creates and returns a new packet instance with the copied match fields.
     *
     * With hard-copied match fields, references to path flows and path links.
     *
     * @return new loop packet instance with the copied match fields
     */
    public TsLoopPacket copyPacketMatch() {

        TsLoopPacket newOne = new TsLoopPacket();

        newOne.pathFlow = this.pathFlow;
        newOne.pathLink = this.pathLink;

        Map<Criterion.Type, Criterion> m = newOne.match;

        for (Map.Entry<Criterion.Type, Criterion> entry : this.match.entrySet()) {
            Criterion.Type k = entry.getKey();
            Criterion v = entry.getValue();

            switch (k) {
                case IN_PORT:
                    m.put(k, matchInPort(((PortCriterion) v).port()));
                    break;
                case ETH_SRC: // At present, not support Ethernet mask (ONOS?)
                    m.put(k, matchEthSrc(((EthCriterion) v).mac()));
                    break;
                case ETH_DST: // At present, not support Ethernet mask (ONOS?)
                    m.put(k, matchEthDst(((EthCriterion) v).mac()));
                    break;
                case ETH_TYPE:
                    m.put(k, matchEthType(((EthTypeCriterion) v).ethType()));
                    break;
                case VLAN_VID: // At present, not support VLAN mask (ONOS?)
                    m.put(k, matchVlanId(((VlanIdCriterion) v).vlanId()));
                    break;
                case VLAN_PCP:
                    m.put(k, matchVlanPcp(((VlanPcpCriterion) v).priority()));
                    break;
                case IPV4_SRC:
                    m.put(k, matchIPSrc(((IPCriterion) v).ip()));
                    break;
                case IPV4_DST:
                    m.put(k, matchIPDst(((IPCriterion) v).ip()));
                    break;
                case IP_PROTO:
                    m.put(k, matchIPProtocol(((IPProtocolCriterion) v).protocol()));
                    break;
                case IP_DSCP: // can't be supported by now
                    m.put(k, matchIPDscp(((IPDscpCriterion) v).ipDscp()));
                    break;
                case IP_ECN: // can't be supported by now
                    m.put(k, matchIPEcn(((IPEcnCriterion) v).ipEcn()));
                    break;
                case TCP_SRC:
                    m.put(k, matchTcpSrc(((TcpPortCriterion) v).tcpPort()));
                    break;
                case TCP_DST:
                    m.put(k, matchTcpDst(((TcpPortCriterion) v).tcpPort()));
                    break;
                case UDP_SRC:
                    m.put(k, matchUdpSrc(((UdpPortCriterion) v).udpPort()));
                    break;
                case UDP_DST:
                    m.put(k, matchUdpDst(((UdpPortCriterion) v).udpPort()));
                    break;
                default:    //can't be supported by OF1.0
                    log.debug("{} can't be supported by OF1.0", k);
                    break;
            }
        }
        return newOne;
    }

    /**
     * Sets the given criterion as a packet header field.
     *
     * @param criterion as packet header field
     * @return the result of set action
     */
    public SetHeaderResult setHeader(Criterion criterion) {

        if (criterion == null) {
            return SETHEADER_FAILURE_NULL;
        }

        boolean hasKey = match.containsKey(criterion.type());

        match.put(criterion.type(), criterion);

        return hasKey ? SETHEADER_OVERRIDE : SETHEADER_SUCCESS;
    }

    /**
     * Deletes a packet header field by the designated header type.
     *
     * @param criterionType as packet header type
     * @return true, if packet contained the corresponding type of header;
     *         false, otherwise
     */
    public boolean delHeader(Criterion.Type criterionType) {
        return match.remove(criterionType) != null;
    }

    /**
     * Returns a packet header field value by the designated header type.
     *
     * Returns null if the field does not exist.
     *
     * @param criterionType as packet header type
     * @return the packet header field value; may be null
     */
    public Criterion getHeader(Criterion.Type criterionType) {
        return match.get(criterionType);
    }

    /**
     * Returns true if there is the type of header field in the packet.
     *
     * @param criterionType packet header type
     * @return true if the field exists; false otherwise
     */
    public boolean headerExists(Criterion.Type criterionType) {
        return match.containsKey(criterionType);
    }

    /**
     * Pushes the given flow entry onto the path flow stack.
     * Packet matches this entry in specific switch hop.
     *
     * @param entry the matched entry
     */
    public void pushPathFlow(FlowEntry entry) {
        pathFlow.push(entry);
    }

    /**
     * Pops a FlowEntry from path flow entry stack.
     */
    public void popPathFlow() {
        pathFlow.pop();
    }

    /**
     * Returns links in the path which the packet passes through.
     *
     * @return an iterator over the set of links
     */
    public Iterator<Link> getPathLink() {
        return pathLink.iterator();
    }

    /**
     * Adds a Link to path link list.
     * Packet goes through this link between two switches.
     *
     * @param link The link through which the packet go
     */
    public void pushPathLink(Link link) {
        // TODO - need CPY link manual?
        pathLink.push(link);
    }

    /**
     * Removes a Link from path link list.
     */
    public void popPathLink() {
        pathLink.pop();
    }

    /**
     * Returns true if the packet passed through the specific device.
     *
     * @param deviceId identify of the device to test
     * @return true if packet passed through the specific device;
     *         false otherwise
     */
    public boolean isPassedDevice(DeviceId deviceId) {
        for (Link linkTemp : pathLink) {
            if (deviceId.equals(linkTemp.src().deviceId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the IN_PORT header field of the packet.
     *
     * Attention:
     * IN_PORT field will be changed when packet goes into the next switch hop.
     *
     * @return a port criterion object.
     */
    public PortCriterion getInport() {
        // TODO - check IN_PORT or IN_PHY_PORT
        return (PortCriterion) match.get(Criterion.Type.IN_PORT);
    }

    /**
     * Creates and returns a loop packet instance with given Match Fields.
     *
     * Returns null,
     * whenever SetHeader_FAILURE or SETHEADER_FAILURE_NULL happened.
     *
     * @param criteria match field of one flow entry
     * @param collision as return value;
     *                  true, if criteria contain multiple ones with same type
     * @return a new loop packet instance; may be null
     */
    public static TsLoopPacket matchBuilder(Iterable<Criterion> criteria,
                                            TsReturn<Boolean> collision) {

        if (null != collision) {
            collision.setValue(false);
        }

        TsLoopPacket pkt = new TsLoopPacket();

        for (Criterion criterion : criteria) {

            SetHeaderResult ret = pkt.setHeader(criterion);

            if (SETHEADER_SUCCESS == ret) {
                //TODO - in the future, we may need to resolve this condition
            } else if (SETHEADER_OVERRIDE == ret) {
                if (null != collision) {
                    collision.setValue(true);
                }
            } else { // SetHeader_FAILURE  or SetHeader_FAILURE_NULL
                pkt = null;
                break;
            }
        }

        return pkt;
    }

    /**
     * Hands in the header of virtual packet one by one.
     * Let the header go up through every layer of recursion.
     * It is called when a loop is discovered.
     *
     * @param loopPkt virtual packet that will trigger Loop Storm
     */
    public void handInLoopMatch(TsLoopPacket loopPkt) {
        match = loopPkt.match;
    }

    /**
     * Resets the path link and path flow structures.
     * And initializing the path flow with the gicen flow entry.
     *
     * @param firstEntry the flow entry from which this packet is built
     */
    public void resetLinkFlow(FlowEntry firstEntry) {
        pathLink = new Stack<>();
        pathFlow = new Stack<>();
        pathFlow.push(firstEntry);
    }

    private static String makeHeader(String title) {
        return String.format(HDR_FMT, title);
    }

    /**
     * Returns a multi-line string representation of this loop packet instance.
     *
     * @return formatted string
     */
    @Override
    public String toString() {
        StringBuilder me = new StringBuilder();

        me.append(LINE);

        me.append(LOOP_HEADER);

        List<Criterion> criteria = new ArrayList<>(match.values());
        Collections.sort(criteria, (o1, o2) -> o1.type().compareTo(o2.type()));

        for (Criterion c : criteria) {
            me.append(c).append(EOL);
        }

        me.append(LOOP_FLOW_ENTRIES);

        for (FlowEntry flow : pathFlow) {
            me.append(flow).append(EOL);
        }

        me.append(LOOP_LINKS);

        for (Link l : pathLink) {
            me.append(l).append(EOL);
        }

        return me.toString();
    }
}
