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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.wso2.carbon.device.mgt.ios.payload.exception.PListException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class PListGenerator {

    private static final Log log = LogFactory.getLog(PListGenerator.class);
    private DateFormat dateFormat;

    public PListGenerator() {
        super();
        dateFormat = new SimpleDateFormat();
    }

    public PListGenerator(DateFormat dateFormat) {
        super();
        this.dateFormat = dateFormat;
    }

    private enum PListDataType {

        STRING("string"),
        DATE("date"),
        INTEGER("integer"),
        REAL("real"),
        DATA("data"),
        TRUE("true"),
        FALSE("false");

        private String value;

        private PListDataType(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    private Document generatePlistDocument() throws ParserConfigurationException {
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = builderFactory.newDocumentBuilder();
        DOMImplementation domImplementation = builder.getDOMImplementation();
        DocumentType documentType = domImplementation.createDocumentType(PListConstants.QUALIFIED_NAME,
                PListConstants.PUBLIC_ID, PListConstants.SYSTEM_ID);
        Document document = domImplementation.createDocument(PListConstants.NAMESPACE_URI,
                PListConstants.QUALIFIED_NAME, documentType);
        document.setXmlStandalone(true);

        return document;
    }

    private Element createRootElement(Document document) {
        Element rootElement = document.getDocumentElement();
        rootElement.setAttribute(PListConstants.ROOT_ELEMENT_VERSION_TEXT,
                PListConstants.ROOT_ELEMENT_VERSION);
        return  rootElement;
    }

    private String transformDocument(Document document) throws TransformerException {
        DOMSource domSource = new DOMSource(document);
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, PListConstants.UTF_8);
        transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, PListConstants.PUBLIC_ID);
        transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, PListConstants.SYSTEM_ID);
        transformer.setOutputProperty(OutputKeys.INDENT, PListConstants.NO);
        transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator","\n");

        StringWriter stringWriter = new StringWriter();
        StreamResult streamResult = new StreamResult(stringWriter);
        transformer.transform(domSource, streamResult);

        return stringWriter.toString();
    }

    private Element convertElementToPList(Document document, Object element) throws PListException {

        Element subElement;

        if(element == null) {
            return null;
        }else if (element instanceof List<?>) {
            return convertArrayToPlist(document, (List<?>)element);
        } else if (element instanceof String) {
            subElement = document.createElement(PListDataType.STRING.getValue());
            subElement.setTextContent((String) element);
        } else if (element instanceof Date) {
            subElement = document.createElement(PListDataType.DATE.getValue());
            subElement.setTextContent(getDateFormat().format((Date) element));
        } else if (element instanceof Integer) {
            subElement = document.createElement(PListDataType.INTEGER.getValue());
            subElement.setTextContent(String.valueOf(element));
        } else if (element instanceof Float) {
            subElement = document.createElement(PListDataType.REAL.getValue());
            subElement.setTextContent(String.valueOf(element));
        } else if (element instanceof Double) {
            subElement = document.createElement(PListDataType.REAL.getValue());
            subElement.setTextContent(String.valueOf(element));
        } else if (element instanceof BigDecimal) {
            subElement = document.createElement(PListDataType.REAL.getValue());
            subElement.setTextContent(String.valueOf(element));
        } else if (element instanceof Boolean) {
            if ((Boolean) element) {
                subElement = document.createElement(PListDataType.TRUE.getValue());
            } else {
                subElement = document.createElement(PListDataType.FALSE.getValue());
            }
        } else if (element instanceof byte[]) {
            byte[] elemByte = (byte[]) element;
            try {
                subElement = document.createElement(PListDataType.DATA.getValue());
                subElement.setTextContent(new String(elemByte, PListConstants.UTF_8));
            } catch (UnsupportedEncodingException e) {
                String errorMsg = "Unsupported encoding when converting elements to plist : ";
                log.error(errorMsg, e);
                throw new PListException(errorMsg, e);
            }

        } else {
            String errorMsg = "Invalid object type when converting elements to plist : ";
            log.error(errorMsg);
            throw new PListException(errorMsg);
        }

        return  subElement;
    }

    private Element convertMapToPList(Document document, Map<String, ?> itemMap) throws PListException {

        Element elementDictionary = document.createElement(PListConstants.PLIST_DICTIONARY);
        for (Map.Entry<String, ?> entry : itemMap.entrySet()) {
            Element keyElement = document.createElement(PListConstants.PLIST_KEY);
            keyElement.setTextContent(entry.getKey());

            Element valueElement;
            if (entry.getValue() instanceof List<?>) {
                valueElement = convertArrayToPlist(document, (List<?>) entry.getValue());
            } else if(entry.getValue() instanceof Map<?, ?>) {
                valueElement = convertMapToPList(document, (Map<String, ?> )entry.getValue());
            } else {
                valueElement = convertElementToPList(document, entry.getValue());
            }

            if (valueElement == null) {
                continue;
            }

            elementDictionary.appendChild(keyElement);
            elementDictionary.appendChild(valueElement);
        }

        return elementDictionary;
    }

    private Element convertArrayToPlist(Document document, List<?> itemArray) throws PListException {

        Element elementArray = document.createElement(PListConstants.PLIST_ARRAY);
        for (Object element : itemArray) {

            Element valueElement;
            if (element instanceof List<?>) {
                valueElement = convertArrayToPlist(document, (List<?>) element);
            } else if(element instanceof Map<?, ?>) {
                valueElement = convertMapToPList(document, (Map<String, ?>) element);
            } else {
                valueElement = convertElementToPList(document, element);
            }

            if (valueElement == null) {
                continue;
            }

            elementArray.appendChild(valueElement);
        }

        return elementArray;
    }

    public String generatePayload(Object collection) throws PListException {

        try {
            Document document = generatePlistDocument();
            Element rootElement = createRootElement(document);

            if(collection instanceof Map) {
                rootElement.appendChild(convertMapToPList(document, (Map<String, Object>)collection));
            } else if(collection instanceof List) {
                rootElement.appendChild(convertArrayToPlist(document, (List<String>) collection));
            } else {
                String errorMsg = "Unsupported collection when generating payload : ";
                log.error(errorMsg);
                throw new PListException(errorMsg);
            }

            return transformDocument(document);
        } catch (ParserConfigurationException e) {
            String errorMsg = "PList parsing error when generating payload : ";
            log.error(errorMsg, e);
            throw new PListException(errorMsg, e);
        } catch (TransformerException e) {
            String errorMsg = "PList transform error when generating payload : ";
            log.error(errorMsg, e);
            throw new PListException(errorMsg, e);
        }
    }

    DateFormat getDateFormat() {
        return dateFormat;
    }

    public void setDateFormat(SimpleDateFormat dateFormat) {
        this.dateFormat = dateFormat;
    }
}
