function UpdateHelper(scope, fn, timeout) {
    this._timeout = (timeout) ? timeout : 1000;
    this._scope = scope;
    this._fn = fn;
}

UpdateHelper.prototype = {

    update: function() {

        if (this._busy) {
            this._refresh = true;
            return;
        }

        if (this._waitfn) {
            $(document).stopTime(null, this._waitfn);
        }

        var scope = this;

        this._waitfn = function() {
            scope._handleTimer();
        }

        $(document).oneTime(1000, null, this._waitfn);

    },

    waiting: function() {
        return (this._busy || this._waitfn) ? true : false;
    },

    commit: function() {
        this._busy = false;
        if (this._refresh) {
            this._refresh = false;
            this.update();
        }
    },

    _handleTimer: function() {
        this._waitfn = null;
        this._busy = true;
        this._fn.call(this._scope, this);
    }

}

function NavigationTitle(element) {
    this.element = element;
    this.selectHandlers = [];
}

NavigationTitle.prototype = {

    _linkTemplate: "<a href=\"#\" />",

    _badLinkTemplate: "<span />",

    _linkCurrent: "<b />",

    _delimitterTemplate: "<span> » </span>",

    _moreTemplate: "<span>...</span>",

    _notifySelect: function(level) {
        for (var i = 0; i < this.selectHandlers.length; i++) {
            var callback = this.selectHandlers[i];
            callback.handler.call(callback.scope, level, this.status);
        }
    },

    _appendLink: function(level) {
        if (this.status[level].box) {
            var scope = this;
            $(this._linkTemplate).prependTo(this.element)
                .text(this.status[level].name).click(function(event) {
                    scope._notifySelect(level);
                    return false;
                });
        } else {
            $(this._badLinkTemplate).prependTo(this.element)
                .text(this.status[level].name);
        }
    },

    _appendCurrent: function(level) {
        this._appendLink(level);
        return;
        var scope = this;
        $(this._linkCurrent).prependTo(this.element)
            .text(this.status[level].name);
    },

    _appendDelimitter: function() {
        $(this._delimitterTemplate).prependTo(this.element);
    },

    _appendMore: function() {
        $(this._moreTemplate).prependTo(this.element);
    },

    setLocation: function(status) {

        this.status = status;

        this.element.html("");

        var keys = ['district', 'subarea', 'area', 'subregion', 'region',
            'country'];
        var cnt = 0;

        for (var i = 0; i < keys.length; i++) {
            var name = keys[i];
            //		console.debug('check', name);
            if (name in status) {
                if (cnt > 0) this._appendDelimitter();
                if (cnt == 3) {
                    this._appendMore();
                    break;
                }
                this._appendLink(name);
                cnt++;
            }
        }

    },

    addSelectHandler: function(scope, handler) {
        this.selectHandlers.push({
            scope: scope,
            handler: handler
        });
    }

}

function NavLink(nav, map, geo, sel) {

    this._nav = nav;
    this._map = map;
    this._geo = geo;
    this._sel = sel;

    this._events = new myterra.Observable();

    this._updateHelper = new UpdateHelper(this, this._updateRegionNames);

    this._map.addBoundsChangeHandler(this, function(south, west, north, east) {
        this._startUpdate();
    });

    this._map.addDragEndHandler(this, function() {
        this._notifyChange();
    });

    this._nav.addSelectHandler(this, function(level, status) {
        this._notifyChange();

        var pos = status[level].box;

        this._map.setRangeBound("region", pos.south, pos.west, pos.north,
            pos.east, "red");

        var frame = geofunc.getBaseRangeSize(pos.south, pos.west, pos.north,
            pos.east);
        this._map.setMapFrame(frame.lat, frame.lng, frame.latSize,
            frame.lngSize);
    });

    if (sel) {
        sel.addListener("regionselect", function(s, region) {
            this.seek(region);
        }, this);
    }

    var frame = this._map.getMapFrame();

    if (frame) {
        this._startUpdate();
    }
}

