/**
 * Copyright (c) 2018, Intel Corporation
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Intel Corporation nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.intel.stl.api.management.qosgroups.impl;

import static com.intel.stl.api.management.XMLConstants.NAME;
import static com.intel.stl.api.management.XMLConstants.QOSGROUP;
import static com.intel.stl.api.management.XMLConstants.QOSGROUPS;

import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.intel.stl.api.IMessage;
import com.intel.stl.api.StringUtils;
import com.intel.stl.api.management.ChangeManager;
import com.intel.stl.api.management.DuplicateBaseSLException;
import com.intel.stl.api.management.DuplicateNameException;
import com.intel.stl.api.management.FMConfHelper;
import com.intel.stl.api.management.XMLUtils;
import com.intel.stl.api.management.qosgroups.IQOSGroupManagement;
import com.intel.stl.api.management.qosgroups.QOSGroup;
import com.intel.stl.api.management.qosgroups.QOSGroupException;
import com.intel.stl.api.management.qosgroups.QOSGroups;
import com.intel.stl.api.management.virtualfabrics.BaseSL;
import com.intel.stl.common.STLMessages;

public class QOSGroupManagement implements IQOSGroupManagement {
    private final static Logger log =
            LoggerFactory.getLogger(QOSGroupManagement.class);

    private final static Set<String> RESERVED = new HashSet<String>() {
        private static final long serialVersionUID = 1112569917279950166L;

        {
            add("Networking");
            add("LowPriority");
            add("HighPriority");

        }

    };

    private Boolean supportQOSGroup = null;

    private final FMConfHelper confHelp;

    private final ChangeManager changeMgr = new ChangeManager();

    /**
     * Description:
     *
     * @param confHelp
     */
    public QOSGroupManagement(FMConfHelper confHelp) {
        super();
        this.confHelp = confHelp;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.intel.stl.api.management.qosgroups.IQOSGroupManagement#
     * supportQOSGroup()
     */
    @Override
    public boolean supportQOSGroup() {
        if (supportQOSGroup == null) {
            supportQOSGroup = false;
            try {
                XMLStreamReader xsr =
                        getQOSGroupStreamReader(confHelp.getConfFile());
                if (xsr != null) {
                    try {
                        supportQOSGroup = getQOSGroups() != null;
                    } catch (QOSGroupException e) {
                        e.printStackTrace();
                    }
                }
            } catch (XMLStreamException e) {
            }
        }
        return supportQOSGroup;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.intel.stl.api.management.qosgroups.IQOSGroupManagement#
     * getReservedQOSGroups()
     */
    @Override
    public Set<String> getReservedQOSGroups() {
        return RESERVED;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.intel.stl.api.management.qosgroups.IQOSGroupManagement#getQOSGroups()
     */
    @Override
    public synchronized List<QOSGroup> getQOSGroups() throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            QOSGroups groups = unmarshal(confFile);
            List<QOSGroup> groupList = groups.getQosGroups();
            log.info("Fetch " + (groupList == null ? "0" : groupList.size())
                    + " QOSGroups from host '" + confHelp.getHost() + "'");
            return groupList;
        } catch (Exception e) {
            throw createQOSGroupException(STLMessages.STL63031_GET_QOSGS_ERR, e,
                    confHelp.getHost(), StringUtils.getErrorMessage(e));
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.intel.stl.api.management.qosgroups.IQOSGroupManagement#getQOSGroup(
     * java.lang.String)
     */
    @Override
    public synchronized QOSGroup getQOSGroup(String name)
            throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            QOSGroups groups = unmarshal(confFile);
            return groups.getQOSGroup(name);
        } catch (Exception e) {
            throw createQOSGroupException(STLMessages.STL63036_GET_QOSG_ERR, e,
                    name, confHelp.getHost(), StringUtils.getErrorMessage(e));
        }
    }

    protected QOSGroups unmarshal(File xmlFile)
            throws XMLStreamException, JAXBException {
        final XMLStreamReader xsr = getQOSGroupStreamReader(xmlFile);
        if (xsr == null) {
            throw new UnsupportedOperationException(
                    STLMessages.STL63037_NO_SUPPORT_QOSG.getDescription());
        }

        JAXBContext jc = JAXBContext.newInstance(QOSGroups.class);
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        JAXBElement<QOSGroups> jb =
                unmarshaller.unmarshal(xsr, QOSGroups.class);
        xsr.close();

        return jb.getValue();
    }

    protected XMLStreamReader getQOSGroupStreamReader(File xmlFile)
            throws XMLStreamException {
        XMLInputFactory xif = XMLInputFactory.newFactory();
        StreamSource xml = new StreamSource(xmlFile);
        final XMLStreamReader xsr = xif.createXMLStreamReader(xml);
        while (xsr.hasNext()) {
            if (xsr.isStartElement() && xsr.getLocalName().equals(QOSGROUPS)) {
                break;
            }
            xsr.next();
        }
        if (xsr.hasNext()) {
            return xsr;
        } else {
            return null;
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.intel.stl.api.management.qosgroups.IQOSGroupManagement#addQOSGroup(
     * com.intel.stl.api.management.qosgroups.QOSGroup)
     */
    @Override
    public synchronized void addQOSGroup(QOSGroup group)
            throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            uniqueNameCheck(null, group.getName());
            uniqueBaseSLCheck(null, group.getBaseSL());
            // TODO loop check
            addQOSGroup(confFile, confFile, group);
            log.info("Added QOSGroup " + group);
            changeMgr.addChange(group.getName());
        } catch (Exception e) {
            throw createQOSGroupException(STLMessages.STL63032_ADD_QOSGS_ERR, e,
                    group.getName(), confHelp.getHost(),
                    StringUtils.getErrorMessage(e));
        }

    }

    protected void addQOSGroup(File srcXml, File dstXml, QOSGroup group)
            throws Exception {
        // transfer app to DOM
        DOMResult res = new DOMResult();
        JAXBContext context = JAXBContext.newInstance(group.getClass());
        context.createMarshaller().marshal(group, res);
        Document appsDoc = (Document) res.getNode();
        Node newApp = appsDoc.getFirstChild();

        // read in old xml
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(srcXml);

        // check group in old xml
        Node groupsNode = doc.getElementsByTagName(QOSGROUPS).item(0);
        Node matchedApp = getQOSGroupByName(groupsNode, group.getName());
        if (matchedApp != null) {
            throw new IllegalArgumentException(
                    "QOSGroup '" + group.getName() + "' alreday exist!");
        }

        // append app to Applications node
        XMLUtils.appendNode(doc, groupsNode, newApp);

        // save back to xml file
        XMLUtils.writeDoc(doc, dstXml);
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.intel.stl.api.management.qosgroups.IQOSGroupManagement#removeQOSGroup
     * (java.lang.String)
     */
    @Override
    public synchronized void removeQOSGroup(String name)
            throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            removeQOSGroup(confFile, confFile, name);
            log.info("Removed QOSGroup '" + name + "'");
            changeMgr.addChange(name);
        } catch (Exception e) {
            throw createQOSGroupException(STLMessages.STL63033_REMOVE_QOSGS_ERR,
                    e, name, confHelp.getHost(),
                    StringUtils.getErrorMessage(e));
        }

    }

    protected void removeQOSGroup(File srcXml, File dstXml, String name)
            throws Exception {
        // read in old xml
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(srcXml);

        // check group in old xml
        Node groupsNod = doc.getElementsByTagName(QOSGROUPS).item(0);
        Node matchedGroup = getQOSGroupByName(groupsNod, name);
        if (matchedGroup != null) {
            XMLUtils.removeNode(doc, groupsNod, matchedGroup, name);

            // save back to xml file
            XMLUtils.writeDoc(doc, dstXml);
        } else {
            // this can happen when we create a new one and then rename it
            log.warn("Couldn't find QOSGroup '" + name + "'");
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.intel.stl.api.management.qosgroups.IQOSGroupManagement#updateQOSGroup
     * (java.lang.String, com.intel.stl.api.management.qosgroups.QOSGroup)
     */
    @Override
    public synchronized void updateQOSGroup(String oldName, QOSGroup qos)
            throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            if (!oldName.equals(qos.getName())) {
                QOSGroups groups = unmarshal(confFile);
                uniqueNameCheck(groups, qos.getName());
                uniqueBaseSLCheck(groups, qos.getBaseSL());
            }
            updateQOSGroup(confFile, confFile, oldName, qos, false);
            log.info("Updated application " + qos);
            changeMgr.addChange(oldName);
            changeMgr.addChange(qos.getName());
        } catch (Exception e) {
            throw createQOSGroupException(STLMessages.STL63034_UPDATE_QOSGS_ERR,
                    e, qos.getName(), confHelp.getHost(),
                    StringUtils.getErrorMessage(e));
        }

    }

    protected void updateQOSGroup(File srcXml, File dstXml, String oldName,
            QOSGroup group, boolean allowAdd) throws Exception {
        // transfer group to DOM
        DOMResult res = new DOMResult();
        JAXBContext context = JAXBContext.newInstance(group.getClass());
        context.createMarshaller().marshal(group, res);
        Document appsDoc = (Document) res.getNode();
        Node newApp = appsDoc.getFirstChild();

        // read in old xml
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.parse(srcXml);

        doc.adoptNode(newApp);
        // check qosgroup in old xml
        Node groupsNode = doc.getElementsByTagName(QOSGROUPS).item(0);
        Node matchedQOSGroup = getQOSGroupByName(groupsNode, oldName);
        if (matchedQOSGroup == null) {
            if (allowAdd) {
                XMLUtils.appendNode(doc, groupsNode, newApp);
            } else {
                throw new IllegalArgumentException(
                        "Couldn't find QOSGroup '" + oldName + "'");
            }
        } else {
            XMLUtils.replaceNode(doc, groupsNode, matchedQOSGroup, newApp);
        }
        XMLUtils.writeDoc(doc, dstXml);
    }

    /*
     * (non-Javadoc)
     *
     * @see com.intel.stl.api.management.qosgroups.IQOSGroupManagement#
     * addOrUpdateQOSGroup(java.lang.String,
     * com.intel.stl.api.management.qosgroups.QOSGroup)
     */
    @Override
    public synchronized void addOrUpdateQOSGroup(String oldName, QOSGroup qos)
            throws QOSGroupException {
        try {
            File confFile = confHelp.getConfFile();
            if (!oldName.equals(qos.getName())) {
                QOSGroups groups = unmarshal(confFile);
                uniqueNameCheck(groups, qos.getName());
            }
            updateQOSGroup(confFile, confFile, oldName, qos, true);
            log.info("Added or updated QOSGroup " + qos);
            changeMgr.addChange(oldName);
            changeMgr.addChange(qos.getName());
        } catch (Exception e) {
            throw createQOSGroupException(
                    STLMessages.STL63035_ADDUPDATE_QOSGS_ERR, e, qos.getName(),
                    confHelp.getHost(), StringUtils.getErrorMessage(e));
        }

    }

    protected void uniqueNameCheck(QOSGroups groups, String name)
            throws Exception {
        if (groups == null) {
            File confFile = confHelp.getConfFile();
            groups = unmarshal(confFile);
        }
        for (QOSGroup group : groups.getQosGroups()) {
            if (group.getName().equals(name)) {
                throw new DuplicateNameException(name);
            }
        }
    }

    protected void uniqueBaseSLCheck(QOSGroups groups, BaseSL baseSL)
            throws Exception {
        if (groups == null) {
            File confFile = confHelp.getConfFile();
            groups = unmarshal(confFile);
        }
        if (baseSL != null) {
            for (QOSGroup group : groups.getQosGroups()) {
                if (baseSL.equals(group.getBaseSL())) {
                    throw new DuplicateBaseSLException(baseSL);
                }
            }
        }
    }

    private Node getQOSGroupByName(Node groupsNode, String name) {
        NodeList children = groupsNode.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child.getNodeName().equals(QOSGROUP)) {
                Node nameNode = XMLUtils.getNodeByName(child, NAME);
                if (nameNode != null) {
                    if (nameNode.getTextContent().equals(name)) {
                        return child;
                    }
                }
            }
        }
        return null;
    }

    protected QOSGroupException createQOSGroupException(IMessage msg,
            Throwable error, Object... args) {
        return new QOSGroupException(msg, error, args);
    }

    public boolean hasChanges() {
        return !changeMgr.getChanges().isEmpty();
    }

    public void applyChangesTo(QOSGroupManagement target)
            throws QOSGroupException {
        List<QOSGroup> groups = getQOSGroups();
        Map<String, QOSGroup> map = new HashMap<String, QOSGroup>();
        for (QOSGroup app : groups) {
            map.put(app.getName(), app);
        }
        for (String change : changeMgr.getChanges()) {
            QOSGroup cur = map.get(change);
            if (cur == null) {
                target.removeQOSGroup(change);
            } else {
                target.addOrUpdateQOSGroup(change, cur);
            }
        }
        changeMgr.resetChanges();
    }

    public void reset() {
        supportQOSGroup = null;
        changeMgr.resetChanges();
    }
}
