"use strict";

// # can-component.js
// This implements the `Component` which allows you to create widgets
// that use a view, a view-model, and custom tags.
//
// `Component` implements most of it's functionality in the `Component.setup`
// and the `Component.prototype.setup` functions.
//
// `Component.setup` prepares everything needed by the `Component.prototype.setup`
// to hookup the component.
var namespace = require('can-namespace');
var Bind = require("can-bind");
var Construct = require("can-construct");
var stache = require("can-stache");
var stacheBindings = require("can-stache-bindings");
var Scope = require("can-view-scope");
var viewCallbacks = require("can-view-callbacks");
var canReflect = require("can-reflect");
var SimpleObservable = require("can-simple-observable");
var SimpleMap = require("can-simple-map");
var DefineMap = require("can-define/map/map");
var canLog = require('can-log');
var canDev = require('can-log/dev/dev');
var assign = require('can-assign');
var ObservationRecorder = require("can-observation-recorder");
var queues = require("can-queues");
var domData = require('can-dom-data');
var string = require("can-string");
var domEvents = require('can-dom-events');
var domMutate = require('can-dom-mutate');
var domMutateNode = require('can-dom-mutate/node');
var canSymbol = require('can-symbol');
var DOCUMENT = require('can-globals/document/document');

var ComponentControl = require("./control/control");

// #### Side effects

require('can-view-model');
// DefineList must be imported so Arrays on the ViewModel
// will be converted to DefineLists automatically
require("can-define/list/list");

// Makes sure bindings are added simply by importing component.
stache.addBindings(stacheBindings);

// #### Symbols
var createdByCanComponentSymbol = canSymbol("can.createdByCanComponent");
var getValueSymbol = canSymbol.for("can.getValue");
var setValueSymbol = canSymbol.for("can.setValue");
var viewInsertSymbol = canSymbol.for("can.viewInsert");
var viewModelSymbol = canSymbol.for('can.viewModel');


// ## Helpers

// ### addContext
// For replacement elements like `<can-slot>` and `<context>`, this is used to
// figure out what data they should render with.  Slots can have bindings like
// `this:from="value"` or `x:from="y"`.
//
// If `this` is set, a compute is created for the context.
// If variables are set, a variable scope is created.
//
// Arguments:
//
// - el - the insertion element
// - tagData - the tagData the insertion element will be rendered with
// - insertionElementTagData - the tagData found at the insertion element.
//
// Returns: the tagData the template should be rendered with.
function addContext(el, tagData, insertionElementTagData) {
	var vm,
		newScope;

	// Prevent setting up bindings manually.
	domData.set(el, "preventDataBindings", true);

	var teardown = stacheBindings.behaviors.viewModel(el, insertionElementTagData,
		// `createViewModel` is used to create the ViewModel that the
		// bindings will operate on.
		function createViewModel(initialData, hasDataBinding, bindingState) {

			if(bindingState && bindingState.isSettingOnViewModel === true) {
				// If we are setting a value like `x:from="y"`,
				// we need to make a variable scope.
				newScope = tagData.scope.addLetContext(initialData);
				return newScope._context;

			} else {
				// If we are setting the ViewModel itself, we
				// stick the value in an observable: `this:from="value"`.
				return vm = new SimpleObservable(initialData);
			}
		}, undefined, true);

	if(!teardown) {
		// If no teardown, there's no bindings, no need to change the scope.
		return tagData;
	} else {
		// Copy `tagData` and overwrite the scope.
		return assign( assign({}, tagData), {
			teardown: teardown,
			scope: newScope || tagData.scope.add(vm)
		});
	}
}

