/*
 *
 * Copyright 2013 Anis Kadri
 *
 * 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.
 *
*/

/* jshint sub:true, laxcomma:true */

/**
 * contains XML utility functions, some of which are specific to elementtree
 */

var fs = require('fs')
  , path = require('path')
  , _ = require('underscore')
  , et = require('elementtree')
  ;

  var ROOT = /^\/([^\/]*)/,
      ABSOLUTE = /^\/([^\/]*)\/(.*)/;

module.exports = {
    // compare two et.XML nodes, see if they match
    // compares tagName, text, attributes and children (recursively)
    equalNodes: function(one, two) {
        if (one.tag != two.tag) {
            return false;
        } else if (one.text.trim() != two.text.trim()) {
            return false;
        } else if (one._children.length != two._children.length) {
            return false;
        }

        var oneAttribKeys = Object.keys(one.attrib),
            twoAttribKeys = Object.keys(two.attrib),
            i = 0, attribName;

        if (oneAttribKeys.length != twoAttribKeys.length) {
            return false;
        }

        for (i; i < oneAttribKeys.length; i++) {
            attribName = oneAttribKeys[i];

            if (one.attrib[attribName] != two.attrib[attribName]) {
                return false;
            }
        }

        for (i; i < one._children.length; i++) {
            if (!module.exports.equalNodes(one._children[i], two._children[i])) {
                return false;
            }
        }

        return true;
    },

    // adds node to doc at selector, creating parent if it doesn't exist
    graftXML: function(doc, nodes, selector, after) {
        var parent = module.exports.resolveParent(doc, selector);
        if (!parent) {
            //Try to create the parent recursively if necessary
            try {
                var parentToCreate = et.XML('<' + path.basename(selector) + '>'),
                    parentSelector = path.dirname(selector);

                this.graftXML(doc, [parentToCreate], parentSelector);
            } catch (e) {
                return false;
            }
            parent = module.exports.resolveParent(doc, selector);
            if (!parent) return false;
        }

        nodes.forEach(function (node) {
            // check if child is unique first
            if (uniqueChild(node, parent)) {
                var children = parent.getchildren();
                var insertIdx = after ? findInsertIdx(children, after) : children.length;

                //TODO: replace with parent.insert after the bug in ElementTree is fixed
                parent.getchildren().splice(insertIdx, 0, node);
            }
        });

        return true;
    },

    // adds new attributes to doc at selector
    // Will only merge if attribute has not been modified already or --force is used
    graftXMLMerge: function(doc, nodes, selector, xml) {
        var target = module.exports.resolveParent(doc, selector);
        if (!target) return false;

        // saves the attributes of the original xml before making changes
        xml.oldAttrib = _.extend({}, target.attrib);

        nodes.forEach(function (node) {
            var attributes = node.attrib;
            for (var attribute in attributes) {
                target.attrib[attribute] = node.attrib[attribute];
            }
        });

        return true;
    },

    // overwrite all attributes to doc at selector with new attributes
    // Will only overwrite if attribute has not been modified already or --force is used
    graftXMLOverwrite: function(doc, nodes, selector, xml) {
        var target = module.exports.resolveParent(doc, selector);
        if (!target) return false;

        // saves the attributes of the original xml before making changes
        xml.oldAttrib = _.extend({}, target.attrib);

        // remove old attributes from target
        var targetAttributes = target.attrib;
        for (var targetAttribute in targetAttributes) {
            delete targetAttributes[targetAttribute];
        }

        // add new attributes to target
        nodes.forEach(function (node) {
            var attributes = node.attrib;
            for (var attribute in attributes) {
                target.attrib[attribute] = node.attrib[attribute];
            }
        });

        return true;
    },

    // removes node from doc at selector
    pruneXML: function(doc, nodes, selector) {
        var parent = module.exports.resolveParent(doc, selector);
        if (!parent) return false;

        nodes.forEach(function (node) {
            var matchingKid = null;
            if ((matchingKid = findChild(node, parent)) !== null) {
                // stupid elementtree takes an index argument it doesn't use
                // and does not conform to the python lib
                parent.remove(matchingKid);
            }
        });

        return true;
    },

    // restores attributes from doc at selector
    pruneXMLRestore: function(doc, selector, xml) {
        var target = module.exports.resolveParent(doc, selector);
        if (!target) return false;

        if (xml.oldAttrib) {
            target.attrib = _.extend({}, xml.oldAttrib);
        }

        return true;
    },


    parseElementtreeSync: function (filename) {
        var contents = fs.readFileSync(filename, 'utf-8');
        if(contents) {
            //Windows is the BOM. Skip the Byte Order Mark.
            contents = contents.substring(contents.indexOf('<'));
        }
        return new et.ElementTree(et.XML(contents));
    },

    resolveParent: function (doc, selector) {
        var parent, tagName, subSelector;

        // handle absolute selector (which elementtree doesn't like)
        if (ROOT.test(selector)) {
            tagName = selector.match(ROOT)[1];
            // test for wildcard "any-tag" root selector
            if (tagName == '*' || tagName === doc._root.tag) {
                parent = doc._root;

                // could be an absolute path, but not selecting the root
                if (ABSOLUTE.test(selector)) {
                    subSelector = selector.match(ABSOLUTE)[2];
                    parent = parent.find(subSelector);
                }
            } else {
                return false;
            }
        } else {
            parent = doc.find(selector);
        }
        return parent;
    }
};