NavLink.prototype = {

    _notifyChange: function() {
        this._events.fireEvent("startchange");
    },

    _checkBounds: function(container, area) {
        return area && (container.south <= area.south)
            && (container.north >= area.north) && (container.west <= area.west)
            && (container.east >= area.east);
    },

    _startUpdate: function() {
        this._updateHelper.update();
    },

    _currentRegion: function(status) {
        var bounds = this._map.getMapFrame();

        bounds = geofunc.getLatLngSizeRange(bounds.lat, bounds.lng,
            bounds.latSize, bounds.lngSize);

        var limspace = Math.abs((bounds.north - bounds.south)
            * (bounds.east - bounds.west))
            * 0.02;

        var region = null;
        var keys = ['country', 'region', 'subregion', 'area', 'subarea',
            'district'];

        var lev = 0;

        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            if (!(key in status)) continue;
            var box = status[key].box;
            console.debug(status[key].name, 'box', box);
            if (box && lev > 1) {
                var space = Math.abs((box.north - box.south)
                    * (box.east - box.west));
                console.debug('space', space);
                if (space < limspace) break;
            }
            lev++;
            region = key;
        }

        return region;
        
    },

    _selectNavRegion: function(status) {

        var keys = ['region', 'subregion', 'area', 'subarea', 'district'];

        var current = this._currentRegion(status);
        var regions = [];

        this._lastArea = null;

        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            if (!(key in status)) continue;
            if (key == 'area') this._lastArea = status['area'].box;
            regions.push(key);
            if (key == current) break;
        }

        filter = {};

        for (var i = 0; i < regions.length; i++) {
            filter[regions[i]] = status[regions[i]];
        }

        this._nav.setLocation(filter);

        this._events.fireEvent("changeregion", filter, status);

    },

    _updateRegionNames: function() {

        this._notifyChange();

        var bounds = this._map.getMapFrame();

        this._geo.geocode(bounds.lat, bounds.lng, this, function(status) {
            this._updateHelper.commit();
            this._selectNavRegion(status.best);
        });

    },

    seek: function(address, scope, nfcallback) {

        console.debug('NAV seek initiate');

        var frame = null;

        if (this._lastArea) {
            console.debug('****** Last area', this._lastArea);

            this._geo.seek2(address, this._lastArea, this, function(status) {
                console.debug('NAV seek complete', status);

                if (status.best) {
                    var en = ['district', 'subarea', 'area', 'subregion',
                        'region', 'country'];

                    var best = status.best;

                    if (best.box) {
                        this._map.setMapBounds(best.box.south, best.box.west,
                            best.box.north, best.box.east);
                        return;
                    }

                    if (best.location) {
                        this._map.setMapFrame(best.location.lat,
                            best.location.lng, 500, 500);
                        return;
                    }

                    for (var i = 0; i < en.length; i++) {
                        if (en[i] in best && best[en[i]].box) {
                            var pos = best[en[i]].box;
                            this._map.setMapBounds(pos.south, pos.west,
                                pos.north, pos.east);
                            return;
                        }
                    }
                }

                this._addressNotFoundAlert(address, scope, nfcallback);

            });

        } else {

            this._geo.seek(address, this, function(status) {
                console.debug('NAV seek complete', status);

                if (status.best) {
                    var en = ['district', 'subarea', 'area', 'subregion',
                        'region', 'country'];

                    var best = status.best;

                    if (best.box) {
                        this._map.setMapBounds(best.box.south, best.box.west,
                            best.box.north, best.box.east);
                        return;
                    }

                    if (best.location) {
                        this._map.setMapFrame(best.location.lat,
                            best.location.lng, 500, 500);
                        return;
                    }

                    for (var i = 0; i < en.length; i++) {
                        if (en[i] in best && best[en[i]].box) {
                            var pos = best[en[i]].box;
                            this._map.setMapBounds(pos.south, pos.west,
                                pos.north, pos.east);
                            return;
                        }
                    }
                }

                this._addressNotFoundAlert(address, scope, nfcallback);

            });

        }

    },

    _addressNotFoundAlert: function(address, scope, nfcallback) {

        if (nfcallback) {
            nfcallback.call(scope, address);
            return;
        }

        if (this._defaultAddressNotFoundHandler) {
            this._defaultAddressNotFoundHandler.handler.call(
                this._defaultAddressNotFoundHandler.scope, address);
            return;
        }

        myterra.showMessage("Адрес \"" + address + "\" не найден.");

    },

    setAddressNotFoundHandler: function(scope, callback) {
        this._defaultAddressNotFoundHandler = {
            handler: callback,
            scope: scope
        };
    },

    addChangeRegionHandler: function(scope, handler) {
        this._events.addListener("changeregion", handler, scope);
    },

    addStartChangeHandler: function(scope, handler) {
        this._events.addListener("startchange", handler, scope);
    }

}
