/**
 * @file Interact with k-fet API.
 * @copyright 2017 cof-geek
 * @license MIT
 */


/**
 * Virtual namespace for models.
 * <br><br>
 *
 * A model subclasses {@link Models.ModelObject}.<br>
 * A model whose instances can be got from API subclasses
 * {@link Models.APIModelObject}.<br>
 * A model to manage ModelObject forests
 * {@link Models.ModelForest}.<br>
 * A ModelObject that can be fetched through API
 * {@link Models.APIModelForest}.<br>
 * These classes should not be used directly.
 * <br><br>
 *
 * Models with API support:
 * {@link Models.Account} (partial),
 * {@link Models.Checkout} (partial).
 * <br>
 * Models without API support:
 * {@link Models.Statement},
 * {@link Models.ArticleCategory},
 * {@link Models.Article}.
 *
 * @namespace Models
 */


/**
 * Extended object
 * @memberof Models
 */
class ModelObject {

    /**
     * These properties always exist on instances of this class.
     * @abstract
     * @type {string[]}
     */
    static get props() { return []; }

    /**
     * Default values for properties given by <tt>props</tt> static member.
     * @abstract
     * @type {Object.<string, *>}
     */
    static get default_data() { return {}; }

    /**
     * Create new instance from data or default values.
     * @param   {Object} [data={}] - data to store in instance
     */
    constructor(data) {
        this.from(data || {});
    }

    /**
     * Check if an instance is empty.
     * @return  {boolean}
     */
    is_empty() {
        return ( this.id === undefined ) ? false : this.id == 0;
    }

    /**
     * Returns a {@link Formatters|Formatter} used for rendering.
     * @default {@link Formatters.Formatter}
     * @type {Formatter}
     */
    formatter() { return Formatter; }

    /**
     * Set properties of this instance from data object ones. If a property is
     * not present in data object, its value fallback to
     * {@link Models.ModelObject.default_data} one.
     * @param   {Object} data
     * @todo    Restrict to props
     */
    from(data) {
        // TODO: add restrict
        this.clear();
        this.update(data);
    }

    /**
     * Update properties of this instance from data ones.
     * @param   {Object} data
     */
    update(data) {
        $.extend(this, data);
    }

    /**
     * Clear properties to {@link Models.ModelObject.default_data|default_data}.
     */
    clear() {
        $.extend(this, this.constructor.default_data);
    }


    /**
     * Display stored data in container.
     * @param   {jQuery} $container
     * @param   {Object} [options] Options for formatter render method.
     * @param   {Formatters.Formatter}
     *          [formatter={@link Models.ModelObject#formatter|this.formatter()}]
     *          Formatter class to use.
     * @return  {jQuery} The DOM element $container, allowing methods chaining.
     */
    display($container, options, formatter) {
        formatter = formatter || this.formatter();
        formatter.render(this, $container, options);
        return $container;
    }

    /**
     * Returns a string value for the model, to use in comparisons
     * @see {@link Models.TreeNode.compare|TreeNode.compare}
     */
    comparevalue() {
        return this.id.toString();
    }
}


/**
 * Describes a model whose instances can be obtained through API.
 * @extends Models.ModelObject
 * @memberof Models
 */
class APIModelObject extends ModelObject {

    /**
     * Request url to get array of model instances data.
     * @abstract
     * @type {string}
     */
    static get url_model() {}

    /**
     * Request url to create an instance.
     * @abstract
     * @type {string}
     */
    static url_create() {}

    /**
     * Request url to edit an instance of this model.
     * @abstract
     * @param   {*} api_pk - Identifier of a model instance.
     * @type {string}
     */
    static url_update_for(api_pk) {}

    /**
     * Request url to get a single model instance data.
     * @abstract
     * @param   {*} api_pk - Identifier of a model instance.
     * @return  {string}
     */
    static url_read_for(api_pk) {}

    /**
     * See {@link Models.ModelObject|new ModelObject(data)}.
     * @param   {Object} [data={}] - data to store in instance
     */
    constructor(data) {
        super(data);

        if (this.id === undefined)
            this.id = 0;
    }

