var fs = require('fs');
var path = require('path');
var et = require('elementtree');
var subElement = et.SubElement;
var common = require('../common/common.js');
var errMsg = common.messages.error;

/**
 * Object used to read and manipulate the config.xml file of an ADC
 *
 * @constructor
 * @param {String} dir Path of the ADC directory
 */
function Configurator(dir) {
    if (!dir) {
        throw new Error(errMsg.invalidPathArg);
    }
    /**
     * Path of the ADC directory
     * @type {String}
     */
    this.path   = dir;


    this.xmldoc = null;

    /**
     * Info of the ADC
     * @type {ADCInfo}
     */
    this.info = null;
}


/**
 * Read the config.xml file and initialize all properties of the current instance object
 *
 * @param {Function} [callback] Callback function
 * @param {Error} [callback.err] Error
 */
Configurator.prototype.load = function load(callback) {
    callback = callback || function () {};
    var self = this;

    common.dirExists(this.path, function (err, isExist) {
        if (err) {
            callback(err);
            return;
        }
        if (!isExist) {
            callback(errMsg.noSuchFileOrDirectory);
            return;
        }

        var filePath = path.join(self.path, 'config.xml');

        fs.readFile(filePath, function (err, data) {
            if (err) {
                callback(err);
                return;
            }

            self.xmldoc = et.parse(data.toString());

            self.info = new ADCInfo(self);
            callback(null);

        });


    });
};

/**
 * Return the configuration as xml
 *
 * @return {String}
 */
Configurator.prototype.toXml = function toXml() {
    var xml = [];

    xml.push('<?xml version="1.0" encoding="utf-8"?>');
    xml.push('<control  xmlns="http://www.askia.com/ADCSchema"' +
            '\n          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
            '\n          xsi:schemaLocation="http://www.askia.com/ADCSchema http://www.askia.com/Downloads/dev/schemas/adc2.0/Config.xsd"' +
            '\n          version="2.0.0"' +
            '\n          askiaCompat="5.3.3">');
    xml.push(this.info.toXml());
    xml.push('</control>');

    return xml.join('\n');
};

/**
 * Re-init the configurator using the xml string
 *
 * @return {String}
 */
Configurator.prototype.fromXml = function fromXml(xml) {
    this.xmldoc = et.parse(xml);
    this.info = new ADCInfo(this);
};

/**
 * ADC Info
 * @constructor
 * @param {Configurator} configurator Instance of the configurator
 */
function ADCInfo(configurator) {
    this.configurator = configurator;
}

/**
 * Get the entire information as object
 *
 * @return {Object}
 */
ADCInfo.prototype.get = function get() {
    var self = this,
        result = {};

    ["name", "guid", "version", "date", "description", "company", "author", "site",
        "helpURL", "categories", "style", "constraints"].forEach(function (methodName) {
            result[methodName] = self[methodName]();
    });
    return result;
};

/**
 * Set the information using a plain object
 *
 * @param {Object} data Data to set
 * @return {Object}
 */
ADCInfo.prototype.set = function set(data) {
    var self = this;

    if (!data) {
        return;
    }


    ["name", "guid", "version", "date", "description", "company", "author", "site",
        "helpURL", "categories", "style", "constraints"].forEach(function (methodName) {
            if (data.hasOwnProperty(methodName)) {
                self[methodName](data[methodName]);
            }
        });
};

(["name", "guid", "version", "date", "description", "company", "author", "site", "helpURL"].forEach(function (propName) {

    ADCInfo.prototype[propName] = function (data) {
        var xmldoc = this.configurator.xmldoc;
        var el = xmldoc.find("info/" + propName);
        if (data !== undefined) {
            el.text = data;
        }
        return el.text;
    };
}));

/**
 * Get or set the style
 *
 * @param {Object} [data] Style to set
 * #param {Number} [data.width] Style width
 * @param {Number} [data.height] Style height
 * @returns {Object}
 */
ADCInfo.prototype.style = function style(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("info/style");
    var result = {}, w, h;
    if (data !== undefined) {
        if (data.width !== undefined) {
            el.set("width", data.width);
        }
        if (data.height !== undefined) {
            el.set("height", data.height);
        }
    }
    w = el.get("width") || "0";
    h = el.get("height") || "0";

    result.width = parseInt(w, 10);
    result.height = parseInt(h, 10);

    return result;
};

