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
 *
 *      var ADC = require('adcutil').ADC;
 *
 *      var myAdc = new ADC('path/to/adc/');
 *      myAdc.load(function (err) {
 *          if (err) {
 *              throw err;
 *          }
 *
 *          // Get the instance of the Configurator
 *          var conf = myAdc.configurator;
 *
 *          console.log(conf.info.name());
 *
 *      });
 *
 *
 * @class ADC.Configurator
 */
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 {ADC.Configurator.Info}
     */
    this.info = null;

    /**
     * Outputs of the ADC
     * @type {ADC.Configurator.Outputs}
     */
    this.outputs = null;

    /**
     * Properties of the ADC
     * @type {ADC.Configurator.Properties}
     */
    this.properties = null;
}

/**
 * Create a new instance of the ADC configurator object
 *
 * @constructor
 * @param {String} dir Path of the ADC directory
 */
Configurator.prototype.constructor = Configurator;

/**
 * Read the config.xml file and initialize all properties of the current instance object
 *
 *       // Load the config file
 *       adcInfo.load(function (err) {
 *          if (err) {
 *              throw err;
 *          }
 *          console.log(adcInfo.name());
 *       });
 *
 * @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, common.CONFIG_FILE_NAME);

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

            self.fromXml(data.toString());
            callback(null);

        });

    });
};

/**
 * Get the entire configuration as object
 *
 *       // Get the info object
 *       configurator.get();
 *       // {
 *       //   info : { // .... },
 *       //   outputs : { // ... },
 *       //   properties : { // ...}
 *       // }
 *
 * @return {Object}
 */
Configurator.prototype.get = function get() {
    return {
        info : this.info.get(),
        outputs : this.outputs.get(),
        properties : this.properties.get()
    };
};

/**
 * Set th configuration using an object
 *
 *       // Get the info object
 *       configurator.set(
 *          info {
 *              name : "My ADC"
 *              version : "2.2.0.beta1",
 *              date  : "2015-06-25",
 *              guid  : "the-guid",
 *              description : "Description of the ADC"
 *              author  : "The author name",
 *              company : "The company name",
 *              site    : "http://website.url.com",
 *              helpURL : "http://help.url.com",
 *              style   : { width : 400, height : 200},
 *              categories : ["General", "Slider", "Single"],
 *              constraints : {
 *                  questions : {
 *                     single : true,
 *                     multiple : true
 *                  },
 *                  controls : {
 *                      responseBlock : true
 *                  },
 *                  responses : {
 *                      max : 10
 *                  }
 *              }
 *          },
 *          outputs : {
 *              defaultOutput : "main",
 *              outputs : [
 *                 {
 *                     id : "main",
 *                     description : "Main output",
 *                      contents : [
 *                           {
 *                              fileName : 'main.css',
 *                              type : 'css',
 *                              mode : 'static',
 *                              position : 'head'
 *                          },
 *                          {
 *                              fileName : 'main.html',
 *                              type : 'html',
 *                              mode : 'dynamic',
 *                              position : 'placeholder'
 *                          },
 *                          {
 *                              fileName : 'main.js',
 *                              type : 'javascript',
 *                              mode : 'static',
 *                              position: 'foot'
 *                          }
 *                      ]
 *                  },
 *                  {
 *                      id : "second",
 *                      description : "Second output",
 *                      condition : "Browser.Support(\"javascript\")",
 *                      contents : [
 *                          {
 *                              fileName : 'second.css',
 *                              type : 'css',
 *                              mode : 'static',
 *                              position : 'head'
 *                          },
 *                          {
 *                              fileName : 'second.html',
 *                              type : 'html',
 *                              mode : 'dynamic',
 *                              position : 'placeholder'
 *                          },
 *                          {
 *                              fileName : 'second.js',
 *                              type : 'javascript',
 *                              mode : 'static',
 *                              position : 'foot'
 *                          }
 *                      ]
 *                  },
 *                  {
 *                      id : "third",
 *                      description : "Third output",
 *                      maxIterations : 12,
 *                      defaultGeneration : false,
 *                      contents : [
 *                          {
 *                              fileName : "third.css",
 *                              type  : "css",
 *                              mode : "static",
 *                              position : "head",
 *                              attributes : [
 *                                  {
 *                                      name : "rel",
 *                                      value : "alternate"
 *                                  },
 *                                  {
 *                                      name : "media",
 *                                      value : "print"
 *                                  }
 *                              ]
 *                          },
 *                          {
 *                              fileName : 'HTML5Shim.js',
 *                              type : 'javascript',
 *                              mode : 'static',
 *                              position : 'head',
 *                              yieldValue : '<!--[if lte IE 9]><script type="text/javascript"  src="{%= CurrentADC.URLTo("static/HTML5Shim.js") %}" ></script><![endif]-->'
 *                          }
 *                      ]
 *                 }
 *              },
 *              properties : {
 *                  categories : [
 *                     {
 *                         id : "general",
 *                         description : "General",
 *                         properties  : [
 *                               {
 *                                  id : "background",
 *                                  name : "Background color",
 *                                  type : "color",
 *                                  description : "Color of the ADC background",
 *                                  colorFormat : "rgb",
 *                                  value  : "255,255,255"
 *                              }
 *                        ]
 *                     }
 *                  ]
 *              }
 *        });
 *
 * @param {Object} data Data to set
 * @param {Object} [data.info] Info data
 * @param {Object} [data.outputs] Outputs data
 * @param {Object} [data.properties] Properties data
 */