    /**
     * Identifier of the model instance in api requests.
     * @default <tt>this.id</tt>
     */
    get api_pk() { return this.id; }

    /**
     * Request url used to get current instance data.
     */
    get url_read() {
        if (this._url_object === undefined)
            return this.is_empty() ? '' : this.constructor.url_read_for(this.api_pk);
        return this._url_object;
    }

    set url_read(v) { this._url_object = v; }

    /**
     * Get data of a distant model instance. It sends a GET HTTP request to
     * {@link Models.APIModelObject#url_read}.
     * @param   {object} [api_options] Additional data appended to the request.
     */
    fromAPI(api_options) {
        api_options = api_options || {};

        api_options['format'] = 'json';

        return $.getJSON(this.url_read, api_options)
            .done( (json) => this.from(json) );
    }

    /**
     * Get data of a distant model instance via its id.
     * @param   {*} api_pk
     * @param   {object} [api_options]
     * @param   {jQueryAjaxSuccess} [on_success]
     * @param   {jQueryAjaxError} [on_error]
     * @see {@link Models.APIModelObject#fromAPI|fromAPI}
     */
    get_by_apipk(api_pk, api_options) {
        this.url_read = this.constructor.url_read_for(api_pk);
        return this.fromAPI(api_options);
    }

    /**
     * Add <tt>empty_props</tt> and <tt>empty_attrs</tt> options to
     * <tt>render</tt> call in {@link Models.ModelObject#display} if instance
     * is empty.
     * @see {@link Models.ModelObject#display}
     * @see {@link Models.APIModelObject#is_empty}
     */
    display($container, options, formatter) {
        if (this.is_empty()) {
            options.empty_props = true;
            options.empty_attrs = true;
        }
        return ModelObject.prototype.display.call(this, $container, options, formatter);
    }

}


/**
 * Account model. Can be accessed through API.
 * @extends Models.APIModelObject
 * @memberof Models
 */
class Account extends APIModelObject {

    /**
     * Properties retrieved through API.
     * @default <tt>['id', 'trigramme', 'name', 'nickname', 'email', 'is_cof',
     *          'promo', 'balance', 'is_frozen', 'departement']</tt>
     * @see {@link Models.ModelObject.props|ModelObject.props}
     */
    static get props() {
        return ['id', 'trigramme', 'name', 'nickname', 'email', 'is_cof',
                'promo', 'balance', 'is_frozen', 'departement'];
    }

    /**
     * Default values for Account model instances.
     * @default <tt>{ 'id': 0, 'trigramme': '', 'name': '', 'nickname': '',
     *          'email': '', ''is_cof': false, 'promo': '', 'balance': 0,
     *          'is_frozen': false, 'departement': '' }</tt>
     * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
     */
    static get default_data() {
        return {
            'id': 0, 'trigramme': '', 'name': '', 'nickname': '', 'email': '',
            'is_cof' : false, 'promo': '', 'balance': 0, 'is_frozen': false,
            'departement': '',
        };
    };

    /**
     * @default <tt>django-js-reverse('kfet.account')</tt>
     * @see {@link Models.APIModelObject.url_model|APIModelObject.url_model}
     */
    static get url_model() { return Urls['kfet.account'](); }

    static url_create(trigramme) {
        var url = Urls['kfet.account.create']();
        if (trigramme) {
            var trigramme_url = encodeURIComponent(trigramme);
            url += `?trigramme=${trigramme_url}`
        }
        return url;
    }

    /**
     * @default <tt>django-js-reverse('kfet.account.read')(trigramme)</tt>
     * @param   {string} trigramme
     * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for}
     */
    static url_read_for(trigramme) {
        var trigramme_url = encodeURIComponent(trigramme);
        return Urls['kfet.account.read'](trigramme_url);
    }

    static url_update_for(trigramme) {
        var trigramme_url = encodeURIComponent(trigramme);
        return Urls['kfet.account.update'](trigramme_url);
    }