/**
 * Get or set the categories
 *
 * @param {String[]} [data] Array of string which represent the categories to set
 * @returns {String[]} Name of categories
 */
ADCInfo.prototype.categories = function categories(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("info/categories");
    var result = [];
    if (Array.isArray(data)) {
        el.delSlice(0, el.len());
        data.forEach(function (text) {
            var cat = subElement(el, 'category');
            cat.text = text;
        });
    }

    el.iter('category', function (cat) {
        result.push(cat.text);
    });

    return result;
};

/**
 * Get or set the constraints
 *
 * @param {Object} [data] Constraint data to set
 * @return {Object} Constraints
 */
ADCInfo.prototype.constraints = function constraints(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("info/constraints");
    var result = {};
    if (data) {
        Object.keys(data).forEach(function (on) {
            if (on !== 'questions' &&  on !== 'responses' &&  on !== 'controls') {
               return;
            }
            var node = el.find("constraint[@on='" + on + "']");
            if (!node) {
                node = subElement(el, "constraint");
                node.set("on", on);
            }

            Object.keys(data[on]).forEach(function (attName) {
                var value = data[on][attName].toString();
                node.set(attName,  value);
            });

        });
    }

    el.iter('constraint', function (constraint) {
        var on = constraint.get("on");
        var value = {};

        constraint.keys().forEach(function (attName) {
            if (attName === 'on') {
                return;
            }
            var v = constraint.get(attName);
            if (attName === 'min' || attName === 'max') {
                if (v !== '*') {
                    v = parseInt(v, 10);
                }
            } else {
                v = v !== undefined && (v !== 'false' && v !== '0' );
            }

            value[attName] = v;
        });

        result[on] = value;
    });

    return result;
};

/**
 * Get or set the constraint
 *
 * @param {String} where Which constraint to target
 * @param {String} attName Name of the constraint attribute to get or set
 * @param {Boolean|Number} [attValue] Value of the attribute to set
 * @return {Boolean|Number} Value of the attribute
 */
ADCInfo.prototype.constraint = function constraint(where, attName, attValue) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("info/constraints/constraint[@on='" + where + "']");
    var result;
    if (attValue !== undefined) {
        if (!el) {
            var parent = xmldoc.find('info/constraints');
            if (!parent) {
                throw new Error("Unable to find the  `constraints` node ");
            }
            el = subElement(parent, 'constraint');
            el.set("on", where);
        }
        el.set(attName, attValue.toString());
    }

    if (!el) {
        return (attName === 'min' || attName === 'max') ? Infinity  : false;
    }

    result = el.get(attName);

    // Some properties are treat as number instead of boolean
    if (attName === 'min' || attName === 'max') {
        if (result === '*') {
            return Infinity;
        }
        return parseInt(result, 10);
    }

    if (result === undefined) {
        return false;
    }

    return (result !== "false" && result !== "0");
};

/**
 * Return the info as xml string
 *
 * @return {String}
 */
ADCInfo.prototype.toXml = function toXml() {
    var xml = [],
        self = this,
        style,
        constraints,
        constraintsKeys = ['questions', 'controls', 'responses'];

    xml.push('  <info>');



    ["name", "guid", "version", "date", "description", "company", "author", "site",
        "helpURL"].forEach(function (methodName) {
            var data = self[methodName]();
            if (methodName === 'description' || methodName === 'author') {
                data = '<![CDATA[' + data + ']]>';
            }
            xml.push('    <' + methodName + '>' + data + '</' + methodName + '>');
    });

    xml.push('    <categories>');
    self.categories().forEach(function (cat) {
        xml.push('      <category>' + cat + '</category>');
    });
    xml.push('    </categories>');

    style = self.style();
    xml.push('    <style width="' + style.width + '" height="' + style.height + '" />' );

    constraints = self.constraints();
    xml.push('    <constraints>');

    constraintsKeys.forEach(function (on) {
        if (!constraints[on]) {
            return;
        }
        var str = '      <constraint on="' + on + '"',
            constraint = constraints[on];
        for(var key in constraint) {
            if (constraint.hasOwnProperty(key)) {
                str += ' ' + key + '="' + constraint[key].toString() + '"';
            }
        }
        str += ' />';
        xml.push(str);
    });
    xml.push('    </constraints>');

    xml.push('  </info>');
    return xml.join('\n');
};

// Make it public
exports.Configurator = Configurator;