Configurator.prototype.set = function set(data) {
    if (data.info) {
        this.info.set(data.info);
    }
    if (data.outputs) {
        this.outputs.set(data.outputs);
    }
    if (data.properties) {
        this.properties.set(data.properties);
    }
};

/**
 * Return the configuration as xml
 *
 *       // Serialize the config to XML
 *       adcInfo.toXml();
 *       // -> <?xml version="1.0" encoding="utf-8"?>
 *             <control  xmlns="http://www.askia.com/ADCSchema"
 *             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 *             xsi:schemaLocation="http://www.askia.com/ADCSchema http://www.askia.com/Downloads/dev/schemas/adc2.0/Config.xsd"
 *             version="2.0.0"
 *             askiaCompat="5.3.3">
 *                 <info>
 *                     <name>My Name</name>
 *                     <guid>the-guid</guid>
 *                     ....
 *                 </info>
 *                 <outputs defaultOutput="default">
 *                     ....
 *                 </outputs>
 *                 <properties>
 *                     ....
 *                 </properties>
 *               </control>
 *
 * @return {String}
 */
Configurator.prototype.toXml = function toXml() {
    var xml = [],
        infoXml = this.info.toXml(),
        outputsXml = this.outputs.toXml(),
        propertiesXml = this.properties.toXml();

    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">');

    if (infoXml) {
        xml.push(infoXml);
    }
    if (outputsXml) {
        xml.push(outputsXml);
    }
    if (propertiesXml) {
        xml.push(propertiesXml);
    }
    xml.push('</control>');

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

/**
 * Re-init the configurator using the xml string
 *
 *       // Load the configuration using an xml string
 *       // xmlString contains information from config.xml
 *       adcInfo.fromXml(xmlString);
 *
 */
Configurator.prototype.fromXml = function fromXml(xml) {
    this.xmldoc = et.parse(xml);
    this.info = new ADCInfo(this);
    this.outputs = new ADCOutputs(this);
    this.properties = new ADCProperties(this);
};

/**
 * Save the current configuration
 *
 * @param {Function} [callback]
 * @param {Error} callback.err
 */
Configurator.prototype.save = function save(callback) {
    var filePath = path.join(this.path, common.CONFIG_FILE_NAME);
    var self = this;
    fs.writeFile(filePath, this.toXml(), {encoding : 'utf8'}, function (err) {
        if (!err) {
            self.load(callback);
        } else {
            if (typeof callback === 'function') {
                callback(err);
            }
        }
    });
};

/**
 * Provide an object to manipulate the meta-information of the ADC (config.xml > info)
 *
 *      var ADC = require('adcutil').ADC;
 *
 *      var myAdc = new ADC('path/to/adc/');
 *      myAdc.load(function (err) {
 *          if (err) {
 *              throw err;
 *          }
 *
 *          // Get the instance of the Info
 *          var info = myAdc.configurator.info;
 *
 *          console.log(info.get());
 *
 *      });
 *
 * @class ADC.Configurator.Info
 */
function ADCInfo(configurator) {
    this.configurator = configurator;
}

/**
 * Creates a new instance of ADC Info
 *
 * @constructor
 * @param {ADC.Configurator} configurator Instance of the configurator
 */
ADCInfo.prototype.constructor = ADCInfo;

/**
 * Get the entire information as object
 *
 *       // Get the info object
 *       adcInfo.get();
 *       // {
 *       //   name : "My ADC"
 *       //   version : "2.2.0.beta1",
 *       //   date  : "2015-06-25",
 *       //   guid  : "the-guid",
 *       //   description : "Description of the ADC"
 *       //   author  : "The author name",
 *       //   company : "The company name",
 *       //   site    : "http://website.url.com",
 *       //   helpURL : "http://help.url.com",
 *       //   style   : { width : 400, height : 200},
 *       //   categories : ["General", "Slider", "Single"],
 *       //   constraints : {
 *       //       questions : {
 *       //          single : true,
 *       //          multiple : true
 *       //       },
 *       //       controls : {
 *       //           responseBlock : true
 *       //       },
 *       //       responses : {
 *       //           max : 10
 *       //       }
 *       //   }
 *       // }
 *
 * @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
 *
 *       // Get the info object
 *       adcInfo.set({
 *          name : "My ADC"
 *          version : "2.2.0.beta1",
 *          date  : "2015-06-25",
 *          guid  : "the-guid",
 *          description : "Description of the ADC"
 *          author  : "The author name",
 *          company : "The company name",
 *          site    : "http://website.url.com",
 *          helpURL : "http://help.url.com",
 *          style   : { width : 400, height : 200},
 *          categories : ["General", "Slider", "Single"],
 *          constraints : {
 *              questions : {
 *                 single : true,
 *                 multiple : true
 *              },
 *              controls : {
 *                  responseBlock : true
 *              },
 *              responses : {
 *                  max : 10
 *              }
 *          }
 *        });
 *
 *
 * @param {Object} data Data to set
 * @param {String} [data.name] Name of the ADC
 * @param {String} [data.version] Version of the ADC
 * @param {String} [data.date] Date of the ADC (YYYY-MM-dd)
 * @param {String} [data.guid] GUID of the ADC
 * @param {String} [data.description] Description of the ADC
 * @param {String} [data.author] Author(s) of the ADC (name1 <name1@email.com, name2 <name2@email.com>)
 * @param {String} [data.company] Company name of the creator
 * @param {String} [data.site] Web site URL of the creator
 * @param {String} [data.helpURL] URL to the ADC help
 * @param {Object} [data.style] Style of the ADC
 * @param {Number} [data.style.width] Width of the ADC (in pixel)
 * @param {Number} [data.style.height] Height of the ADC (in pixel)
 * @param {String[]} [data.categories] Categories of the ADC
 * @param {Object} [data.constraints] Constraints of the ADC
 * @param {Object} [data.constraints.questions] Questions constraints of the ADC
 * @param {Boolean} [data.constraints.questions.chapter] Allow or not on chapter
 * @param {Boolean} [data.constraints.questions.single] Allow  or not on single questions
 * @param {Boolean} [data.constraints.questions.multiple] Allow or not on multi-coded questions
 * @param {Boolean} [data.constraints.questions.numeric] Allow or not on numeric questions
 * @param {Boolean} [data.constraints.questions.open] Allow or not on open-ended questions
 * @param {Boolean} [data.constraints.questions.date] Allow or not on date questions
 * @param {Boolean} [data.constraints.questions.requireParentLoop] Require or not on a parent loop question
 * @param {Object} [data.constraints.controls] Controls constraints of the ADC
 * @param {Boolean} [data.constraints.controls.responseBlock] Allow or not on response-block
 * @param {Boolean} [data.constraints.controls.label] Allow or not on label
 * @param {Boolean} [data.constraints.controls.textbox] Allow or not on text-box
 * @param {Boolean} [data.constraints.controls.listbox] Allow or not on list-box
 * @param {Boolean} [data.constraints.controls.checkbox] Allow or not on checkbox
 * @param {Boolean} [data.constraints.controls.radiobutton] Allow or not on radio button
 * @param {Object} [data.constraints.responses] Responses constraints of the ADC
 * @param {Number} [data.constraints.responses.min] Minimum allowed responses
 * @param {Number} [data.constraints.responses.max] Maximum allowed responses
 */
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) {
    /**
     * Get or set the name of the ADC
     *
     *       // Get the name of the ADC
     *       adcInfo.name();
     *
     *       // Set the name of the ADC
     *       adcInfo.name("New name");
     *
     * @method name
     * @param {String} [data] Name of the ADC to set
     * @returns {String} Name of the ADC
     */
    /**
     * Get or set the GUID of the ADC
     *
     *       // Get the guid of the ADC
     *       adcInfo.guid();
     *
     *       // Set the guid of the ADC
     *       var uuid = require('node-uuid'');
     *       adcInfo.guid(uuid.v4());
     *
     * @method guid
     * @param {String} [data] GUID of the ADC to set
     * @returns {String} GUID of the ADC
     */
    /**
     * Get or set the version of the ADC
     *
     *       // Get the version of the ADC
     *       adcInfo.version();
     *
     *       // Set the version of the ADC
     *       adcInfo.version("2.0.0.beta1");
     *
     * @method version
     * @param {String} [data] Version of the ADC to set
     * @returns {String} Version of the ADC
     */
    /**
     * Get or set the description of the ADC
     *
     *       // Get the description of the ADC
     *       adcInfo.description();
     *
     *       // Set the description of the ADC
     *       adcInfo.description("This is the description of the ADC");
     *
     * @method description
     * @param {String} [data] Description of the ADC to set
     * @returns {String} Description of the ADC
     */
    /**
     * Get or set the company name of the ADC creator
     *
     *       // Get the company of the ADC
     *       adcInfo.company();
     *
     *       // Set the company of the ADC
     *       adcInfo.company("Askia SAS");
     *
     * @method company
     * @param {String} [data] Company name to set
     * @returns {String} Company of the ADC creator
     */
    /**
     * Get or set the author(s) of the ADC
     *
     *       // Get the author(s) of the ADC
     *       adcInfo.author();
     *
     *       // Set the author(s) of the ADC
     *       adcInfo.author("John Doe <john.doe@unknow.com>, Foo Bar <foo@bar.com>");
     *
     * @method author
     * @param {String} [data] Author(s) to set
     * @returns {String} Author(s)
     */
    /**
     * Get or set the date creation of the ADC
     *
     *       // Get the date
     *       adcInfo.date();
     *
     *       // Set the date
     *       adcInfo.date("2015-06-25");
     *
     * @method date
     * @param {String} [data] Date to set
     * @returns {String} Date
     */
    /**
     * Get or set the website URL of the ADC creator
     *
     *       // Get the site
     *       adcInfo.site();
     *
     *       // Set the site URL
     *       adcInfo.site("http://my.website.com");
     *
     * @method site
     * @param {String} [data] URL to set
     * @returns {String} Site URL
     */
    /**
     * Get or set the help URL of the ADC
     *
     *       // Get the help URL
     *       adcInfo.helpURL();
     *
     *       // Set the help URL
     *       adcInfo.helpURL("http://my.help.file.com");
     *
     * @method helpURL
     * @param {String} [data] URL to set
     * @returns {String} Help URL
     */
    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
 *
 *       // Get the style of the ADC
 *       adcInfo.style();
 *
 *       // Set the style of the ADC
 *       adcInfo.style({
 *          width  : 400,
 *          height : 200
 *       });
 *
 *
 * @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 && el) {
        if (data.width !== undefined) {
            el.set("width", data.width);
        }
        if (data.height !== undefined) {
            el.set("height", data.height);
        }
    }
    w = (el && el.get("width")) || "0";
    h = (el && el.get("height")) || "0";

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

    return result;
};

