define(['./viewLocator', './viewModelBinder', './viewEngine', './system', './viewModel'],
    function (viewLocator, viewModelBinder, viewEngine, system, viewModel) {

    var dummyModel = {},
        activeViewAttributeName = 'data-active-view';

    function shouldPerformActivation(settings) {
        return settings.model && settings.model.activate
            && ((composition.activateDuringComposition && settings.activate == undefined) || settings.activate);
    }

    function tryActivate(settings, successCallback) {
        if (shouldPerformActivation(settings)) {
            viewModel.activator().activateItem(settings.model).then(function (success) {
                if (success) {
                    successCallback();
                }
            });
        } else {
            successCallback();
        }
    }

    function getHostState(parent) {
        var elements = [];
        var state = {
            childElements: elements,
            activeView: null
        };

        var child = ko.virtualElements.firstChild(parent);

        while (child) {
            if (child.nodeType == 1) {
                elements.push(child);
                if (child.getAttribute(activeViewAttributeName)) {
                    state.activeView = child;
                }
            }

            child = ko.virtualElements.nextSibling(child);
        }

        return state;
    }

    function afterContentSwitch(parent, newChild, settings) {
        if (settings.activeView) {
            settings.activeView.removeAttribute(activeViewAttributeName);
        }

        if (newChild) {
            if (settings.model && settings.model.viewAttached) {
                if (settings.composingNewView || settings.alwaysAttachView) {
                    settings.model.viewAttached(newChild);
                }
            }

            newChild.setAttribute(activeViewAttributeName, true);
        }

        if (settings.afterCompose) {
            settings.afterCompose(parent, newChild, settings);
        }
    }

    function shouldTransition(newChild, settings) {
        if (typeof settings.transition == 'string') {
            if (settings.activeView) {
                if (settings.activeView == newChild) {
                    return false;
                }

                if (!newChild) {
                    return true;
                }

                if (settings.skipTransitionOnSameViewId) {
                    var currentViewId = settings.activeView.getAttribute('data-view');
                    var newViewId = newChild.getAttribute('data-view');
                    return currentViewId != newViewId;
                }
            }
            
            return true;
        }
        
        return false;
    }

    var composition = {
        activateDuringComposition: false,
        convertTransitionToModuleId: function (name) {
            return 'durandal/transitions/' + name;
        },
        switchContent: function (parent, newChild, settings) {
            settings.transition = settings.transition || this.defaultTransitionName;

            if (shouldTransition(newChild, settings)) {
                var transitionModuleId = this.convertTransitionToModuleId(settings.transition);
                system.acquire(transitionModuleId).then(function (transition) {
                    settings.transition = transition;
                    transition(parent, newChild, settings).then(function () {
                        afterContentSwitch(parent, newChild, settings);
                    });
                });
            } else {
                if (newChild != settings.activeView) {
                    if (settings.cacheViews && settings.activeView) {
                        $(settings.activeView).css('display', 'none');
                    }

                    if (!newChild) {
                        if (!settings.cacheViews) {
                            ko.virtualElements.emptyNode(parent);
                        }
                    } else {
                        if (settings.cacheViews) {
                            if (settings.composingNewView) {
                                settings.viewElements.push(newChild);
                                ko.virtualElements.prepend(parent, newChild);
                            } else {
                                $(newChild).css('display', '');
                            }
                        } else {
                            ko.virtualElements.emptyNode(parent);
                            ko.virtualElements.prepend(parent, newChild);
                        }
                    }
                }

                afterContentSwitch(parent, newChild, settings);
            }
        },
        bindAndShow: function (element, view, settings) {
            if (settings.cacheViews) {
                settings.composingNewView = (ko.utils.arrayIndexOf(settings.viewElements, view) == -1);
            } else {
                settings.composingNewView = true;
            }

            tryActivate(settings, function () {
                if (settings.beforeBind) {
                    settings.beforeBind(element, view, settings);
                }

                if (settings.preserveContext && settings.bindingContext) {
                    if (settings.composingNewView) {
                        viewModelBinder.bindContext(settings.bindingContext, view, settings.model);
                    }
                } else if (view) {
                    var modelToBind = settings.model || dummyModel;
                    var currentModel = ko.dataFor(view);

                    if (currentModel != modelToBind) {
                        if (!settings.composingNewView) {
                            $(view).remove();
                            viewEngine.createView(view.getAttribute('data-view')).then(function(recreatedView) {
                                composition.bindAndShow(element, recreatedView, settings);
                            });
                            return;
                        }
                        viewModelBinder.bind(modelToBind, view);
                    }
                }

                composition.switchContent(element, view, settings);
            });
        },
        defaultStrategy: function (settings) {
            return viewLocator.locateViewForObject(settings.model, settings.viewElements);
        },
        getSettings: function (valueAccessor, element) {
            var value = ko.utils.unwrapObservable(valueAccessor()) || {};

            if (typeof value == 'string') {
                return value;
            }

            var moduleId = system.getModuleId(value);
            if (moduleId) {
                return {
                    model: value
                };
            }

            for (var attrName in value) {
                value[attrName] = ko.utils.unwrapObservable(value[attrName]);
            }

            return value;
        },
        executeStrategy: function (element, settings) {
            settings.strategy(settings).then(function (view) {
                composition.bindAndShow(element, view, settings);
            });
        },
        inject: function (element, settings) {
            if (!settings.model) {
                this.bindAndShow(element, null, settings);
                return;
            }

            if (settings.view) {
                viewLocator.locateView(settings.view, settings.area, settings.viewElements).then(function (view) {
                    composition.bindAndShow(element, view, settings);
                });
                return;
            }

            if (settings.view !== undefined && !settings.view) {
                return;
            }

            if (!settings.strategy) {
                settings.strategy = this.defaultStrategy;
            }

            if (typeof settings.strategy == 'string') {
                system.acquire(settings.strategy).then(function (strategy) {
                    settings.strategy = strategy;
                    composition.executeStrategy(element, settings);
                });
            } else {
                this.executeStrategy(element, settings);
            }
        },
        compose: function (element, settings, bindingContext) {
            if (typeof settings == 'string') {
                if (viewEngine.isViewUrl(settings)) {
                    settings = {
                        view: settings
                    };
                } else {
                    settings = {
                        model: settings
                    };
                }
            }

            var moduleId = system.getModuleId(settings);
            if (moduleId) {
                settings = {
                    model: settings
                };
            }

            var hostState = getHostState(element);

            settings.bindingContext = bindingContext;
            settings.activeView = hostState.activeView;

            if (settings.cacheViews && !settings.viewElements) {
                settings.viewElements = hostState.childElements;
            }

            if (!settings.model) {
                if (!settings.view) {
                    this.bindAndShow(element, null, settings);
                } else {
                    settings.area = settings.area || 'partial';
                    settings.preserveContext = true;

                    viewLocator.locateView(settings.view, settings.area, settings.viewElements).then(function (view) {
                        composition.bindAndShow(element, view, settings);
                    });
                }
            } else if (typeof settings.model == 'string') {
                system.acquire(settings.model).then(function (module) {
                    if (typeof (module) == 'function') {
                        settings.model = new module(element, settings);
                    } else {
                        settings.model = module;
                    }

                    composition.inject(element, settings);
                });
            } else {
                composition.inject(element, settings);
            }
        }
    };

    ko.bindingHandlers.compose = {
        update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            var settings = composition.getSettings(valueAccessor);
            composition.compose(element, settings, bindingContext);
        }
    };

    ko.virtualElements.allowedBindings.compose = true;

    return composition;
});