// ### makeReplacementTagCallback
// Returns a `viewCallbacks.tag` function for `<can-slot>` or `<content>`.
// The `replacementTag` function:
// - gets the proper tagData
// - renders it the template
// - adds the rendered result to the page using nodeLists
//
// Arguments:
// - `tagName` - the tagName being created (`"can-slot"`).
// - `componentTagData` - the component's tagData, including its scope.
// - `shadowTagData` - the tagData where the element was found.
// - `leakScope` - how scope is being leaked.
// - `getPrimaryTemplate(el)` - a function to call to get the template to be rendered.
function makeReplacementTagCallback(tagName, componentTagData, shadowTagData, leakScope, getPrimaryTemplate) {

	var options = shadowTagData.options;

	// `replacementTag` is called when `<can-slot>` is found.
	// Arguments:
	// - `el` - the element
	// - `insertionElementTagData` - the tagData where the element was found.
	return function replacementTag(el, insertionElementTagData) {
		// If there's no template to be rendered, we'll render what's inside the
		// element. This is usually default content.
		var template = getPrimaryTemplate(el) || insertionElementTagData.subtemplate,
			// `true` if we are rendering something the user "passed" to this component.
			renderingLightContent = template !== insertionElementTagData.subtemplate;

		// If there's no template and no default content, we will do nothing. If
		// there is a template to render, lets render it!
		if (template) {

			// It's possible that rendering the contents of a `<can-slot>` will end up
			// rendering another `<can-slot>`.  We should make sure we can't render ourselves.
			delete options.tags[tagName];

			// First, lets figure out what we should be rendering
			// the template with.
			var tagData;

			// If we are rendering something the user passed.
			if( renderingLightContent ) {

				if(leakScope.toLightContent) {
					// We want to render with the same scope as the
					// `insertionElementTagData.scope`, but we don't want the
					// TemplateContext of the component's view included.
					tagData = addContext(el, {
						scope: insertionElementTagData.scope.cloneFromRef(),
						options: insertionElementTagData.options
					}, insertionElementTagData);
				}
				else {
					// render with the same scope the component was found within.
					tagData = addContext(el, componentTagData, insertionElementTagData);
				}
			} else {
				// We are rendering default content so this content should
				// use the same scope as the <content> tag was found within.
				tagData = addContext(el, insertionElementTagData, insertionElementTagData);
			}


			// Now we need to render the right template and insert its result in the page.
			// We need to teardown any bindings created too so we create a nodeList
			// to do this.
			var fragment = template(tagData.scope, tagData.options);
			if(tagData.teardown) {

				var placeholder = el.ownerDocument.createComment(tagName);
				fragment.insertBefore(placeholder, fragment.firstChild);
				domMutate.onNodeRemoved(placeholder, tagData.teardown);
			}

			el.parentNode.replaceChild(
				fragment,
				el
			);
			/*
			var nodeList = nodeLists.register([el], tagData.teardown || noop,
				insertionElementTagData.parentNodeList || true,
				insertionElementTagData.directlyNested);

			nodeList.expression = "<can-slot name='"+el.getAttribute('name')+"'/>";

			var frag = template(tagData.scope, tagData.options);


			var newNodes = canReflect.toArray( getChildNodes(frag) );
			var oldNodes = nodeLists.update(nodeList, newNodes);
			nodeLists.replace(oldNodes, frag);*/

			// Restore the proper tag function so it could potentially be used again (as in lists)
			options.tags[tagName] = replacementTag;
		}
	};
}
// ### getSetupFunctionForComponentVM
// This helper function is used to setup a Component when `new Component({viewModel})`
// is called.
// Arguments:
// - `componentInitVM` - The `viewModel` object used to initialize the actual viewModel.
// Returns: A component viewModel setup function.
function getSetupFunctionForComponentVM(componentInitVM) {


	return ObservationRecorder.ignore(function(el, componentTagData, makeViewModel, initialVMData) {

		var bindingContext = {
			element: el,
			scope: componentTagData.scope,
			parentNodeList: componentTagData.parentNodeList,
			viewModel: undefined
		};

		var bindingSettings = {};

		var bindings = [];

		// Loop through all viewModel props and create dataBindings.
		canReflect.eachKey(componentInitVM, function(parent, propName) {

			var canGetParentValue = parent != null && !!parent[getValueSymbol];
			var canSetParentValue = parent != null && !!parent[setValueSymbol];

			// If we can get or set the value, then we’ll create a binding
			if (canGetParentValue === true || canSetParentValue) {

				// Create an observable for reading/writing the viewModel
				// even though it doesn't exist yet.
				var child = stacheBindings.getObservableFrom.viewModel({
					name: propName,
				}, bindingContext, bindingSettings);

				// Create the binding similar to what’s in can-stache-bindings
				var canBinding = new Bind({
					child: child,
					parent: parent,
					queue: "dom",
					element: el,

					//!steal-remove-start
					// For debugging: the names that will be assigned to the updateChild
					// and updateParent functions within can-bind
					updateChildName: "update viewModel." + propName + " of <" + el.nodeName.toLowerCase() + ">",
					updateParentName: "update " + canReflect.getName(parent) + " of <" + el.nodeName.toLowerCase() + ">"
					//!steal-remove-end
				});

				bindings.push({
					binding: canBinding,
					siblingBindingData: {
						parent: {
							source: "scope",
							exports: canGetParentValue
						},
						child: {
							source: "viewModel",
							exports: canSetParentValue,
							name: propName
						}
					}
				});

			} else {
				// Can’t get or set the value, so assume it’s not an observable
				initialVMData[propName] = parent;
			}
		});

		// Initialize the viewModel.  Make sure you
		// save it so the observables can access it.
		var initializeData = stacheBindings.behaviors.initializeViewModel(bindings, initialVMData, function(properties){
			return bindingContext.viewModel = makeViewModel(properties);
		}, bindingContext);

		// Return a teardown function
		return function() {
			for (var attrName in initializeData.onTeardowns) {
				initializeData.onTeardowns[attrName]();
			}
		};
	});
}