function findChild(node, parent) {
    var matchingKids = parent.findall(node.tag)
      , i, j;

    for (i = 0, j = matchingKids.length ; i < j ; i++) {
        if (module.exports.equalNodes(node, matchingKids[i])) {
            return matchingKids[i];
        }
    }
    return null;
}

function uniqueChild(node, parent) {
    var matchingKids = parent.findall(node.tag)
      , i = 0;

    if (matchingKids.length === 0) {
        return true;
    } else  {
        for (i; i < matchingKids.length; i++) {
            if (module.exports.equalNodes(node, matchingKids[i])) {
                return false;
            }
        }
        return true;
    }
}

// Find the index at which to insert an entry. After is a ;-separated priority list
// of tags after which the insertion should be made. E.g. If we need to
// insert an element C, and the rule is that the order of children has to be
// As, Bs, Cs. After will be equal to "C;B;A".
function findInsertIdx(children, after) {
    var childrenTags = children.map(function(child) { return child.tag; });
    var afters = after.split(';');
    var afterIndexes = afters.map(function(current) { return childrenTags.lastIndexOf(current); });
    var foundIndex = _.find(afterIndexes, function(index) { return index != -1; });

    //add to the beginning if no matching nodes are found
    return typeof foundIndex === 'undefined' ? 0 : foundIndex+1;
}

var BLACKLIST = ['platform', 'feature','plugin','engine'];
var SINGLETONS = ['content', 'author', 'name'];
function mergeXml(src, dest, platform, clobber) {
    // Do nothing for blacklisted tags.
    if (BLACKLIST.indexOf(src.tag) != -1) return;

    //Handle attributes
    Object.getOwnPropertyNames(src.attrib).forEach(function (attribute) {
        if (clobber || !dest.attrib[attribute]) {
            dest.attrib[attribute] = src.attrib[attribute];
        }
    });
    //Handle text
    if (src.text && (clobber || !dest.text)) {
        dest.text = src.text;
    }
    //Handle children
    src.getchildren().forEach(mergeChild);

    //Handle platform
    if (platform) {
        src.findall('platform[@name="' + platform + '"]').forEach(function (platformElement) {
            platformElement.getchildren().forEach(mergeChild);
        });
    }

    //Handle duplicate preference tags (by name attribute)
    removeDuplicatePreferences(dest);

    function mergeChild (srcChild) {
        var srcTag = srcChild.tag,
            destChild = new et.Element(srcTag),
            foundChild,
            query = srcTag + '',
            shouldMerge = true;

        if (BLACKLIST.indexOf(srcTag) === -1) {
            if (SINGLETONS.indexOf(srcTag) !== -1) {
                foundChild = dest.find(query);
                if (foundChild) {
                    destChild = foundChild;
                    dest.remove(destChild);
                }
            } else {
                //Check for an exact match and if you find one don't add
                Object.getOwnPropertyNames(srcChild.attrib).forEach(function (attribute) {
                    query += '[@' + attribute + '="' + srcChild.attrib[attribute] + '"]';
                });
                var foundChildren = dest.findall(query);
                for(var i = 0; i < foundChildren.length; i++) {
                    foundChild = foundChildren[i];
                    if (foundChild && textMatch(srcChild, foundChild) && (Object.keys(srcChild.attrib).length==Object.keys(foundChild.attrib).length)) {
                        destChild = foundChild;
                        dest.remove(destChild);
                        shouldMerge = false;
                        break;
                    }
                }
            }

            mergeXml(srcChild, destChild, platform, clobber && shouldMerge);
            dest.append(destChild);
        }
    }

    function removeDuplicatePreferences(xml) {
        // reduce preference tags to a hashtable to remove dupes
        var prefHash = xml.findall('preference[@name][@value]').reduce(function(previousValue, currentValue) {
            previousValue[ currentValue.attrib.name ] = currentValue.attrib.value;
            return previousValue;
        }, {});

        // remove all preferences
        xml.findall('preference[@name][@value]').forEach(function(pref) {
            xml.remove(pref);
        });

        // write new preferences
        Object.keys(prefHash).forEach(function(key, index) {
            var element = et.SubElement(xml, 'preference');
            element.set('name', key);
            element.set('value', this[key]);
        }, prefHash);
    }
}

// Expose for testing.
module.exports.mergeXml = mergeXml;

function textMatch(elm1, elm2) {
    var text1 = elm1.text ? elm1.text.replace(/\s+/, '') : '',
        text2 = elm2.text ? elm2.text.replace(/\s+/, '') : '';
    return (text1 === '' || text1 === text2);
}
