//= require ./lib/patterns //= require ./lib/extensions

// See how that must be resolved without compromising the builder pattern

var

esPhinx,
Iterable,
SearchContext,
Search;

// IIFE (function($module) {

"use strict";

// closure (private static attribute)
var
    NAME = "esPhinx",

    selectInBreadth = function(iterable, callback) {
        var
            strategy;

        if (iterable instanceof window.Node) {
            strategy = SearchContext.Proxy.new(Search.Graphs.BFS.Element
                                               .new(iterable));
        } else {
            strategy = SearchContext.Proxy.new(Search.Graphs.BFS.Object
                                               .new(iterable));
        }

        return strategy.research(callback);
    },

    resolveContext = function(context) {
        if (typeof context == "object") {
            if (context instanceof window.esPhinx ||
                context instanceof Element) {
                return context;
            }
        } else if (typeof context == "string") {
            // recursivity
            return window.esPhinx(context);
        }

        return window.document;
    },

    Extender = {
        extend: function(object, final, structure) {
            var
                context,
                strategy,
                verbose = false,
                propertyDescriptors = {
                    enumerable: true,
                    configurable: false
                },

                // create or mount the context. If there is trace and object type is an Object, but it doesn't exists, it will be created, else it will be mounted.
                mountContext = function(methodName, trace, originalObject) {
                    var
                        context = originalObject,

                        callback = function(v) {
                            if (!context[v]) {
                                context[v] = {};
                            }

                            context = context[v];
                        },

                        isAnAccessor = function(methodName, trace) {
                            return (methodName == "get" ||
                                    methodName == "set") && trace.length;
                        };

                    if ((trace.length &&
                         !isAnAccessor(methodName, trace)) ||
                        methodName != "get" && methodName != "set") {

                        trace.forEach(callback);
                    }

                    return context;
                },

                informeIf = function(verbose) {
                    if (verbose) {
                        console.warn("Property \"" + name +
                            "\" of class \"" +
                            Object.className(context) +
                            "\" can't be redefined because " +
                            "it's configured as read-only.");
                    }
                },

                hasAnyAccessor = function(body) {
                    return Object.implementsMethod(body, "get") ||
                     Object.implementsMethod(body, "set");
                },

                callback = function(body, name, trace) {

                    if (!Object.belongToClass(body, Object) ||
                        // otherwise it will arrive there still
                        Object.empty(body) || hasAnyAccessor(body)) {

                        context = mountContext(name, trace, object);

                        // propertyDescriptors = JSON
                        //     .parse(JSON.parse(JSON.stringify("{\"" + name +
                        //     "\":" + JSON.stringify(propertyDescriptors) + "}")));

                        if (Object.belongToClass(body, Object)){
                            if (hasAnyAccessor(body)) {
                                delete propertyDescriptors.writable;
                                delete propertyDescriptors.configurable;
                                propertyDescriptors.get = body.get ||
                                    function() {};
                                propertyDescriptors.set = body.set ||
                                    function() {};
                            } else if (Object.empty(body)) {
                                propertyDescriptors.value = {};
                            }
                        } else {
                            if (typeof body == "function") {
                                delete propertyDescriptors.enumerable;
                            }

                            propertyDescriptors.value = body;
                        }

                        try {
                            Object.defineProperty(context, name,
                                                  propertyDescriptors);
                        } catch (e) {
                            informeIf(verbose);
                        }

                        // restart propertyDescriptors
                        propertyDescriptors = {
                            enumerable: true,
                            configurable: false,
                            writable: !final
                        };
                    }

                };

            if (!structure && (typeof final == "object" ||
                typeof final == "function")) {
                structure = final;
            }

            if (typeof final != "boolean") {
                final = false;
            }
            propertyDescriptors.writable = !final;
            propertyDescriptors.configurable = false;

            strategy = SearchContext.Proxy.new(Search.Graphs.BFS.Object
                                               .new(structure));
            strategy.research(callback);
        }
    };