    /**
     * @default <tt>this.trigramme</tt>
     */
    get api_pk() { return this.trigramme; }

    /**
     * @default {@link Formatters.AccountFormatter}
     */
    formatter() {
        return (this.trigramme == 'LIQ') ? LIQFormatter : AccountFormatter;
    }

    // take care of "balance" type
    // API currently returns a string object (serialization of Decimal type within Django)
    get balance()  { return this._balance; }
    set balance(v) { return this._balance = floatCheck(v); }

    /**
     * Balance converted to UKF according to cof status.
     */
    get balance_ukf() { return amountToUKF(this.balance, this.is_cof, true); }

}


/**
 * Checkout model. Can be accessed through API.
 * @extends Models.APIModelObject
 * @memberof Models
 */
class Checkout extends APIModelObject {

    /**
     * Properties retrieved through API.
     * @default <tt>['id', 'name', 'balance', 'valid_from', 'valid_to']</tt>
     * @see {@link Models.ModelObject.props|ModelObject.props}
     */
    static get props() {
        return ['id', 'name', 'balance', 'valid_from', 'valid_to'];
    }

    /**
     * Default values for Account model instances.
     * @default <tt>{ 'id': 0, 'name': '', 'balance': 0, 'valid_from': '',
     *          'valid_to': '' }</tt>
     * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
     */
    static get default_data() {
        return {
            'id': 0, 'name': '', 'balance': 0, 'valid_from': '', 'valid_to': '',
        };
    }

    /**
     * Not supported by API yet.
     * @see {@link Models.APIModelObject.url_model|APIModelObject.url_model}
     */
    static get url_model() { return ''; }

    /**
     * @default <tt>django-js-reverse('kfet.kpsul.checkout_data.read')(pk)</tt>
     * @param   {string} api_pk - a checkout id
     * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for}
     */
    static url_read_for(api_pk) {
        return Urls['kfet.checkout.read'](api_pk);
    }

    static url_update_for(api_pk) {
        return Urls['kfet.checkout.update'](api_pk);
    }

    /**
     * @default {@link Formatters.CheckoutFormatter}
     */
    formatter() {
        return CheckoutFormatter;
    }

    // take care of "balance" type
    // API currently returns a string object (serialization of Decimal type within Django)
    get balance()  { return this._balance; }
    set balance(v) { this._balance = floatCheck(v); }

}


/**
 * Statement model. Cannot be accessed through API.
 * @extends Models.ModelObject
 * @memberof Models
 */
class Statement extends ModelObject {

    /**
     * Properties associated to a statement.
     * @default <tt>['id', 'at', 'balance_old', 'balance_new', 'by']</tt>
     * @see {@link Models.ModelObject.props|ModelObject.props}
     */
    static get props() {
        return ['id', 'at', 'balance_old', 'balance_new', 'by'];
    }

    /**
     * Default values for Statement model instances.
     * @default <tt>{ 'id': 0, 'at': '', 'balance_old': 0, 'balance_new': 0,
     *          'by': '' }</tt>
     * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
     */
    static get default_data() {
        return {
            'id': 0, 'at': '', 'balance_old': 0, 'balance_new': 0, 'by': '',
        };
    }

    static url_create(checkout_pk) {
        return Urls['kfet.checkoutstatement.create'](checkout_pk);
    }

    /**
     * @default {@link Formatters.StatementFormatter}
     */
    formatter() {
        return StatementFormatter;
    }

    // take care of "balance" type
    // API currently returns a string object (serialization of Decimal type within Django)
    get balance_old()  { return this._balance_old; }
    set balance_old(v) { this._balance_old = floatCheck(v); }

    get balance_new()  { return this._balance_new; }
    set balance_new(v) { this._balance_new = floatCheck(v); }

    get at()  { return this._at; }
    set at(v) { this._at = moment.isMoment(v) ? v : moment.tz(v, 'UTC'); }

}

/**
 * ArticleCategory model. Cannot be accessed through API.
 * @extends Models.ModelObject
 * @memberof Models
 */
class ArticleCategory extends ModelObject {