/**
 * Get or set the categories
 *
 *       // Get the categories of the ADC
 *       adcInfo.categories();
 *
 *       // Set the categories of the ADC
 *       adcInfo.categories(["General", "Slider", "Single"]);
 *
 *
 * @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
 *
 *       // Get the constraints of the ADC
 *       adcInfo.constraints();
 *
 *       // Set the constraints of the ADC
 *       adcInfo.constraints({
 *          questions : {
 *              single : true,
 *              multiple : true
 *          },
 *          controls : {
 *              responseBlock : true,
 *              label : false
 *          },
 *          responses : {
 *              max : 25
 *          }
 *       });
 *
 *
 * @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
 *
 *       // Get the constraint 'single' on questions
 *       adcInfo.constraint('questions', 'single');
 *
 *       // Set the constraint 'single' on questions
 *       adcInfo.constraint('questions', 'single', true);
 *
 * @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
 *
 *       // Serialize the info to XML
 *       adcInfo.toXml();
 *       // -> <info><name>MyADC</name><guid>the-guid</guid>....</info>
 *
 * @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');
};


/**
 * Provide an object to manipulate the outputs  of the ADC (config.xml > outputs)
 *
 *      var ADC = require('adcutil').ADC;
 *
 *      var myAdc = new ADC('path/to/adc/');
 *      myAdc.load(function (err) {
 *          if (err) {
 *              throw err;
 *          }
 *
 *          // Get the instance of the Outputs
 *          var outputs = myAdc.configurator.outputs;
 *
 *          console.log(outputs.get());
 *
 *      });
 *
 * @class ADC.Configurator.Outputs
 */
