//= 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.implementsMethods(body, "get") ||
                         Object.implementsMethods(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 ".+".