    /**
     * Properties associated to a category
     * @default <tt>['id', 'name', 'has_addcost', 'article']</tt>
     * @see {@link Models.ModelObject.props|ModelObject.props}
     */
    static get props() {
        return ['id', 'name', 'has_addcost', 'articles'];
    }

    /**
     * Default values for ArticleCategory model instances.
     * @default <tt>{ 'id': 0, 'name': '', 'has_addcost': true, 'articles': [] }</tt>
     * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
     */
    static get default_data() {
        return {'id': 0, 'name': '', 'has_addcost': true, 'articles': []};
    }

    /**
     * Verbose name for ArticleCategory model
     * @default <tt>'category'</tt>
     */
    static get verbose_name() { return 'category'; }

    /**
     * @default {@link Formatters.ArticleCategoryFormatter}
     */
    formatter() {
        return ArticleCategoryFormatter;
    }

    /**
     * Comparison function between ArticleCategory model instances.
     * @see {@link Models.ModelObject.compare|ModelObject.compare}
     */
    static compare(a, b) {
        return a.name.localeCompare(b.name) ;
    }
}

/**
 * Article model. Cannot be accessed through API.
 * @extends Models.ModelObject
 * @memberof Models
 */
class Article extends ModelObject {
    /**
     * Properties associated to an article
     * @default <tt>['id', 'name', 'price', 'stock', 'category']</tt>
     * @see {@link Models.ModelObject.props|ModelObject.props}
     */
    static get props() {
        return ['id', 'name', 'price', 'stock', 'category'];
    }

    /**
     * Default values for Article model instances.
     * @default <tt>{ 'id': 0, 'name': '', 'price': 0, 'stock': 0,
     *          'category': new ArticleCategory() }</tt>
     * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
     */
    static get default_data() {
        return {
            'id': 0, 'name': '', 'price': 0, 'stock': 0,
            'category': new ArticleCategory(),
        };
    }

    /**
     * Verbose name for Article model
     * @default <tt>'article'</tt>
     */
    static get verbose_name() { return 'article'; }

    /**
     * @default {@link Formatters.ArticleFormatter}
     */
    formatter() {
        return ArticleFormatter;
    }

    /**
     * Comparison function between Article model instances.
     * @see {@link Models.ModelObject.compare|ModelObject.compare}
     */
    static compare(a, b) {
        return a.name.localeCompare(b.name);
    }

    // Take care of 'price' type
    // API currently returns a string object (serialization of Decimal type within Django)
    get price()  { return this._price; }
    set price(v) { this._price = floatCheck(v); }

    is_low_stock(nb) {
        return (-5 <= this.stock - nb && this.stock - nb <= 5);
    }
}

/**
 * Simple {@link Models.ModelObject} forest.
 * @memberof Models
 */
class ModelForest {

    /**
     * Abstract structure of the forest
     * @abstract
     * @type {Object}
     */
    static get structure() { return {}; }

    /**
     * Creates empty instance and populates it with data if given
     * @param {Object[]} [datalist=[]]
     */
    constructor(datalist) {
        this.from(datalist || []);
    }

    /**
     * Shortcut functions to get parent and children of a given node
     */
    get_parent(node) {
        var parent_name = this.constructor.structure[node.constructor.verbose_name].parent;
        return node[parent_name];
    }

    get_children(node) {
        var child_name = this.constructor.structure[node.constructor.verbose_name].children;
        return node[child_name];
    }

