amp = Lanes.Vendor.Ampersand
delegateEventSplitter = /^(\S+)\s*(.*)$/
class ViewBase

    extendedProperties: ['ui','subviews']

    # Custom datatypes for views
    dataTypes:
        element:
            compare: (a, b)-> a is b
            set: (newVal)->
                val: newVal
                type: (if newVal instanceof Element then "element" else typeof newVal)

        collection:
            compare: (currentVal, newVal)-> currentVal is newVal
            set: (newVal)->
                val: newVal
                type: (if newVal and newVal.isCollection then "collection" else typeof newVal)

    session:
        el         : "element"
        model      : "state"
        collection : "collection"
        pubSub     : "boolean"
        parent     : "object"
        subviewId  : { type: "string", setOnce: true }
        formBindings: { type: 'any', default: false }

    derived:
        '$el':
            deps: ['el'], cached: false
            fn: -> Lanes.$(this.el)

        root_view:
            deps: ['parent'], fn: ->
                view = this
                while view.parent
                    view = view.parent
                if view == this then null else view

        rendered:
            deps: ["el"], fn: -> !!@el

        hasModels:
            deps: ["model"], fn: -> !!@model

        viewport:
            deps: ['parent'], fn:->
                this.root_view?.viewport


    constructor: (attrs={})->

        this.cid = _.uniqueId('view');
        attrs || (attrs = {});
        parent = attrs.parent;
        delete attrs.parent;

        this._substituteEventUI()

        Lanes.Vendor.Ampersand.State.call(this, attrs, {init: false, parent: parent})

        this.on('change:$el',   this._onElementChange, this)
        this.on('change:model', this._onModelChange, this)
        this.on('change:collection', this._onCollectionChange, this)
        this._onModelChange()      if @model
        this._onCollectionChange() if @collection
        this._parsedBindings = Lanes.Vendor.Ampersand.Bindings(this.bindings, this)
        this._initializeBindings();
        if attrs.el && !this.autoRender
            this._onElementChange();

        this._initializeSubviews();
        this.initialize.apply(this, arguments);

        this.set(_.pick(attrs, 'model', 'collection', 'el'))

        this.render() if this.autoRender && this.template

        if !this.pubSub? # if it's unset, not true/false; default to parent or true
            this.pubSub = if this.parent?.pubSub? then this.parent.pubSub else true

        if @formBindings || @useFormBindings || @parent?.formBindings
            @formBindings = new Lanes.Views.FormBindings(this, @formBindings)

        if @keyBindings
            Lanes.Views.Keys.add(this, @keyBindings, @keyScope)

        if @pubSub
            this._pubSub = new Lanes.Views.PubSub(this)


    _normalizeUIString: (uiString, ui)->
        uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, (r)->
            return ui[r.slice(4)]
        )

    _substituteEventUI: ->
        return unless @ui
        if @domEvents
            for selector in _.keys(@domEvents)
                replaced_selector = this._normalizeUIString(selector,@ui)
                if replaced_selector != selector
                    @domEvents[replaced_selector] = @domEvents[selector]
                    delete @domEvents[selector]
        # TODO - also apply ui to binding keys


    # Initialize is an empty function by default. Override it with your own
    # initialization logic.
    initialize: Lanes.emptyFn

    # **render** is the core function that your view can override, its job is
    # to populate its element (`this.el`), with the appropriate HTML.
    render: ->
        Lanes.Views.RenderContext.push( @subviewId, @model )
        this.renderContextFree()
        Lanes.Views.RenderContext.pop()
        this

    renderContextFree: ->
        this.replaceEl( this.renderTemplateMethod() );
        this

    # ## listenToAndRun
    # Shortcut for registering a listener for a model
    # and also triggering it right away.
    listenToAndRun: (object, events, handler)->
        bound = _.bind(handler, this)
        this.listenTo(object, events, bound)
        bound();

    # Replaces the current root element with the contents of template
    replaceEl: (template)->
        newDom = Lanes.Vendor.domify(template)
        parent = this.el && this.el.parentNode;
        parent.replaceChild(newDom, this.el) if parent
        if newDom.nodeName == '#document-fragment'
            throw new Error("View #{Lanes.u.path(this.FILE)} can only have one root element.")
        if newDom.nodeName == '#text'
            throw new Error("View #{Lanes.u.path(this.FILE)} must have a root element.")
        this.el = newDom;
        this.onRender?()
        return this;

    # Renders the results of calling method "name"
    # to a string and returns it.
    #
    # The template can either be a string property or a function that returns a string.
    renderTemplateMethod:(name="template",data)->
        template = if this[name]
            _.result(this, name)
        else
            data ||= _.result(this,"#{name}Data")
            template_name = _.result(this, "#{name}Name")
            path = this.resultsFor('templatePrefix', template_name) + '/' + template_name
            Lanes.Templates.render(this, path, data)
        throw new Error("#{Lanes.u.path(this.FILE)} failed to render #{path || name}") unless template
        template

    templateName: -> _.dasherize(_.last(this.FILE.path))

    templatePrefix: (template)->
        if template == "empty-span"
            "lanes/views"
        else
            Lanes.u.dirname(this.FILE)

    templateData: ->
        { model: @model?.toJSON?(), collection: @collection?.toJSON?() }

    FILE: FILE

    # Remove this view by taking the element out of the DOM, and removing any
    # applicable events listeners.
    remove: ->
        parsedBindings = this._parsedBindings;
        if this.el && this.el.parentNode then this.el.parentNode.removeChild(this.el);
        if this._subviews then _.chain(this._subviews).flatten().invoke('remove');
        this.stopListening();
        # TODO: Not sure if this is actually necessary.
        # Just trying to de-reference this potentially large
        # amount of generated functions to avoid memory leaks.
        _.each(parsedBindings, (properties, modelName)->
            _.each(properties, (value, key)->
                delete parsedBindings[modelName][key];
            );
            delete parsedBindings[modelName];
        );
        this._unbindFromObject(@model, @modelEvents) if @model and @modelEvents
        this._unbindFromObject(@collection, @collectionEvents) if @collection and @collectionEvents

        Lanes.Views.Keys.remove(this, @keyBindings, @keyScope) if @keyBindings
        this.trigger('remove', this);
        return this;

    # Change the view's element (`this.el` property), including event re-delegation.
    _onElementChange: (element, delegate) ->
        if changes = this.changedAttributes()
            Lanes.$(changes['el']).off('.delegateEvents' + this.cid) if changes['el']
        this.bindDomEvents()
        this._cacheUI()
        this._applyBindingsForKey();

    _cacheUI:->
        return unless this.ui
        # store the ui hash in _uiBindings so they can be reset later
        # and so re-rendering the view will be able to find the bindings
        this._uiBindings = this.ui unless this._uiBindings

        # get the bindings result, as a function or otherwise
        bindings = _.result(this, '_uiBindings')

        # empty the ui so we don't have anything to start with
        this.ui = {};

        # bind each of the selectors
        for key, selector of bindings
            this.ui[key] = this.$el.filter(selector).add(this.$el.find(selector))


    # Sets callbacks, where `this.domEvents` is a hash of
    #
    # *{"event selector": "callback"}*
    #
    #     {
    #       'mousedown .title':  'edit',
    #       'click .button':     'save',
    #       'click .open':       function (e) { ... }
    #     }
    #
    # pairs. Callbacks will be bound to the view, with `this` set properly.
    # Uses event delegation for efficiency.
    # Omitting the selector binds the event to `this.el`.
    # This only works for delegate-able events: not `focus`, `blur`, and
    # not `change`, `submit`, and `reset` in Internet Explorer.
    bindDomEvents: (events)->
        return this if (!(events || (events = _.result(this, 'domEvents'))))
        this.unbindDomEvents();
        for key, method of events
            method = this[ method ] unless _.isFunction(method);
            continue unless method
            match = key.match( delegateEventSplitter )
            this.bindEvent(match[1], match[2], _.bind(method, this))
        return this;

    # Add a single event listener to the view's element (or a child element
    # using `selector`). This only works for delegate-able events: not `focus`,
    # `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
    bindEvent: (eventName, selector, listener)->
        this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener)

    # Clears all callbacks previously bound to the view with `delegateEvents`.
    # You usually don't need to use this, but may wish to if you have multiple
    # views attached to the same DOM element.
    unbindDomEvents: ->
        this.$el.off('.delegateEvents' + this.cid) if this.el
        return this;

    # A finer-grained `unbind` for removing a single delegated event.
    # `selector` and `listener` are both optional.
    unbind: (eventName, selector, listener)->
        this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener)


    _applyBindingsForKey: (name)->
        return unless this.el
        for item, fns of this._parsedBindings.getGrouped(name)
            for fn in fns
                fn(this.el, _.getPath(this, item), _.last(item.split('.')))

    _initializeBindings: ->
        return unless this.bindings
        this.on('all', (eventName)->
            if eventName.slice(0, 7) == 'change:'
                this._applyBindingsForKey(eventName.split(':')[1]);
        ,this);

    # ## _initializeSubviews
    # this is called at setup and grabs declared subviews
    _initializeSubviews: ->
        return unless this.subviews
        for name, definition of this.subviews
            this._parseSubview(definition, name)

    # ## _subviewClass
    # helper to detect the class for a given subviev
    _subviewClass: (subview)->
        klass = if subview.component
            Lanes.u.findObject(subview.component, 'Components', this.FILE)
        else if subview.view
            if _.isString(subview.view)
                Lanes.u.getPath(subview.view, this.subviewPrefix() )
            else
                subview.view
        Lanes.warn( "Unable to obtain view for %o", subview) if ! klass
        klass

    # ## subviewPrefix
    # returns the namespace that the views should be in
    # is needed so views can be specified by only thier name, rather than complete path
    subviewPrefix: ->
        Lanes.u.objectPath(this.FILE)

    # ## _parseSubview
    # helper for parsing out the subview declaration and registering
    # the `waitFor` if need be.
    _parseSubview: (subview, name)->
        self = this;
        opts = {
            selector: subview.container || "[data-hook='#{subview.hook||name}']"
            waitFor: subview.waitFor || subview.model || subview.collection || ''
            prepareView: subview.prepareView || (el,options)->
                klass = self._subviewClass(subview)
                if subview.collection
                    new Lanes.Vendor.Ampersand.CollectionView({
                        el: el, parent: self, viewOptions: options, view: klass,
                        collection: _.getPath(this, subview.collection)
                    })
                else
                    options = _.extend(options,{ el: el, parent: self })
                    for attr in ['model','data']
                        if subview[attr]
                            options[attr] = _.getPath(this, subview[attr])
                    new klass(options)

        }
        action = ->
            return if (!this.el || !(el = this.query(opts.selector)))
            if (!opts.waitFor || _.getPath(this, opts.waitFor))
                options = this.subviewOptions(name,subview)
                options.subviewId = name
                subview = this[name] = opts.prepareView.call(this, el, options);
                subview.render();
                this.off('change', action);

        # we listen for main `change` items
        this.on('change', action, this);

    # ## subviewOptions
    # Options to initailize a subview with when it's created
    # Subviews will always be passed the parent (this) and the el.
    # Additional options can be given by specifying "options" on the definition
    # The options can be a method name to call or an object
    # This method can also be overridden to specify the same options to all subviews
    subviewOptions: (name,definition)->
        this.resultsFor(definition.options, name, definition) || {}

    # ## query
    # Get an single element based on CSS selector scoped to this.el
    # if you pass an empty string it return `this.el`.
    # If you pass an element we just return it back.
    # This lets us use `get` to handle cases where users
    # can pass a selector or an already selected element.
    query: (selector)->
        return this el unless selector
        if typeof selector == 'string'
            return this.el if this.$el.is(selector)
            return this.el.querySelector(selector) || undefined;
        return selector;

    # Called when a different model is set
    _onModelChange: ->
        for name, definition of this.subviews
            continue unless ( view = this[name] )
            view.model = Lanes.u.getPath(definition.model, this) if definition.model
            view.collection = Lanes.u.getPath(definition.collection, this) if definition.collection
        prev = this.previous('model')
        return if prev == @model
        this._unbindFromObject(prev, @modelEvents) if prev
        this._bindToObject(@model, @modelEvents)
        this.onModelChange?()
        true

    _onCollectionChange: ->
        prev = this.previous('collection')
        return if prev == @collection
        this._unbindFromObject(prev, @collectionEvents) if prev
        this._bindToObject(@collection, @collectionEvents)

    _bindToObject: (state_object,events)->
        for event, fn of events
            fn = this[fn] unless _.isFunction(fn)
            this.listenTo(state_object, event, fn, this)

    _unbindFromObject: (state_object,events)->
        for event, fn of events
            fn = this[fn] unless _.isFunction(fn)
            this.stopListening(state_object, event, fn, this)

    initializeKeyBindings: Lanes.emptyFn


    setFieldsFromBindings: ->
        binding.setAllFields() for name, binding of @_form_bindings

    viewpath: ''

    detach: ->
        this.el.parentNode.removeChild(this.el) if this.el.parentNode

    $: (selector)->
        this.$el.find(selector)


    parentScreen: ->
        view = this
        while view and ! ( view instanceof Lanes.Views.Screen )
            view = view.parent
        view

    @extended: (klass)->
        if klass::events && !klass::domEvents
            klass::domEvents == klass::events
            delete klass::events
        # perhaps we should merge 'events','bindings'?


    Lanes.lib.ModuleSupport.includeInto(@)
    @include Lanes.lib.defer
    @include Lanes.lib.debounce
    @include Lanes.lib.results


Lanes.Views.Base = Lanes.lib.MakeBaseClass( Lanes.Vendor.Ampersand.State, ViewBase )