var Component = Construct.extend(

	// ## Static
	{
		// ### setup
		//
		// When a component is extended, this sets up the component's internal constructor
		// functions and views for later fast initialization.
		// jshint maxdepth:6
		setup: function() {
			Construct.setup.apply(this, arguments);

			// When `Component.setup` function is ran for the first time, `Component` doesn't exist yet
			// which ensures that the following code is ran only in constructors that extend `Component`.
			if (Component) {
				var self = this;

				// Define a control using the `events` prototype property.
				if(this.prototype.events !== undefined && canReflect.size(this.prototype.events) !== 0) {
					this.Control = ComponentControl.extend(this.prototype.events);
				}

				//!steal-remove-start
				if (process.env.NODE_ENV !== 'production') {
					// If a constructor is assigned to the viewModel, give a warning
					if (this.prototype.viewModel && canReflect.isConstructorLike(this.prototype.viewModel)) {
						canDev.warn("can-component: Assigning a DefineMap or constructor type to the viewModel property may not be what you intended. Did you mean ViewModel instead? More info: https://canjs.com/doc/can-component.prototype.ViewModel.html");
					}
				}
				//!steal-remove-end

				// Look at viewModel, scope, and ViewModel properties and set one of:
				//  - this.viewModelHandler
				//  - this.ViewModel
				//  - this.viewModelInstance
				var protoViewModel = this.prototype.viewModel || this.prototype.scope;

				if(protoViewModel && this.prototype.ViewModel) {
					throw new Error("Cannot provide both a ViewModel and a viewModel property");
				}
				var vmName = string.capitalize( string.camelize(this.prototype.tag) )+"VM";
				if(this.prototype.ViewModel) {
					if(typeof this.prototype.ViewModel === "function") {
						this.ViewModel = this.prototype.ViewModel;
					} else {
						this.ViewModel = DefineMap.extend(vmName, {}, this.prototype.ViewModel);
					}
				} else {

					if(protoViewModel) {
						if(typeof protoViewModel === "function") {
							if(canReflect.isObservableLike(protoViewModel.prototype) && canReflect.isMapLike(protoViewModel.prototype)) {
								this.ViewModel = protoViewModel;
							} else {
								this.viewModelHandler = protoViewModel;
							}
						} else {
							if(canReflect.isObservableLike(protoViewModel) && canReflect.isMapLike(protoViewModel)) {
								//!steal-remove-start
								if (process.env.NODE_ENV !== 'production') {
									canLog.warn("can-component: "+this.prototype.tag+" is sharing a single map across all component instances");
								}
								//!steal-remove-end
								this.viewModelInstance = protoViewModel;
							} else {
								canLog.warn("can-component: "+this.prototype.tag+" is extending the viewModel into a can-simple-map");
								this.ViewModel = SimpleMap.extend(vmName,{},protoViewModel);
							}
						}
					} else {
						this.ViewModel = SimpleMap.extend(vmName,{},{});
					}
				}

				// Convert the template into a renderer function.
				if (this.prototype.template) {
					//!steal-remove-start
					if (process.env.NODE_ENV !== 'production') {
						canLog.warn('can-component.prototype.template: is deprecated and will be removed in a future release. Use can-component.prototype.view');
					}
					//!steal-remove-end
					this.view = this.prototype.template;
				}
				if (this.prototype.view) {
					this.view = this.prototype.view;
				}
				
				var viewName;
				if(this.view !== undefined && typeof this.view !== "function"){
					viewName = string.capitalize( string.camelize(this.prototype.tag) )+"View";
					this.view = stache(viewName, this.view || "");
				}

				var renderComponent = function(el, tagData) {
					// Check if a symbol already exists on the element; if it does, then
					// a new instance of the component has already been created
					if (el[createdByCanComponentSymbol] === undefined) {
						new self(el, tagData);
					}
				};

				//!steal-remove-start
				if (process.env.NODE_ENV !== 'production') {
					Object.defineProperty(renderComponent, "name",{
						value: "render <"+this.prototype.tag+">",
						configurable: true
					});
					renderComponent = queues.runAsTask(renderComponent, function(el, tagData) {
						return ["Rendering", el, "with",tagData.scope];
					});
				}
				//!steal-remove-end

				// Register this component to be created when its `tag` is found.
				viewCallbacks.tag(this.prototype.tag, renderComponent);
			}
		}
	}, {
		// ## Prototype
		// ### setup
		// When a new component instance is created, setup bindings, render the view, etc.
		setup: function(el, componentTagData) {
			// Save arguments so if this component gets re-inserted,
			// we can setup again.
			this._initialArgs = [el,componentTagData];

			var component = this;

			var options = {
				helpers: {},
				tags: {}
			};

			// #### Clean up arguments

			// If componentTagData isn’t defined, check for el and use it if it’s defined;
			// otherwise, an empty object is needed for componentTagData.
			if (componentTagData === undefined) {
				if (el === undefined) {
					componentTagData = {};
				} else {
					componentTagData = el;
					el = undefined;
				}
			}

			// Create an element if it doesn’t exist and make it available outside of this
			if (el === undefined) {
				el = DOCUMENT().createElement(this.tag);
				el[createdByCanComponentSymbol] = true;
			}
			this.element = el;

			if(componentTagData.initializeBindings === false && !this._skippedSetup) {
				// Temporary, will be overridden.
				this._skippedSetup = this._torndown = true;
				this.viewModel = Object.create(null);
				return;
			}

			var componentContent = componentTagData.content;
			if (componentContent !== undefined) {
				// Check if it’s already a renderer function or
				// a string that needs to be parsed by stache
				if (typeof componentContent === "function") {
					componentTagData.subtemplate = componentContent;
				} else if (typeof componentContent === "string") {
					componentTagData.subtemplate = stache(componentContent);
				}
			}

			var componentScope = componentTagData.scope;
			if (componentScope !== undefined &&
				componentScope instanceof Scope === false
			) {
				if(canReflect.isScopeLike(componentScope)) {
					// replace foreign scope-like with native scope to ensure consistent behavior.
					componentTagData.scope = new Scope(componentScope._context, componentScope._parent, componentScope._meta);
				} else {
					componentTagData.scope = new Scope(componentScope);
				}
			}

			// Hook up any templates with which the component was instantiated
			var componentTemplates = componentTagData.templates;
			if (componentTemplates !== undefined) {
				canReflect.eachKey(componentTemplates, function(template, name) {
					// Check if it’s a string that needs to be parsed by stache
					if (typeof template === "string") {
						var debugName = name + " template";
						componentTemplates[name] = stache(debugName, template);
					}
				});
			}

			// #### Setup ViewModel
			var viewModel;
			var initialViewModelData = {};

			var preventDataBindings = domData.get(el, "preventDataBindings");

			var teardownBindings;
			if (preventDataBindings) {
				viewModel = el[viewModelSymbol];
			} else {
				// Set up the bindings
				var setupFn;
				if (componentTagData.setupBindings) {
					setupFn = function(el, componentTagData, callback, initialViewModelData){
						return componentTagData.setupBindings(el, callback, initialViewModelData);
					};
				} else if (componentTagData.viewModel) {
					// Component is being instantiated with a viewModel
					setupFn = getSetupFunctionForComponentVM(componentTagData.viewModel);

					//!steal-remove-start
					if (process.env.NODE_ENV !== 'production') {
						setupFn = queues.runAsTask(setupFn, function(el, componentTagData) {
							return ["Constructing", el, "with viewModel",componentTagData.viewModel];
						});
					}
					//!steal-remove-end
				} else {
					setupFn = stacheBindings.behaviors.viewModel;
				}


				teardownBindings = setupFn(el, componentTagData, function(initialViewModelData) {

					var ViewModel = component.constructor.ViewModel,
						viewModelHandler = component.constructor.viewModelHandler,
						viewModelInstance = component.constructor.viewModelInstance;

					if(viewModelHandler) {
						var scopeResult = viewModelHandler.call(component, initialViewModelData, componentTagData.scope, el);
						if (canReflect.isObservableLike(scopeResult) && canReflect.isMapLike(scopeResult) ) {
							// If the function returns a can.Map, use that as the viewModel
							viewModelInstance = scopeResult;
						} else if (canReflect.isObservableLike(scopeResult.prototype) && canReflect.isMapLike(scopeResult.prototype)) {
							// If `scopeResult` is of a `can.Map` type, use it to wrap the `initialViewModelData`
							ViewModel = scopeResult;
						} else {
							// Otherwise extend `SimpleMap` with the `scopeResult` and initialize it with the `initialViewModelData`
							ViewModel = SimpleMap.extend(scopeResult);
						}
					}

					if(ViewModel) {
						viewModelInstance = new ViewModel(initialViewModelData);
					}
					viewModel = viewModelInstance;
					return viewModelInstance;
				}, initialViewModelData);
			}

			// Set `viewModel` to `this.viewModel` and set it to the element's `data` object as a `viewModel` property
			this.viewModel = viewModel;
			el[viewModelSymbol] = viewModel;
			el.viewModel = viewModel;
			domData.set(el, "preventDataBindings", true);

			// TEARDOWN SETUP
			var removedDisposal,
				connectedDisposal,
				viewModelDisconnectedCallback;
			function teardownComponent(){
				if(removedDisposal) {
					removedDisposal();
					removedDisposal = null;
				}
				component._torndown = true;
				domEvents.dispatch(el, "beforeremove", false);
				if(teardownBindings) {
					teardownBindings();
				}
				if(viewModelDisconnectedCallback) {
					viewModelDisconnectedCallback(el);
				} else if(typeof viewModel.stopListening === "function"){
					viewModel.stopListening();
				}
				if(connectedDisposal) {
					connectedDisposal();
					connectedDisposal = null;
				}
			}

			// #### Helpers
			// TODO: remove in next release
			// Setup helpers to callback with `this` as the component
			if(this.helpers !== undefined) {
				canReflect.eachKey(this.helpers, function(val, prop) {
					if (typeof val === "function") {
						options.helpers[prop] = val.bind(viewModel);
					}
				});
			}

			// #### `events` control
			// TODO: remove in next release
			// Create a control to listen to events
			if(this.constructor.Control) {
				this._control = new this.constructor.Control(el, {
					// Pass the viewModel to the control so we can listen to it's changes from the controller.
					scope: this.viewModel,
					viewModel: this.viewModel
				});
			}

			removedDisposal = domMutate.onNodeRemoved(el, function () {
				var doc = el.ownerDocument;
				var rootNode = doc.contains ? doc : doc.documentElement;
				if (!rootNode || !rootNode.contains(el)) {
					teardownComponent();
				}
			});

			// #### Rendering

			var leakScope = {
				toLightContent: this.leakScope === true,
				intoShadowContent: this.leakScope === true
			};

			var hasShadowView = !!(this.constructor.view);
			var shadowFragment;

			// Get what we should render between the component tags
			// and the data for it.
			var betweenTagsView;
			var betweenTagsTagData;
			if( hasShadowView ) {
				var shadowTagData;
				if (leakScope.intoShadowContent) {
					// Give access to the component's data and the VM
					shadowTagData = {
						scope: componentTagData.scope.add(this.viewModel, { viewModel: true }),
						options: options
					};

				} else { // lexical
					// only give access to the VM
					shadowTagData = {
						scope: new Scope(this.viewModel, null, { viewModel: true }),
						options: options
					};
				}

				// Add a hookup for each <can-slot>
				options.tags['can-slot'] = makeReplacementTagCallback('can-slot', componentTagData, shadowTagData, leakScope, function(el) {
					var templates = componentTagData.templates;
					if (templates) {// This is undefined if the component is <self-closing/>
						return templates[el.getAttribute("name")];
					}
				});

				// Add a hookup for <content>
				options.tags.content = makeReplacementTagCallback('content',  componentTagData, shadowTagData, leakScope, function() {
					return componentTagData.subtemplate;
				});

				betweenTagsView = this.constructor.view;
				betweenTagsTagData = shadowTagData;
			}
			else {
				// No shadow template.
				// Render light template with viewModel on top
				var lightTemplateTagData = {
					scope: componentTagData.scope.add(this.viewModel, {
						viewModel: true
					}),
					options: options
				};
				betweenTagsTagData = lightTemplateTagData;
				betweenTagsView = componentTagData.subtemplate || el.ownerDocument.createDocumentFragment.bind(el.ownerDocument);
			}




			// Keep a nodeList so we can kill any directly nested nodeLists within this component



			shadowFragment = betweenTagsView(betweenTagsTagData.scope, betweenTagsTagData.options);

			// TODO: afterRender

			// Append the resulting document fragment to the element
			domMutateNode.appendChild.call(el, shadowFragment);

			// Call connectedCallback
			if(viewModel && viewModel.connectedCallback) {
				var body = DOCUMENT().body;
				var componentInPage = body && body.contains(el);

				if(componentInPage) {
					viewModelDisconnectedCallback = viewModel.connectedCallback(el);
				} else {
					connectedDisposal = domMutate.onNodeConnected(el, function () {
						connectedDisposal();
						connectedDisposal = null;
						viewModelDisconnectedCallback = viewModel.connectedCallback(el);
					});
				}

			}
			component._torndown = false;
		}
	});

// This adds support for components being rendered as values in stache templates
Component.prototype[viewInsertSymbol] = function(viewData) {
	if(this._torndown) {
		this.setup.apply(this,this._initialArgs);
	}
	return this.element;
};

module.exports = namespace.Component = Component;