function ADCOutputs(configurator) {
    this.configurator = configurator;
}

/**
 * Creates a new instance of ADC Outputs
 *
 * @constructor
 * @param {ADC.Configurator} configurator Instance of the configurator
 */
ADCOutputs.prototype.constructor = ADCOutputs;

/**
 * Get or set the default ADC output
 *
 *       // Get the id default output
 *       adcOutputs.defaultOutput();
 *
 *       // Set the default output id
 *       adcInfo.defaultOutput("without_javascript");
 *
 * @param {String} [data] Id of the default output to set
 * @returns {String} Id of the default output
 */
ADCOutputs.prototype.defaultOutput = function defaultOutput(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("outputs");
    if (data && typeof data === 'string') {
        el.set('defaultOutput', data);
    }
    return el.get('defaultOutput');
};

/**
 * Get outputs as an object
 *
 *       // Get the outputs object
 *       adcOutputs.get();
 *       // {
 *       //   defaultOutput  : "main",
 *       //   outputs : [{
 *       //      id : 'main',
 *       //      description : "Description of the output"
 *       //      condition : "Condition of the output",
 *       //      contents  : [{
 *       //         fileName : "default.html",
 *       //         type     : "html",
 *       //         mode     : "dynamic",
 *       //         position : "placeholder"
 *       //      }]
 *       //   }]
 *
 * @returns {Object}
 */
ADCOutputs.prototype.get = function get() {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("outputs");
    var outputs = [];

    if (!el) {
        return null;
    }

    el.iter('output', function (output) {
        // Output element
        var item = {
            id : output.get("id")
        };
        var descEl = output.find("description");
        if (descEl) {
            item.description = descEl.text;
        }
        var conditionEl = output.find("condition");
        if (conditionEl) {
            item.condition = conditionEl.text;
        }
        var defaultGeneration = output.get("defaultGeneration");
        if (defaultGeneration) {
            item.defaultGeneration = (defaultGeneration === "1" || defaultGeneration === "true");
        }
        var maxIter = output.get("maxIterations");
        if (maxIter) {
            item.maxIterations = (maxIter === "*") ? "*" : parseInt(maxIter, 10);
        }

        // Contents
        var contents = [];
        output.iter('content', function (content) {
            var itemContent = {};
            var fileName = content.get('fileName');
            if (fileName) {
                itemContent.fileName = fileName;
            }
            var type = content.get('type');
            if (type) {
                itemContent.type = type;
            }
            var mode = content.get('mode');
            if (mode) {
                itemContent.mode = mode;
            }
            var position = content.get('position');
            if (position) {
                itemContent.position = position;
            }


            // Attributes
            var attributes = [];
            content.iter('attribute', function (attribute) {
                var itemAttr = {};
                itemAttr.name = attribute.get("name");
                var value = attribute.find("value");
                if (value) {
                    itemAttr.value = value.text;
                }
                attributes.push(itemAttr);
            });

            if (attributes.length) {
                itemContent.attributes = attributes;
            }

            // Yield
            var yieldNode = content.find('yield');
            if (yieldNode) {
                itemContent.yieldValue = yieldNode.text;
            }

            contents.push(itemContent);

        });

        if (contents.length) {
            item.contents = contents;
        }

        outputs.push(item);
    });

    return {
        defaultOutput : this.defaultOutput(),
        outputs       : outputs
    };
};

