/*
 *    Copyright [2015] [wisemapping]
 *
 *   Licensed under WiseMapping Public License, Version 1.0 (the "License").
 *   It is basically the Apache License, Version 2.0 (the "License") plus the
 *   "powered by wisemapping" text requirement on every single page;
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the license at
 *
 *       http://www.wisemapping.org/license
 *
 *   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.
 */
const Core = require('@wismapping/core-js')
const core = Core();
const Mindmap = require('../model/Mindmap').default
const INodeModel = require('../model/INodeModel').default;
const { TopicShape } = require('../model/INodeModel');
const TopicFeature = require('../TopicFeature').default
const ConnectionLine = require('../ConnectionLine').default;

/**
 * @class
 */
const XMLSerializer_Pela = new Class(
    /** @lends XMLSerializer_Pela */ {
        /**
         * @param mindmap
         * @throws will throw an error if mindmap is null or undefined
         * @return the created XML document (using the cross-browser implementation in core)
         */
        toXML: function (mindmap) {
            $assert(mindmap, 'Can not save a null mindmap');

            var document = core.Utils.createDocument();

            // Store map attributes ...
            var mapElem = document.createElement('map');
            var name = mindmap.getId();
            if ($defined(name)) {
                mapElem.setAttribute('name', this.rmXmlInv(name));
            }
            var version = mindmap.getVersion();
            if ($defined(version)) {
                mapElem.setAttribute('version', version);
            }

            document.appendChild(mapElem);

            // Create branches ...
            var topics = mindmap.getBranches();
            for (var i = 0; i < topics.length; i++) {
                var topic = topics[i];
                var topicDom = this._topicToXML(document, topic);
                mapElem.appendChild(topicDom);
            }

            // Create Relationships
            var relationships = mindmap.getRelationships();
            if (relationships.length > 0) {
                for (var j = 0; j < relationships.length; j++) {
                    var relationship = relationships[j];
                    if (
                        mindmap.findNodeById(relationship.getFromNode()) !== null &&
                        mindmap.findNodeById(relationship.getToNode()) !== null
                    ) {
                        // Isolated relationships are not persisted ....
                        var relationDom = this._relationshipToXML(document, relationship);
                        mapElem.appendChild(relationDom);
                    }
                }
            }

            return document;
        },

        _topicToXML: function (document, topic) {
            var parentTopic = document.createElement('topic');

            // Set topic attributes...
            if (topic.getType() == INodeModel.CENTRAL_TOPIC_TYPE) {
                parentTopic.setAttribute('central', 'true');
            } else {
                var pos = topic.getPosition();
                parentTopic.setAttribute('position', pos.x + ',' + pos.y);

                var order = topic.getOrder();
                if (typeof order === 'number' && isFinite(order))
                    parentTopic.setAttribute('order', order);
            }

            var text = topic.getText();
            if ($defined(text)) {
                this._noteTextToXML(document, parentTopic, text);
            }

            var shape = topic.getShapeType();
            if ($defined(shape)) {
                parentTopic.setAttribute('shape', shape);

                if (shape == TopicShape.IMAGE) {
                    parentTopic.setAttribute(
                        'image',
                        topic.getImageSize().width +
                            ',' +
                            topic.getImageSize().height +
                            ':' +
                            topic.getImageUrl()
                    );
                }
            }

            if (
                topic.areChildrenShrunken() &&
                topic.getType() != INodeModel.CENTRAL_TOPIC_TYPE
            ) {
                parentTopic.setAttribute('shrink', 'true');
            }

            // Font properties ...
            var id = topic.getId();
            parentTopic.setAttribute('id', id);

            var font = '';

            var fontFamily = topic.getFontFamily();
            font += (fontFamily ? fontFamily : '') + ';';

            var fontSize = topic.getFontSize();
            font += (fontSize ? fontSize : '') + ';';

            var fontColor = topic.getFontColor();
            font += (fontColor ? fontColor : '') + ';';

            var fontWeight = topic.getFontWeight();
            font += (fontWeight ? fontWeight : '') + ';';

            var fontStyle = topic.getFontStyle();
            font += (fontStyle ? fontStyle : '') + ';';

            if (
                $defined(fontFamily) ||
                $defined(fontSize) ||
                $defined(fontColor) ||
                $defined(fontWeight) ||
                $defined(fontStyle)
            ) {
                parentTopic.setAttribute('fontStyle', font);
            }

            var bgColor = topic.getBackgroundColor();
            if ($defined(bgColor)) {
                parentTopic.setAttribute('bgColor', bgColor);
            }

            var brColor = topic.getBorderColor();
            if ($defined(brColor)) {
                parentTopic.setAttribute('brColor', brColor);
            }

            var metadata = topic.getMetadata();
            if ($defined(metadata)) {
                parentTopic.setAttribute('metadata', metadata);
            }

            // Serialize features ...
            var features = topic.getFeatures();
            for (var i = 0; i < features.length; i++) {
                var feature = features[i];

                var featureType = feature.getType();
                var featureDom = document.createElement(featureType);
                var attributes = feature.getAttributes();

                for (var key in attributes) {
                    var value = attributes[key];
                    if (key == 'text') {
                        var cdata = document.createCDATASection(this.rmXmlInv(value));
                        featureDom.appendChild(cdata);
                    } else {
                        featureDom.setAttribute(key, this.rmXmlInv(value));
                    }
                }
                parentTopic.appendChild(featureDom);
            }

            //CHILDREN TOPICS
            var childTopics = topic.getChildren();
            for (var j = 0; j < childTopics.length; j++) {
                var childTopic = childTopics[j];
                var childDom = this._topicToXML(document, childTopic);
                parentTopic.appendChild(childDom);
            }
            return parentTopic;
        },

        _noteTextToXML: function (document, elem, text) {
            if (text.indexOf('\n') == -1) {
                elem.setAttribute('text', this.rmXmlInv(text));
            } else {
                var textDom = document.createElement('text');
                var cdata = document.createCDATASection(this.rmXmlInv(text));
                textDom.appendChild(cdata);
                elem.appendChild(textDom);
            }
        },

        _relationshipToXML: function (document, relationship) {
            var result = document.createElement('relationship');
            result.setAttribute('srcTopicId', relationship.getFromNode());
            result.setAttribute('destTopicId', relationship.getToNode());

            var lineType = relationship.getLineType();
            result.setAttribute('lineType', lineType);
            if (
                lineType == ConnectionLine.CURVED ||
                lineType == ConnectionLine.SIMPLE_CURVED
            ) {
                if ($defined(relationship.getSrcCtrlPoint())) {
                    var srcPoint = relationship.getSrcCtrlPoint();
                    result.setAttribute(
                        'srcCtrlPoint',
                        Math.round(srcPoint.x) + ',' + Math.round(srcPoint.y)
                    );
                }
                if ($defined(relationship.getDestCtrlPoint())) {
                    var destPoint = relationship.getDestCtrlPoint();
                    result.setAttribute(
                        'destCtrlPoint',
                        Math.round(destPoint.x) + ',' + Math.round(destPoint.y)
                    );
                }
            }
            result.setAttribute('endArrow', relationship.getEndArrow());
            result.setAttribute('startArrow', relationship.getStartArrow());
            return result;
        },

        /**
         * @param dom
         * @param mapId
         * @throws will throw an error if dom is null or undefined
         * @throws will throw an error if mapId is null or undefined
         * @throws will throw an error if the document element is not consistent with a wisemap's root
         * element
         */
        loadFromDom: function (dom, mapId) {
            $assert(dom, 'dom can not be null');
            $assert(mapId, 'mapId can not be null');

            var rootElem = dom.documentElement;

            // Is a wisemap?.
            $assert(
                rootElem.tagName == XMLSerializer_Pela.MAP_ROOT_NODE,
                'This seem not to be a map document.'
            );

            this._idsMap = {};
            // Start the loading process ...
            var version = rootElem.getAttribute('version');

            var mindmap = new Mindmap(mapId, version);
            var children = rootElem.childNodes;
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (child.nodeType == 1) {
                    switch (child.tagName) {
                        case 'topic':
                            var topic = this._deserializeNode(child, mindmap);
                            mindmap.addBranch(topic);
                            break;
                        case 'relationship':
                            var relationship = this._deserializeRelationship(child, mindmap);
                            if (relationship != null) mindmap.addRelationship(relationship);
                            break;
                    }
                }
            }
            this._idsMap = null;
            mindmap.setId(mapId);
            return mindmap;
        },

        _deserializeNode: function (domElem, mindmap) {
            var type =
                domElem.getAttribute('central') != null
                    ? INodeModel.CENTRAL_TOPIC_TYPE
                    : INodeModel.MAIN_TOPIC_TYPE;

            // Load attributes...
            var id = domElem.getAttribute('id');
            if ($defined(id)) {
                id = parseInt(id);
            }

            if (this._idsMap[id]) {
                id = null;
            } else {
                this._idsMap[id] = domElem;
            }

            var topic = mindmap.createNode(type, id);

            // Set text property is it;s defined...
            var text = domElem.getAttribute('text');
            if ($defined(text) && text) {
                topic.setText(text);
            }

            var fontStyle = domElem.getAttribute('fontStyle');
            if ($defined(fontStyle) && fontStyle) {
                var font = fontStyle.split(';');

                if (font[0]) {
                    topic.setFontFamily(font[0]);
                }

                if (font[1]) {
                    topic.setFontSize(font[1]);
                }

                if (font[2]) {
                    topic.setFontColor(font[2]);
                }

                if (font[3]) {
                    topic.setFontWeight(font[3]);
                }

                if (font[4]) {
                    topic.setFontStyle(font[4]);
                }
            }

            var shape = domElem.getAttribute('shape');
            if ($defined(shape)) {
                topic.setShapeType(shape);

                if (shape == TopicShape.IMAGE) {
                    var image = domElem.getAttribute('image');
                    var size = image.substring(0, image.indexOf(':'));
                    var url = image.substring(image.indexOf(':') + 1, image.length);
                    topic.setImageUrl(url);

                    var split = size.split(',');
                    topic.setImageSize(split[0], split[1]);
                }
            }

            var bgColor = domElem.getAttribute('bgColor');
            if ($defined(bgColor)) {
                topic.setBackgroundColor(bgColor);
            }

            var borderColor = domElem.getAttribute('brColor');
            if ($defined(borderColor)) {
                topic.setBorderColor(borderColor);
            }

            var order = domElem.getAttribute('order');
            if ($defined(order) && order != 'NaN') {
                // Hack for broken maps ...
                topic.setOrder(parseInt(order));
            }

            var isShrink = domElem.getAttribute('shrink');
            // Hack: Some production maps has been stored with the central topic collapsed. This is a bug.
            if ($defined(isShrink) && type != INodeModel.CENTRAL_TOPIC_TYPE) {
                topic.setChildrenShrunken(isShrink);
            }

            var position = domElem.getAttribute('position');
            if ($defined(position)) {
                var pos = position.split(',');
                topic.setPosition(pos[0], pos[1]);
            }

            var metadata = domElem.getAttribute('metadata');
            if ($defined(metadata)) {
                topic.setMetadata(metadata);
            }

            //Creating icons and children nodes
            var children = domElem.childNodes;
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (child.nodeType == Node.ELEMENT_NODE) {
                    if (child.tagName == 'topic') {
                        var childTopic = this._deserializeNode(child, mindmap);
                        childTopic.connectTo(topic);
                    } else if (TopicFeature.isSupported(child.tagName)) {
                        // Load attributes ...
                        var namedNodeMap = child.attributes;
                        var attributes = {};
                        for (var j = 0; j < namedNodeMap.length; j++) {
                            var attribute = namedNodeMap.item(j);
                            attributes[attribute.name] = attribute.value;
                        }

                        // Has text node ?.
                        var textAttr = this._deserializeTextAttr(child);
                        if (textAttr) {
                            attributes['text'] = textAttr;
                        }

                        // Create a new element ....
                        var featureType = child.tagName;
                        var feature = TopicFeature.createModel(featureType, attributes);
                        topic.addFeature(feature);
                    } else if (child.tagName == 'text') {
                        var nodeText = this._deserializeNodeText(child);
                        topic.setText(nodeText);
                    }
                }
            }
            return topic;
        },

        _deserializeTextAttr: function (domElem) {
            var value = domElem.getAttribute('text');
            if (!$defined(value)) {
                var children = domElem.childNodes;
                for (var i = 0; i < children.length; i++) {
                    var child = children[i];
                    if (child.nodeType == Node.CDATA_SECTION_NODE) {
                        value = child.nodeValue;
                    }
                }
            } else {
                // Notes must be decoded ...
                value = unescape(value);

                // Hack for empty nodes ...
                if (value == '') {
                    value = ' ';
                }
            }

            return value;
        },

        _deserializeNodeText: function (domElem) {
            var children = domElem.childNodes;
            var value = null;
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (child.nodeType == Node.CDATA_SECTION_NODE) {
                    value = child.nodeValue;
                }
            }
            return value;
        },

        _deserializeRelationship: function (domElement, mindmap) {
            var srcId = domElement.getAttribute('srcTopicId');
            var destId = domElement.getAttribute('destTopicId');
            var lineType = domElement.getAttribute('lineType');
            var srcCtrlPoint = domElement.getAttribute('srcCtrlPoint');
            var destCtrlPoint = domElement.getAttribute('destCtrlPoint');
            var endArrow = domElement.getAttribute('endArrow');
            var startArrow = domElement.getAttribute('startArrow');
            //If for some reason a relationship lines has source and dest nodes the same, don't import it.
            if (srcId == destId) {
                return null;
            }
            // Is the connections points valid ?. If it's not, do not load the relationship ...
            if (mindmap.findNodeById(srcId) == null || mindmap.findNodeById(destId) == null) {
                return null;
            }

            var model = mindmap.createRelationship(srcId, destId);
            model.setLineType(lineType);
            if ($defined(srcCtrlPoint) && srcCtrlPoint != '') {
                model.setSrcCtrlPoint(core.Point.fromString(srcCtrlPoint));
            }
            if ($defined(destCtrlPoint) && destCtrlPoint != '') {
                model.setDestCtrlPoint(core.Point.fromString(destCtrlPoint));
            }
            model.setEndArrow('false');
            model.setStartArrow('true');
            return model;
        },

        /**
         * This method ensures that the output String has only
         * valid XML unicode characters as specified by the
         * XML 1.0 standard. For reference, please see
         * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
         * standard</a>. This method will return an empty
         * String if the input is null or empty.
         *
         * @param in The String whose non-valid characters we want to remove.
         * @return The in String, stripped of non-valid characters.
         */
        rmXmlInv: function (str) {
            if (str == null || str == undefined) return null;

            var result = '';
            for (var i = 0; i < str.length; i++) {
                var c = str.charCodeAt(i);
                if (
                    c == 0x9 ||
                    c == 0xa ||
                    c == 0xd ||
                    (c >= 0x20 && c <= 0xd7ff) ||
                    (c >= 0xe000 && c <= 0xfffd) ||
                    (c >= 0x10000 && c <= 0x10ffff)
                ) {
                    result = result + str.charAt(i);
                }
            }
            return result;
        },
    }
);

/**
 * a wisemap's root element tag name
 * @constant
 * @type {String}
 * @default
 */
XMLSerializer_Pela.MAP_ROOT_NODE = 'map';

export default XMLSerializer_Pela;