if (!$module.esPhinx) {

    Extender.extend($module, {
        esPhinx: function(selector, context) {
            var
                parsed,
                self = window.esPhinx,
                mainReference = this,
                collection = [],

                callback = function(node) {
                    collection = collection.concat(Array
                        .from(node
                        .querySelectorAll(selector)))
                    .flatten();
                },

                hasElement = function(iterable) {
                    var
                        iterator,
                        response = false,

                        callback = function(object) {
                            if (object instanceof window.Element) {
                                this.finalize();
                                response = true;
                            }
                        };

                    iterator = Iterable.Proxy.new(iterable);
                    iterator.each(callback);
                    return response;
                };

            if (!(mainReference instanceof self)) {
                return new self(selector, context);
            }

            context = resolveContext(context);

            if (selector) {
                if (selector instanceof self) {
                    return selector;
                } else if (typeof selector == "function") {
                    // don't never pass a autoexecutable function as parameter
                    return window.document
                        .addEventListener("DOMContentLoaded",
                                          function(e) {
                        selector.call(self, self, e);
                    });
                // get children "".match(/(>)(<.(?=(<\/)))/)[0]
                // add support to XPath
                } else if (typeof selector == "string") {
                    parsed = (new window.DOMParser())
                        .parseFromString(selector, "text/html");

                    if (parsed.head.childElementCount) {
                        collection = Array.from(parsed.head
                                                .childNodes);
                    } else {
                        collection = Array.from(parsed.body
                                                .childNodes);
                    }

                    if (!hasElement(collection)) {
                        collection = [];
                        if (Iterable.isIterable(context)) {
                            Array.from(context)
                                .forEach(callback);
                        } else {
                            try {
                                collection = Array.from(context
                                    .querySelectorAll(selector));
                            } catch (e) {}
                        }
                    }
                } else if (selector instanceof window.Node) {
                    collection = [selector];
                } else if (selector instanceof Object) {
                    collection = Array.from(selector);

                    if (!Object.belongToClass(selector,
                        window.HTMLCollection) &&
                        collection.length) {
                        mainReference[0] = selector;

                        if (selector == window) {
                            collection = [];
                        }
                    }
                }

                Object.assign(mainReference, collection);
            }

            Extender.extend(mainReference, {
                splice: Array.prototype.splice,

                toString: function() {
                    return "[object " + NAME + "]";
                }
            });

            Iterable.toIterable(mainReference);

            return mainReference;
        },

    });

    Extender.extend(esPhinx, {
        Extender: {}
    });

    Extender.extend(esPhinx.Extender, {
        extend: function(object, final, structure) {
            Extender.extend(object, final, structure);
        }
    });

}

esPhinx.Extender.extend(esPhinx, true, {
    extend: function() {
        this.Extender.extend(this, arguments[0], arguments[1]);
    }
});

esPhinx.Extender.extend(esPhinx.prototype, true, {
    extend: function() {
        esPhinx.Extender.extend(this, arguments[0], arguments[1]);
    }
});

esPhinx.extend(true, {

    selectByText: function(text, context) {
        var
            collection = [],

            callback = function(element) {
                collection = collection.concat(document.evaluate(
                    "descendant-or-self::*[normalize-space(text())='" +
                    text.trim() + "']", element).elements()).flatten();
            };

        this.select(this(resolveContext(context)), callback);

        return esPhinx(collection);
    },

    isIterable: function(object) {
        return Iterable.isIterable(object);
    },

    selectInBreadth: function(collection, callback) {
        return selectInBreadth(collection, callback);
    },

    each: function(collection, startingIndex, finalIndex, callback) {
        var
            iterator = Iterable.Proxy.new(collection);

        iterator.each(startingIndex, finalIndex, callback);
    },

    reverseEach: function(collection, startingIndex, callback) {
        var
            iterator = Iterable.Proxy.new(collection);

        if (typeof startingIndex == "function") {
            callback = startingIndex;
        }

        iterator.reverseEach(startingIndex, callback);
    },

    select: function(collection, callback) {
        var
            iterator = Iterable.Proxy.new(collection);

        return iterator.select(callback);
    }

});

})(window);

// recognize a element // (^( *<)[a-z]+ *)( *((/)|(>.*</[a-z]+))(> *)$)

// recognize attributes and values // It's not possible capture attributes and their values, because their values can be given any character, which forces them to use “.+”.