/**
 * Set the outputs using a plain object
 *
 *       // Get the outputs object
 *       adcOutputs.set({
 *          defaultOutput : "main",
 *          outputs : [
 *             {
 *                 id : "main",
 *                 description : "Main output",
 *                  contents : [
 *                       {
 *                          fileName : 'main.css',
 *                          type : 'css',
 *                          mode : 'static',
 *                          position : 'head'
 *                      },
 *                      {
 *                          fileName : 'main.html',
 *                          type : 'html',
 *                          mode : 'dynamic',
 *                          position : 'placeholder'
 *                      },
 *                      {
 *                          fileName : 'main.js',
 *                          type : 'javascript',
 *                          mode : 'static',
 *                          position: 'foot'
 *                      }
 *                  ]
 *              },
 *              {
 *                  id : "second",
 *                  description : "Second output",
 *                  condition : "Browser.Support(\"javascript\")",
 *                  contents : [
 *                      {
 *                          fileName : 'second.css',
 *                          type : 'css',
 *                          mode : 'static',
 *                          position : 'head'
 *                      },
 *                      {
 *                          fileName : 'second.html',
 *                          type : 'html',
 *                          mode : 'dynamic',
 *                          position : 'placeholder'
 *                      },
 *                      {
 *                          fileName : 'second.js',
 *                          type : 'javascript',
 *                          mode : 'static',
 *                          position : 'foot'
 *                      }
 *                  ]
 *              },
 *              {
 *                  id : "third",
 *                  description : "Third output",
 *                  maxIterations : 12,
 *                  defaultGeneration : false,
 *                  contents : [
 *                      {
 *                          fileName : "third.css",
 *                          type  : "css",
 *                          mode : "static",
 *                          position : "head",
 *                          attributes : [
 *                              {
 *                                  name : "rel",
 *                                  value : "alternate"
 *                              },
 *                              {
 *                                  name : "media",
 *                                  value : "print"
 *                              }
 *                          ]
 *                      },
 *                      {
 *                          fileName : 'HTML5Shim.js',
 *                          type : 'javascript',
 *                          mode : 'static',
 *                          position : 'head',
 *                          yieldValue : '<!--[if lte IE 9]><script type="text/javascript"  src="{%= CurrentADC.URLTo("static/HTML5Shim.js") %}" ></script><![endif]-->'
 *                      }
 *                  ]
 *             }
 *
 *       });
 *
 * @param {Object} data Data to set
 * @param {String} [data.defaultOutput] Id of the default output
 * @param {Object[]} [data.outputs] Outputs
 * @param {String} [data.outputs.id] Id of the output
 * @param {String} [data.outputs.description] Description of the output
 * @param {String} [data.outputs.condition] AskiaScript condition to use the output
 * @param {Object[]} [data.outputs.contents] List of contents (files) used by the output
 * @param {String} [data.outputs.contents.fileName] Name of the file
 * @param {String|"text"|"html"|"css"|"javascript"|"binary"|"image"|"audio"|"video"|"flash"} [data.outputs.contents.type] Name of the file
 * @param {String|"dynamic"|"static"|"share"} [data.outputs.contents.mode] Extract mode
 * @param {String|"none"|"head"|"placeholder"|"foot"} [data.outputs.contents.position] Position in the final page document
 * @param {Object[]} [data.outputs.contents.attributes] List of HTML attributes
 * @param {String} [data.outputs.contents.attributes.name] Name of the HTML attribute
 * @param {String} [data.outputs.contents.attributes.value] Value of the HTML attribute
 * @param {String} [data.outputs.contents.yieldValue] Yield value, used to override the auto-generation
 */