    /**
     * Fetches an object from the instance data, or creates it if
     * it does not exist yet.<br>
     * If direction >= 0, parent objects are created recursively.
     * If direction <= 0, child objects are created recursively.
     * @param {Object} data
     * @param {number} direction
     */
    get_or_create(data, direction) {
        var struct_data = this.constructor.structure[data.modelname];
        var model = struct_data.model;

        var existing = this.find(data.modelname, data.content.id);
        if (existing) {
            return existing;
        }

        var node = new model(data.content);

        if (direction <= 0) {
            var parent_name = struct_data.parent;
            var parent_data = data.parent;
            var parent_struct = this.constructor.structure[parent_name];
            if (parent_data) {
                var parent_node = this.get_or_create(parent_data, -1);
                node[parent_name] = parent_node;
                parent_node[parent_struct.children].push(node);
            } else {
                this.roots.push(node);
            }
        }

        if (direction >= 0) {
            var child_name = struct_data.children;
            var child_struct = this.constructor.structure[child_name];
            if (data.children && data.children.length) {
                for (let child_data of data.children) {
                    var child = this.get_or_create(child_data, 1);
                    child[child_struct.parent] = node;
                    node[child_name].push(child);
                }
            }
        }

        return node ;
    }

    /**
     * Resets then populates the instance with the given data.
     * @param {Object[]} datalist
     */
    from(datalist) {
        this.roots = [];
        for (let data of datalist) {
            this.get_or_create(data, 0);
        }
    }

    /**
     * Removes all Models.TreeNode from the tree.
     */
    clear() {
        this.from([]);
    }

    /**
     * Renders a node (and all its offspring) and returns the
     * corresponding jQuery object.
     * @param {Models.TreeNode} node
     * @param {Object} templates Templates to render each model
     * @param {Object} [options] Options for element render method
     */
    render_element(node, templates, options) {
        var modelname = node.constructor.verbose_name;
        var struct_data = this.constructor.structure[modelname];

        var template = templates[modelname];
        var options = options || {} ;

        var $container = $('<div></div>');
        $container.attr('id', modelname+'-'+node.id);

        var $rendered = node.display($(template), options);
        $container.append($rendered);

        var children = this.get_children(node);

        if (children) {
            children.sort(struct_data.child_sort);
            for (let child of children) {
                var $child = this.render_element(child, templates, options);
                $container.append($child);
            }
        }

        return $container;
    }

    add_to_container($container, node, templates, options) {
        var struct = this.constructor.structure;
        var existing = this.get_parent(node) ;
        var first_missing = node;

        while (existing && !($container.find('#'+existing.modelname+'-'+existing.id))) {
            first_missing = existing;
            existing = this.get_parent(existing);
        }

        var $to_insert = render_element(first_missing, templates, options);
        var $insert_in = existing ? $container.find('#'+existing.modelname+'-'+existing.id)
                                  : $container ;
        $insert_in.prepend($to_insert);
    }

    /**
     * Display stored data in container.
     * @param {jQuery} $container
     * @param {Object} templates Templates to render each model
     * @param {Object} [options] Options for element render method
     */
    display($container, templates, options) {
        this.roots.sort(this.constructor.root_sort);
        for (let root of this.roots) {
            $container.append(this.render_element(root, templates, options));
        }

        return $container;
    }

    /**
     * Performs for each node (in a DFS order) the callback function
     * on node.content and node.parent.content, if node has given modelname.
     * @param {string} modelname
     * @param {function} callback
     */
    traverse(modelname, callback) {
        var that = this;
        function recurse(node) {
            if (node.constructor.verbose_name === modelname) {
                if callback(node);
                    return true;
            }

            var children = that.get_children(node);
            if (children) {
                for (let child of children)
                    if (recurse(child))
                        return true;
            }
        }

        for (let root of this.roots)
            if (recurse(root))
                return ;
    }

    /**
     * Find instance in tree with given type and id
     * @param {string} modelname
     * @param {number} id
     */
    find(modelname, id) {
        var result = null;

        function callback(node) {
            if (node.id == id) {
                result = node ;
                return true;
            }
        }

        this.traverse(modelname, callback);

        return result;
    }
}


/**
 * Describes a model list that can be filled through API.
 * @extends Models.ModelForest
 * @memberof Models
 */
class APIModelForest extends ModelForest {

    /**
     * Request url to fill the model.
     * @abstract
     * @type {string}
     */
    static get url_model() {}

    /**
     * Fills the instance with distant data. It sends a GET HTTP request to
     * {@link Models.APIModelForest#url_model}.
     * @param   {object} [api_options] Additional data appended to the request.
     */
    fromAPI(api_options) {
        api_options = api_options || {};

        api_options['format'] = 'json';

        return $.getJSON(this.constructor.url_model, api_options)
            .done( (json) => this.from(json) );
    }
}


