package org.wso2.carbon.device.mgt.ios.payload.enrollment;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.wso2.carbon.device.mgt.ios.payload.dto.APNSStatus;
import org.wso2.carbon.device.mgt.ios.payload.dto.CheckInMessageType;
import org.wso2.carbon.device.mgt.ios.payload.dto.DeviceProperties;
import org.wso2.carbon.device.mgt.ios.payload.exception.PListException;
import org.wso2.carbon.device.mgt.ios.payload.plist.PListConstants;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class EnrollmentAttributeExtractor {

    private static final String KEY = "key";
    private static final String DICTIONARY = "dict";
    private static final String IMEI = "IMEI";
    private static final String PRODUCT = "PRODUCT";
    private static final String SERIAL = "SERIAL";
    private static final String UDID = "UDID";
    private static final String MAC_ADDRESS = "MAC_ADDRESS_EN0";
    private static final String DEVICE_NAME = "DEVICE_NAME";
    private static final String ICCID = "ICCID";
    private static final String VERSION = "VERSION";
    private static final String PUSH_MAGIC = "PushMagic";
    private static final String STATUS = "Status";
    private static final String STATE = "State";
    private static final String ERROR = "ErrorChain";
    private static final String MESSAGE_TYPE = "MessageType";
    private static final String QUERY_RESPONSES = "QueryResponses";
    private static final String INSTALLED_APPLICATION_LIST = "InstalledApplicationList";
    private static final String PROFILE_LIST = "ProfileList";
    private static final String RESTRICTIONS = "Restrictions";
    private static final String GLOBAL_RESTRICTIONS = "GlobalRestrictions";
    private static final String PROFILE_RESTRICTIONS = "ProfileRestrictions";
    private static final String TOKEN = "Token";
    private static final String TOPIC = "Topic";
    private static final String UNLOCK_TOKEN = "UnlockToken";
    private static final String CHALLENGE = "CHALLENGE";
    private static final String COMMAND_UUID = "CommandUUID";

    public DeviceProperties extractDeviceProperties(String xmlProperties)
            throws PListException {

        Document document = getDocumentElement(xmlProperties);
        if (document.getChildNodes().getLength() == 0) {
            throw new PListException("Device properties not available");
        }

        NodeList childNodes = document.getElementsByTagName(DICTIONARY);
        if (childNodes == null || childNodes.item(0) == null || childNodes.item(0).getChildNodes().getLength() == 0) {
            throw new PListException("Device properties not available");
        }
        childNodes = childNodes.item(0).getChildNodes();
        DeviceProperties deviceProperties = new DeviceProperties();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            String childNodeName = childNode.getNodeName();
            if (!KEY.equals(childNodeName)) {
                continue;
            }
            Node valueNode = childNodes.item(i + 2);
            if (valueNode == null) {
                continue;
            }

            String keyValue = valueNode.getTextContent();
            String childNodeValue = childNode.getTextContent();

            if (IMEI.equals(childNodeValue)) {
                deviceProperties.setIMEI(keyValue);
            } else if (PRODUCT.equals(childNodeValue)) {
                deviceProperties.setProduct(keyValue);
            } else if (SERIAL.equals(childNodeValue)) {
                deviceProperties.setSerial(keyValue);
            } else if (UDID.equals(childNodeValue)) {
                deviceProperties.setDeviceIdentifier(keyValue);
            } else if (VERSION.equals(childNodeValue)) {
                deviceProperties.setVersion(keyValue);
            } else if (CHALLENGE.equals(childNodeValue)) {
                deviceProperties.setChallenge(keyValue);
            } else if (MAC_ADDRESS.equals(childNodeValue)) {
                deviceProperties.setMacAddress(keyValue);
            } else if (DEVICE_NAME.equals(childNodeValue)) {
                deviceProperties.setDeviceName(keyValue);
            } else if (ICCID.equals(childNodeValue)) {
                deviceProperties.setICCID(keyValue);
            }

        }

        return deviceProperties;
    }

    public CheckInMessageType extractTokens(String xmlProperties)
            throws PListException {

        Document document = getDocumentElement(xmlProperties);

        if (document.getChildNodes().getLength() == 0) {
            throw new PListException("Device properties not available");
        }

        NodeList childNodes = document.getElementsByTagName(DICTIONARY);
        if (childNodes == null || childNodes.item(0) == null || childNodes.item(0).getChildNodes().getLength() == 0) {
            throw new PListException("Device properties not available");
        }
        childNodes = childNodes.item(0).getChildNodes();
        CheckInMessageType checkInMessageType = new CheckInMessageType();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            String childNodeName = childNode.getNodeName();
            if (!KEY.equals(childNodeName)) {
                continue;
            }
            Node valueNode = childNodes.item(i + 2);

            if (valueNode == null) {
                continue;
            }
            String keyValue = valueNode.getTextContent();
            String childNodeValue = childNode.getTextContent();

            if (MESSAGE_TYPE.equals(childNodeValue)) {
                checkInMessageType.setMessageType(CheckInMessageType.MessageType.fromString(keyValue.trim()));
            } else if (PUSH_MAGIC.equals(childNodeValue)) {
                checkInMessageType.setPushMagic(keyValue.trim());
            } else if (TOKEN.equals(childNodeValue)) {
                checkInMessageType.setToken(keyValue.trim());
            } else if (TOPIC.equals(childNodeValue)) {
                checkInMessageType.setTopic(keyValue.trim());
            } else if (UDID.equals(childNodeValue)) {
                checkInMessageType.setDeviceIdentifier(keyValue.trim());
            } else if (UNLOCK_TOKEN.equals(childNodeValue)) {
                checkInMessageType.setUnlockToken(keyValue.trim().replaceAll("\\s+", ""));
            }
        }

        return checkInMessageType;
    }

    public APNSStatus extractAPNSResponse(String xmlProperties)
            throws PListException {

        Document document = getDocumentElement(xmlProperties);

        if (document.getChildNodes().getLength() == 0) {
            throw new PListException("APNS properties not available");
        }

        NodeList childNodes = document.getElementsByTagName(DICTIONARY);
        if (childNodes == null || childNodes.item(0) == null
                || childNodes.item(0).getChildNodes().getLength() == 0) {
            throw new PListException("APNS properties not available");
        }
        childNodes = childNodes.item(0).getChildNodes();
        APNSStatus apnsStatus = new APNSStatus();

        List<JsonObject> jsonObjects = new ArrayList<>();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            String childNodeName = childNode.getNodeName();
            if (!KEY.equals(childNodeName)) {
                continue;
            }
            Node valueNode = childNodes.item(i + 2);
            if (valueNode == null) {
                continue;
            }

            String keyValue = valueNode.getTextContent();
            String childNodeValue = childNode.getTextContent();

            if (STATUS.equals(childNodeValue)) {
                apnsStatus.setStatus(keyValue.trim());
            } else if (ERROR.equals(childNodeValue)) {
                apnsStatus.setError(arrayToJson(valueNode.getChildNodes()));
            } else if (UDID.equals(childNodeValue)) {
                apnsStatus.setDeviceIdentifier(keyValue.trim());
            } else if (STATE.equals(childNodeValue)) {
                apnsStatus.setState(keyValue.trim());
            } else if (COMMAND_UUID.equals(childNodeValue)) {
                apnsStatus.setCommandUUID(keyValue.trim());
            } else if (QUERY_RESPONSES.equals(childNodeValue)) {
                apnsStatus.setOperation(QUERY_RESPONSES);
                apnsStatus.setResponseData(dictionaryToJson(valueNode.getChildNodes()));
            } else if (INSTALLED_APPLICATION_LIST.equals(childNodeValue)) {
                apnsStatus.setOperation(INSTALLED_APPLICATION_LIST);
                apnsStatus.setResponseData(arrayToJson(valueNode.getChildNodes()));
            } else if (PROFILE_LIST.equals(childNodeValue)) {
                apnsStatus.setOperation(PROFILE_LIST);
                apnsStatus.setResponseData(arrayToJson(valueNode.getChildNodes()));
            } else if (GLOBAL_RESTRICTIONS.equals(childNodeValue)) {
                apnsStatus.setOperation(RESTRICTIONS);
                jsonObjects.add(dictionaryToObject(valueNode.getChildNodes()));
            } else if (PROFILE_RESTRICTIONS.equals(childNodeValue)) {
                apnsStatus.setOperation(RESTRICTIONS);
                jsonObjects.add(dictionaryToObject(valueNode.getChildNodes()));
            }
        }

        if(RESTRICTIONS.equals(apnsStatus.getOperation())) {

            switch (jsonObjects.size()) {
                case 2: {
                    JsonObject mergedObject = mergeJSONObjects(jsonObjects.get(0), jsonObjects.get(1));
                    apnsStatus.setResponseData(mergedObject.getAsString());
                    break;
                }
                case 1: {
                    apnsStatus.setResponseData(jsonObjects.get(0).getAsString());
                    break;
                }
            }
        }

        return apnsStatus;
    }

    String dictionaryToJson(NodeList childNodes) {
        return dictionaryToObject(childNodes).toString();
    }

    JsonObject dictionaryToObject(NodeList childNodes) {

        JsonObject jsonObject = new JsonObject();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            String childNodeName = childNode.getNodeName();
            if (!KEY.equals(childNodeName)) {
                continue;
            }
            Node valueNode = childNodes.item(i + 2);
            if (valueNode == null) {
                continue;
            }

            String keyValue = valueNode.getTextContent();
            String childNodeValue = childNode.getTextContent();

            jsonObject.addProperty(childNodeValue, keyValue);
        }

        return jsonObject;
    }

    public JsonObject mergeJSONObjects(JsonObject json1, JsonObject json2) {

        JsonObject mergedJSON = json1;

        for (Map.Entry<String, JsonElement> entry : json2.entrySet()) {
            json1.addProperty(entry.getKey(), entry.getValue().getAsString());
        }

        return mergedJSON;
    }

    String arrayToJson(NodeList childNodes) {

        JsonArray jsonArray = new JsonArray();

        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            String childNodeName = childNode.getNodeName();
            if (!DICTIONARY.equals(childNodeName)) {
                continue;
            }
            NodeList innerNodes = childNode.getChildNodes();
            JsonObject innerJsonObject = dictionaryToObject(innerNodes);
            jsonArray.add(innerJsonObject);
        }

        return jsonArray.toString();
    }

    private Document getDocumentElement(String xmlProperties)
            throws PListException {
        try {
            InputStream inputStream = new ByteArrayInputStream(xmlProperties.getBytes(PListConstants.UTF_8));
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder dBuilder;
            dBuilder = dbFactory.newDocumentBuilder();
            Document document;
            String escapedXML = null;
            try {
                document = dBuilder.parse(inputStream);
            } catch (SAXException e) {
                //This could mean that there can be invalid xml characters. We try to escape them and retry parsing.
                //We don't escape on the first place due to performance reasons.
                escapedXML = stripNonValidXMLCharacters(xmlProperties);
                document = dBuilder.parse(new ByteArrayInputStream(escapedXML.getBytes(PListConstants.UTF_8)));
            }
            document.getDocumentElement().normalize();
            return document;
        } catch (ParserConfigurationException e) {
            throw new PListException("Parser configuration exception", e);
        } catch (UnsupportedEncodingException e) {
            throw new PListException("Unsupported encoding", e);
        } catch (SAXException e) {
            throw new PListException("SAX parsing error", e);
        } catch (IOException e) {
            throw new PListException("IO exception", e);
        }
    }

    private String stripNonValidXMLCharacters(String xmlString) {
        StringBuffer out = new StringBuffer();
        char current;
        for (int i = 0; i < xmlString.length(); i++) {
            current = xmlString.charAt(i);
            //Check for valid XML chars
            if ((current == 0x9) || (current == 0xA) || (current == 0xD) || ((current >= 0x20) && (current <= 0xD7FF))
                    || ((current >= 0xE000) && (current <= 0xFFFD)) || ((current >= 0x10000) && (current <= 0x10FFFF)))
                out.append(current);
        }
        return out.toString();
    }
}