ADCOutputs.prototype.set = function set(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("outputs");

    if (!data) {
        return;
    }

    if (data.defaultOutput) {
        el.set("defaultOutput", data.defaultOutput);
    }
    if (!data.outputs || !Array.isArray(data.outputs)) {
        return;
    }
    el.delSlice(0, el.len());
    data.outputs.forEach(function (output) {
        var item = subElement(el, 'output');

        // All output xml attributes
        item.set("id", output.id || "");
        if (typeof output.defaultGeneration === 'boolean') {
            item.set("defaultGeneration", output.defaultGeneration.toString());
        }
        if (output.maxIterations) {
            item.set("maxIterations", output.maxIterations);
        }

        // All output sub-nodes
        if (output.description) {
            var desc = subElement(item, 'description');
            desc.text = output.description;
        }
        if (output.condition) {
            var cond = subElement(item, 'condition');
            cond.text = output.condition;
        }

        if (!output.contents || !Array.isArray(output.contents)) {
            return;
        }

        output.contents.forEach(function (content) {
            var itemContent = subElement(item, 'content');
            itemContent.set("fileName", content.fileName || "");
            itemContent.set("type", content.type || "");
            itemContent.set("mode", content.mode || "");
            itemContent.set("position", content.position || "");

            if (content.attributes && Array.isArray(content.attributes)) {
                content.attributes.forEach(function (attribute) {
                    var itemAttr = subElement(itemContent, 'attribute');
                    itemAttr.set('name', attribute.name || "");
                    if (typeof attribute.value === 'string') {
                        var itemAttrVal = subElement(itemAttr, 'value');
                        itemAttrVal.text = attribute.value;
                    }
                });
            }
            if (content.yieldValue) {
                var itemYield = subElement(itemContent, 'yield');
                itemYield.text = content.yieldValue;
            }

        });
    });

};

/**
 * Return the outputs as xml string
 *
 *       // Serialize the outputs to XML
 *       adcOupts.toXml();
 *       // -> <outputs defaultOutput="main"><output id="main"> ...</outputs>
 *
 * @return {String}
 */
ADCOutputs.prototype.toXml = function toXml() {
    var xml = [],
        data = this.get();

    if (!data) {
        return '';
    }
    xml.push('  <outputs defaultOutput="' + data .defaultOutput + '">');
    if (Array.isArray(data.outputs)) {
        data.outputs.forEach(function (output) {
            var outputAttr = '';
            if (typeof output.defaultGeneration === 'boolean') {
                outputAttr += ' defaultGeneration="' + output.defaultGeneration.toString() + '"';
            }
            if (output.maxIterations) {
                outputAttr += ' maxIterations="' + output.maxIterations + '"';
            }

            xml.push('    <output id="' + output.id + '"' + outputAttr + '>');
            if (output.description) {
                xml.push('      <description><![CDATA[' + output.description + ']]></description>');
            }
            if (output.condition) {
                xml.push('      <condition><![CDATA[' + output.condition + ']]></condition>');
            }

            if (Array.isArray(output.contents)) {
                output.contents.forEach(function (content) {
                    var xmlContent = [];
                    xmlContent.push('      <content');
                    xmlContent.push(' fileName="', content.fileName || "", '"');
                    xmlContent.push(' type="', content.type || "", '"');
                    xmlContent.push(' mode="', content.mode || "", '"');
                    xmlContent.push(' position="', content.position || "", '"');
                    if (!content.attributes && !content.yieldValue) {
                        xmlContent.push(' />');
                    } else {
                        xmlContent.push('>');
                        if (Array.isArray(content.attributes)) {
                            content.attributes.forEach(function (attr) {
                                xmlContent.push('\n        <attribute name="' + attr.name + '">');
                                if (attr.value) {
                                    xmlContent.push('\n          <value><![CDATA[' + (attr.value || "") + ']]></value>');
                                }
                                xmlContent.push('\n        </attribute>');
                            });
                        }

                        if (content.yieldValue) {
                            xmlContent.push('\n        <yield><![CDATA[' + content.yieldValue + ']]></yield>');
                        }
                        xmlContent.push('\n      </content>');
                    }
                    xml.push(xmlContent.join(''));
                });
            }
            xml.push('    </output>');
        });
    }
    xml.push('  </outputs>');
    return xml.join('\n');
};


/**
 * Provide an object to manipulate the propertues of the ADC (config.xml > properties)
 *
 *      var ADC = require('adcutil').ADC;
 *
 *      var myAdc = new ADC('path/to/adc/');
 *      myAdc.load(function (err) {
 *          if (err) {
 *              throw err;
 *          }
 *
 *          // Get the instance of the Properties
 *          var properties = myAdc.configurator.properties;
 *
 *          console.log(properties.get());
 *
 *      });
 *
 * @class ADC.Configurator.Properties
 */
function ADCProperties(configurator) {
    this.configurator = configurator;
}

/**
 * Creates a new instance of ADC Properties
 *
 * @constructor
 * @param {ADC.Configurator} configurator Instance of the configurator
 */
ADCProperties.prototype.constructor = ADCProperties;


/**
 * Get properties as an object
 *
 *       // Get the properties object
 *       adcProperties.get();
 *       // {
 *       //   categories : [{
 *       //      id : "general",
 *       //      name : "General"
 *       //      properties  : [{
 *       //         id : "renderingType",
 *       //         name : "Rendering Type",
 *       //         type : "string",
 *       //         description : "Type of the ADC rendering",
 *       //         value : "classic",
 *       //         options : [{
 *       //           value : "classic",
 *       //           text  : "Classical"
 *       //         }, {
 *       //           value : "image",
 *       //           text  : "Image"
 *       //         }]
 *       //      }]
 *       //   }, {
 *       //      id : "styles",
 *       //      name : "Styles",
 *       //      properties : [{
 *       //         id : "bg",
 *       //         name : "Background color",
 *       //         type : "color",
 *       //         description : "Background color of the ADC",
 *       //         colorFormat : "rgb",
 *       //         value : "255,255,255"
 *       //      }]
 *       //   }
 *       // }
 *
 * @returns {Object}
 */