/**
 * ArticleList model. Can be accessed through API.
 * @extends Models.APIModelForest
 * @memberof Models
 */
class ArticleList extends APIModelForest {

    /**
     * Default structure for ArticleList instances
     * @abstract
     */
    static get structure() {
        return {
            'article': {
                'model': Article,
                'parent': 'category',
            },
            'category': {
                'model': ArticleCategory,
                'children': 'articles',
                'child_sort': Article.compare,
            },
        };
                    
    }

    /**
     * Comparison function to sort roots
     * @default <tt>{@link Models.ArticleCategory.compare|ArticleCategory.compare}</tt>
     */
    static get root_sort() {
        return ArticleCategory.compare;
    }

    /**
     * Default url to get ArticlList data
     * @abstract
     * @default <tt>django-js-reverse('kfet.kpsul.articles_data')</tt>
     * @see {@link Models.APIModelForest.url_model|APIModelForest.url_model}
     */
    static get url_model() {
        return Urls['kfet.kpsul.articles_data']();
    }
}



/* ---------- ---------- */

/**
 * Virtual namespace for formatters.
 * <br><br>
 *
 * A formatter subclasses {@link Formatters.Formatter}.<br>
 * Formatters should be accessed statically only.
 *
 * @namespace Formatters
 */


/**
 * @memberof Formatters
 */
class Formatter {

    static get props() { return []; }
    static get attrs() { return []; }

    /**
     * Get value of a property through current formatter if defined, object itself otherwise.
     *
     * @param   {*} object - Object used for formatting
     * @param   {string} name - Name of property to render
     * @param   {string} [prefix=''] - Prefix used for formatter method search
     * @return  {*} Formatted property value
     */
    static get(object, name, prefix) {
        prefix = prefix || '';
        var method = prefix + name;
        if (this[method] !== undefined)
            return this[method](object);
        return object[name];
    }

    /**
     * Get formatted value of a property using prefix 'prop_'.
     * @see Formatter.get
     */
    static get_prop(object, name) {
        return this.get(object, name, 'prop_');
    }

    /**
     * Get formatted value of a property using prefix 'attr_'.
     * @see Formatter.get
     */
    static get_attr(obj, attr) {
        return this.get(obj, attr, 'attr_');
    }

    /**
     * Render container.
     * @param   {*} object - Object used for formatting
     * @param   {jQueryNode} $container
     * @param   {object} [options]
     *                   <tt>options['props']</tt> should be an array of
     *                   properties (default: <tt>class.props</tt>).<br>
     *                   <tt>options['prefix_prop']</tt> is added in front of
     *                   properties names in selector (default: <tt>'.'</tt>).<br>
     *                   <tt>options['prefix_attr']</tt> is added in front of
     *                   attributes names for container (default: <tt>''</tt>).
     */
    static render(object, $container, options) {
        options.props = options.props || [];
        options.attrs = options.attrs || [];

        var props = options.override_props ? options.props : this.props.concat(options.props);
        var attrs = options.override_attrs ? options.attrs : this.attrs.concat(options.attrs);

        var prefix_prop = options.prefix_prop !== undefined ? options.prefix_prop : '.';
        var prefix_attr = options.prefix_attr !== undefined ? options.prefix_attr : '';

        for (let prop of props) {
            var selector = prefix_prop + prop;
            var html = options.empty_props ? '' : this.get_prop(object, prop);
            $container.find( selector ).html( html );
        }

        for (let attr of attrs) {
            var name = prefix_attr + attr;
            var value = options.empty_attrs ? '' : this.get_attr(object, attr);
            $container.attr( name, value );
        }

        return $container;
    }

}


/**
 * @memberof Formatters
 * @extends Formatters.Formatter
 */
class AccountFormatter extends Formatter {