ADCProperties.prototype.get = function get() {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("properties");
    var categories = [];

    if (!el) {
        return null;
    }

    el.iter('category', function (category) {
        // Category element
        var itemCategory = {
            id : category.get("id") || "",
            name : category.get("name") || "",
            properties : []
        };

        category.iter('property', function (property) {
            var itemProperty = {};
            var xsiType = property.get('xsi:type');
            if (xsiType && xsiType !== 'standardProperty') {
                itemProperty.xsiType = xsiType;
            }
            itemProperty.id = property.get('id') || "";
            var val = property.get('name');
            if (val) {
                itemProperty.name = val;
            }
            val = property.get('type');
            if (val) {
                itemProperty.type = val;
            }
            val = property.get('mode');
            if (val) {
                itemProperty.mode = val;
            }
            val = property.get('visible');
            if (val) {
                itemProperty.visible = (val !== "false" && val !== "0");
            }
            val = property.get('require');
            if (val) {
                itemProperty.require = (val !== "false" && val !== "0");
            }

            // Number
            val = property.get('min');
            if (val) {
                itemProperty.min = parseFloat(val) || val;
            }
            val = property.get('max');
            if (val) {
                itemProperty.max = parseFloat(val) || val;
            }
            val = property.get('decimal');
            if (val) {
                itemProperty.decimal = parseInt(val, 10) || val;
            }

            // String
            val = property.get('pattern');
            if (val) {
                itemProperty.pattern = val;
            }

            // File
            val = property.get('fileExtension');
            if (val) {
                itemProperty.fileExtension = val;
            }

            // Color
            val = property.get('colorFormat');
            if (val) {
                itemProperty.colorFormat = val;
            }

            // Questions
            val = property.get('chapter');
            if (val) {
                itemProperty.chapter = (val !== "false" && val !== "0");
            }
            val = property.get('single');
            if (val) {
                itemProperty.single = (val !== "false" && val !== "0");
            }
            val = property.get('multiple');
            if (val) {
                itemProperty.multiple = (val !== "false" && val !== "0");
            }
            val = property.get('numeric');
            if (val) {
                itemProperty.numeric = (val !== "false" && val !== "0");
            }
            val = property.get('open');
            if (val) {
                itemProperty.open = (val !== "false" && val !== "0");
            }
            val = property.get('date');
            if (val) {
                itemProperty.date = (val !== "false" && val !== "0");
            }

            var desc = property.find('description');
            if (desc) {
                itemProperty.description = desc.text;
            }

            property.iter('value', function (value) {
                var theme = value.get("theme");
                if (!theme && !("value" in itemProperty)) {
                    itemProperty.value = value.text || "";
                } else {
                    itemProperty.valueTheme = itemProperty.valueTheme || {};
                    itemProperty.valueTheme[theme] = value.text || "";
                }
            });

            var options = property.find('options');
            if (options) {
                var itemOptions = [];
                options.iter('option', function (option) {
                    itemOptions.push({
                        value : option.get("value") || "",
                        text : option.get("text") || ""
                    });
                });
                if (itemOptions.length) {
                    itemProperty.options = itemOptions;
                }
            }

            itemCategory.properties.push(itemProperty);
        });

        categories.push(itemCategory);
    });

    return {
        categories       : categories
    };
};


/**
 * Set the properties using a plain object
 *
 *       // Get the properties object
 *       adcProperties.set({
 *          categories : [
 *             {
 *                 id : "general",
 *                 description : "General",
 *                 properties  : [
 *                       {
 *                          id : "background",
 *                          name : "Background color",
 *                          type : "color",
 *                          description : "Color of the ADC background",
 *                          colorFormat : "rgb",
 *                          value  : "255,255,255"
 *                      }
 *                ]
 *             }
 *          ]
 *       });
 *
 * @param {Object} data Data to set
 * @param {Object[]} [data.categories] Categories
 * @param {String} [data.categories.id] Id of the category
 * @param {String} [data.categories.name] User friendly name of the category
 * @param {Object[]} [data.categories.properties] Properties of the category
 * @param {String} [data.categories.properties.id] Id of the property
 * @param {String} [data.categories.properties.name] Name of the property
 * @param {String|"number"|"boolean"|"string"|"color"|"file"|"question"} [data.categories.properties.type] Type of the property
 * @param {String} [data.categories.properties.description] Description of the property
 * @param {String|"static"|"dynamic"} [data.categories.properties.mode] Reading mode of the property value
 * @param {Boolean} [data.categories.properties.visible] Visibility of the property value
 * @param {Boolean} [data.categories.properties.require] Requirement of the property value
 * @param {String} [data.categories.properties.pattern] Pattern of the string property
 * @param {Number} [data.categories.properties.min] Min value of the number property
 * @param {Number} [data.categories.properties.max] Max value of the number property
 * @param {Number} [data.categories.properties.decimal] Allowed decimal of the number property
 * @param {Boolean} [data.categories.properties.chapter] Allowed chapter for question property
 * @param {Boolean} [data.categories.properties.single] Allowed single-closed question for question property
 * @param {Boolean} [data.categories.properties.multiple] Allowed multi-coded question for question property
 * @param {Boolean} [data.categories.properties.numeric] Allowed numeric question for question property
 * @param {Boolean} [data.categories.properties.open] Allowed open-ended question for question property
 * @param {Boolean} [data.categories.properties.date] Allowed date/time question for question property
 * @param {String} [data.categories.properties.value] Default property value
 * @param {Object[]} [data.categories.properties.options] List of options
 * @param {String} [data.categories.properties.options.value] Value of the option
 * @param {String} [data.categories.properties.options.text] Text of the option to display
 */
ADCProperties.prototype.set = function set(data) {
    var xmldoc = this.configurator.xmldoc;
    var el = xmldoc.find("properties");

    if (!data || !data.categories || !Array.isArray(data.categories)) {
        return;
    }
    el.delSlice(0, el.len());
    data.categories.forEach(function (category) {
        var itemCategory = subElement(el, 'category');

        itemCategory.set("id", category.id || "");
        itemCategory.set("name", category.name || "");

        if (category.properties && Array.isArray(category.properties)) {
            category.properties.forEach(function (property) {
                var itemProperty = subElement(itemCategory, 'property');
                itemProperty.set('xsi:type', property.xsiType || "standardProperty");
                itemProperty.set('id', property.id || "");
                var props = ["name", "type", "mode", "require", "visible", "min", "max", "decimal", "pattern",
                    "fileExtension", "colorFormat", "chapter", "single", "multiple", "numeric", "open", "date"];
                props.forEach(function (prop){
                    if (prop in property) {
                        itemProperty.set(prop, property[prop].toString());
                    }
                });

                if ("description" in property) {
                    var itemDesc = subElement(itemProperty, "description");
                    itemDesc.text = property.description || "";
                }
                if ("value" in property) {
                    var itemValue = subElement(itemProperty, "value");
                    itemValue.text = property.value || "";
                }

                if (property.valueTheme) {
                    var itemTheme;
                    for (var theme in property.valueTheme) {
                        if (property.valueTheme.hasOwnProperty(theme)) {
                            itemTheme = subElement(itemProperty, "value");
                            itemTheme.set("theme", theme);
                            itemTheme.text = property.valueTheme[theme].toString();
                        }
                    }
                }

                if (property.options && Array.isArray(property.options)) {
                    var itemOptions = subElement(itemProperty, "options");
                    property.options.forEach(function (option) {
                        var itemOption = subElement(itemOptions, "option");
                        itemOption.set("value", option.value.toString());
                        itemOption.set("text", option.text.toString());
                    });
                }
            });
        }
    });

};


/**
 * Return the properties as xml string
 *
 *       // Serialize the properties to XML
 *       adcProps.toXml();
 *       // -> <properties><category id="genereal" name="Genereal"> ...</properties>
 *
 * @return {String}
 */
ADCProperties.prototype.toXml = function toXml() {
    var xml = [],
        data = this.get();

    if (!data) {
        return '';
    }
    xml.push('  <properties>');
    if (Array.isArray(data.categories)) {
        data.categories.forEach(function (category) {
            xml.push('    <category id="' + (category.id || "") + '" name="' + (category.name || "")  + '">');
            if (Array.isArray(category.properties)) {
                category.properties.forEach(function (property) {
                    var xmlProp = [], value;
                    xmlProp.push('      <property');
                    xmlProp.push(' xsi:type="', (property.xsiType || "standardProperty"), '"');
                    xmlProp.push(' id="', property.id || "", '"');
                    var props = ["name", "type", "mode", "require", "visible", "min", "max", "decimal", "pattern",
                        "fileExtension", "colorFormat", "chapter", "single", "multiple", "numeric", "open", "date"];
                    props.forEach(function (prop){
                        if (prop in property) {
                            xmlProp.push(' ' + prop + '="', property[prop].toString() , '"');
                        }
                    });
                    xmlProp.push('>');

                    if ("description" in property) {
                        xmlProp.push('\n        <description><![CDATA[' + property.description + ']]></description>');
                    }
                    if ("value" in property) {
                        value = property.value;
                        if (value !== "") {
                            value = '<![CDATA[' + value + ']]>';
                        }
                        xmlProp.push('\n        <value>' + value + '</value>');
                    }
                    if (property.valueTheme) {
                        for (var theme in property.valueTheme) {
                            value = property.valueTheme[theme];
                            if (value !== "") {
                                value = '<![CDATA[' + value + ']]>';
                            }
                            xmlProp.push('\n        <value theme="' + theme + '">' + value + '</value>');
                        }
                    }

                    if (property.options && Array.isArray(property.options)) {
                        xmlProp.push('\n        <options>');
                        property.options.forEach(function (opt) {
                            xmlProp.push('\n          <option value="' + opt.value +'" text="' + opt.text + '" />');
                        });
                        xmlProp.push('\n        </options>');
                    }


                    xmlProp.push('\n      </property>');

                    xml.push(xmlProp.join(''));
                });
            }
            xml.push('    </category>');
        });
    }
    xml.push('  </properties>');
    return xml.join('\n');
};

// Make it public
exports.Configurator = Configurator;