    /**
     * Properties renderable in html.
     * @default {@link Models.Account.props}<tt> + ['balance_ukf']</tt>
     */
    static get props() {
        return Account.props.concat(['balance_ukf']);
    }

    /**
     * Added attributes to $container element.
     * @default <tt>['data-balance']</tt>
     */
    static get attrs() {
        return ['data_balance'];
    }

    static get _data_balance() {
        return {
            'default': '', 'frozen': 'frozen',
            'ok': 'ok', 'low': 'low', 'neg': 'neg',
        };
    }

    /**
     * <tt>a.balance</tt> with two decimals.
     */
    static prop_balance(a) {
        return a.balance.toFixed(2);
    }

    /**
     * <tt>'COF' if account is cof or 'Non-COF'
     */
    static prop_is_cof(a) {
        return a.is_cof ? 'COF' : 'Non-COF';
    }

    /**
     * Value of data_attribute according to is_frozen status and balance of
     * account <tt>a</tt>.
     */
    static attr_data_balance(a) {
        if      (a.is_frozen)    { return this._data_balance.frozen; }
        else if (a.balance >= 5) { return this._data_balance.ok;  }
        else if (a.balance >= 0) { return this._data_balance.low; }
        else /* a.balance < 0 */ { return this._data_balance.neg; }
    }

}


/**
 * @memberof Formatters
 * @extends Formatters.AccountFormatter
 */
class LIQFormatter extends AccountFormatter {

    /**
     * Rendering a property always returns the empty string
     * @default <tt>''</tt>
     */
    static get_prop() {
        return '';
    }

    /**
     * Attribute <tt>data_balance</tt> is always <tt>ok</tt>.
     */
    static attr_data_balance(a) { return this._data_balance.ok; }

}


/**
 * @memberof Formatters
 * @extends Formatters.Formatter
 */
class CheckoutFormatter extends Formatter {

    /**
     * Properties renderable to html.
     * @default {@link Models.Checkout.props}
     */
    static get props() {
        return Checkout.props;
    }

    /**
     * <tt>c.balance</tt> with two decimals.
     */
    static prop_balance(c) {
        return c.balance.toFixed(2);
    }

}


/**
 * @memberof Formatters
 * @extends Formatters.Formatter
 */

class StatementFormatter extends Formatter {

    /**
     * Properties renderable to html.
     * @default {@link Models.Statement.props}
     */
    static get props() {
        return Statement.props;
    }

    /**
     * <tt>s.balance_old</tt> with two decimals.
     */
    static prop_balance_old(s) {
        return s.balance_old.toFixed(2);
    }

    /**
     * <tt>s.balance_new</tt> with two decimals.
     */
    static prop_balance_new(s) {
        return s.balance_new.toFixed(2);
    }

    /**
     * <tt>s.at</tt> formatted as <tt>DD/MM/YY à HH:MM</tt>.
     */
    static prop_at(s) {
        return moment.isMoment(s.at) ? s.at.tz('Europe/Paris').format('DD/MM/YY à HH:mm') : s.at;
    }

}


/**
 * @memberof Formatters
 * @extends Formatters.Formatter
 */

class ArticleCategoryFormatter extends Formatter {

    /**
     * Properties renderable to html.
     * @default {@link Models.ArticleCategory.props}
     */
    static get props() {
        return ArticleCategory.props;
    }
}


/**
 * @memberof Formatters
 * @extends Formatters.Formatter
 */

class ArticleFormatter extends Formatter {

    /**
     * Properties renderable to html.
     * @default {@link Models.Article.props}
     */
    static get props() {
        return Article.props;
    }

    static get attrs() {
        return ['data_stock'];
    }

    static prop_price(s) {
        return amountToUKF(s.price, true);
    }

    static get _data_stock() {
        return {
            'default': '', 'low': 'low',
            'ok': 'ok', 'neg': 'neg',
        };
    }

    static attr_data_stock(a) {
        if      (a.stock > 5)  { return this._data_stock.ok; }
        else if (a.stock >= -5) { return this._data_stock.low; }
        else /* a.stock < -5 */ { return this._data_stock.neg; }
    }
}
