/**
 * Silex, live web creation
 * http://projects.silexlabs.org/?/silex/
 *
 * Copyright (c) 2012 Silex Labs
 * http://www.silexlabs.org/
 *
 * Silex is available under the GPL license
 * http://www.silexlabs.org/silex/silex-licensing/
 */

/**
 * @fileoverview
 *   This class is used to manage Silex elements
 *   It has methods to manipulate the DOM elements
 *      created by new silex.model.Element().createElement
 */

import { Constants } from '../../Constants';
import { Model, View } from '../types';
import { Dom } from '../utils/dom';
import { Style } from '../utils/style';
import { Url } from '../utils/url';
import { getUiElements } from '../view/UiElements';
import { StyleData, TemplateName } from './Data';
import { Property } from './property';

/**
 * direction in the dom
 */
export class DomDirection {
  static UP = 'UP';
  static DOWN = 'DOWN';
  static TOP = 'TOP';
  static BOTTOM = 'BOTTOM';
}

/**
 * @param model  model class which holds the other models
 * @param view  view class which holds the other views
 */
export class SilexElement {
  /**
   * constant for default size of an element
   */
  static INITIAL_ELEMENT_SIZE = 100;

  static async loadImage(url: string): Promise<HTMLImageElement> {
    return new Promise<HTMLImageElement>((resolve, reject) => {
      const img = new Image();
      img.onload = (e) => {
        img.onload = null;
        img.onerror = null;
        resolve(img);
      };
      img.onerror = (e: Event) => {
        img.onload = null;
        img.onerror = null;
        reject(e);
      };
      img.src = url;
    });
  }

  constructor(public model: Model, public view: View) {}

  /**
   * get num tabs
   * example: getTabs(2) returns '        '
   */
  getTabs(num: number): string {
    let tabs = '';
    for (let n = 0; n < num; n++) {
      tabs += '    ';
    }
    return tabs;
  }

  /**
   * for properties or style which are to be applied to elements
   * but in the case of a section not to the internal container, only the whole
   * section this method will return the element or the section when the element
   * is a section container
   */
  noSectionContent(element) {
    if (this.isSectionContent(element)) {
      return (element.parentElement as HTMLElement);
    }
    return element;
  }

  /**
   * get/set type of the element
   * @param element   created by silex, either a text box, image, ...
   * @return           the type of element
   * example: for a container this will return "container"
   */
  getType(element: HTMLElement): string {
    return element.getAttribute(Constants.TYPE_ATTR);
  }

  /**
   * @param element   created by silex
   * @return true if `element` is a an element's content (the element in an
   *     image, html box, section...)
   */
  isElementContent(element: HTMLElement): boolean {
    return element.classList.contains(Constants.ELEMENT_CONTENT_CLASS_NAME);
  }

  /**
   * @param element   created by silex
   * @return true if `element` is a section
   */
  isSection(element: HTMLElement): boolean {
    // FIXME: this is a workaround, it happens in mobile editor, when
    // dragg/dropping (element is document)
    if (!element || !element.classList) {
      return false;
    }
    return element.classList.contains(Constants.TYPE_SECTION);
  }

  /**
   * @param element   created by silex
   * @return true if `element` is the content container of a section
   */
  isSectionContent(element: HTMLElement): boolean {
    // FIXME: this is a workaround, it happens in mobile editor, when
    // dragg/dropping (element is document)
    if (!element || !element.classList) {
      return false;
    }
    return element.classList.contains(Constants.TYPE_CONTAINER_CONTENT);
  }

  /**
   * get/set the "hide on mobile" property
   * @return true if the element is hidden on mobile
   */
  getHideOnMobile(element: HTMLElement): boolean {
    // FIXME: this is a workaround, it happens in mobile editor, when
    // dragg/dropping (element is document)
    if (!element || !element.classList) {
      return false;
    }
    return this.noSectionContent(element).classList.contains(Constants.HIDE_ON_MOBILE);
  }

  /**
   * get/set the "hide on mobile" property
   * @param hide, true if the element has to be hidden on mobile
   */
  setHideOnMobile(element: HTMLElement, hide: boolean) {
    if (hide) {
      this.noSectionContent(element).classList.add(Constants.HIDE_ON_MOBILE);
    } else {
      this.noSectionContent(element).classList.remove(Constants.HIDE_ON_MOBILE);
    }
  }

  /**
   * get/set the "hide on desktop" property
   * @return true if the element is hidden on desktop
   */
  getHideOnDesktop(element: HTMLElement): boolean {
    if (!element || !element.classList) {
      return false;
    }

    // FIXME: this is a workaround, it happens in mobile editor, when
    // dragg/dropping (element is document)
    return this.noSectionContent(element).classList.contains(
        Constants.HIDE_ON_DESKTOP);
  }

  /**
   * get/set the "hide on desktop" property
   * @param hide, true if the element has to be hidden on desktop
   */
  setHideOnDesktop(element: HTMLElement, hide: boolean) {
    if (hide) {
      this.noSectionContent(element).classList.add(Constants.HIDE_ON_DESKTOP);
    } else {
      this.noSectionContent(element).classList.remove(Constants.HIDE_ON_DESKTOP);
    }
  }

  /**
   * get all the element's styles
   * @param element   created by silex, either a text box, image, ...
   * @return           the styles of the element
   */
  getAllStyles(element: HTMLElement): string {
    const styleObject = this.model.property.getStyle(element);
    const styleStr = Style.styleToString(styleObject);
    return styleStr;
  }

  /**
   * get/set style of the element
   * @param element   created by silex, either a text box, image, ...
   * @param styleName  the style name
   * @return           the style of the element
   */
  getStyle(element: HTMLElement, cssName: string): string {
    const isMobile = this.view.workspace.getMobileEditor();
    let styleObject = this.model.property.getStyle(element, isMobile);
    if (styleObject && styleObject[cssName]) {
      return styleObject[cssName];
    } else {
      if (isMobile) {
        // get the non mobile style if it is not defined in mobile
        styleObject = this.model.property.getStyle(element, false);
        if (styleObject && styleObject[cssName]) {
          return styleObject[cssName];
        }
      }
    }
    return null;
  }

  /**
   * get/set style of element from a container created by silex
   * @param element            created by silex, either a text box, image, ...
   * @param  styleName          the style name, camel case, not css with dashes
   * @param  opt_styleValue     the value for this styleName
   * @param  opt_preserveJustAdded     if true, do not remove the "just added"
   *     css class, default is false
   */
  setStyle(element: HTMLElement, styleName: string, opt_styleValue?: string, opt_preserveJustAdded?: boolean) {
    // retrieve style
    let styleObject = this.model.property.getStyle(element);
    if (!styleObject) {
      styleObject = {};
    }

    // apply the new style
    if (styleObject[styleName] !== opt_styleValue) {
      if (opt_styleValue != null ) {
        styleObject[styleName] = opt_styleValue;
      } else {
        styleObject[styleName] = '';
      }
      this.model.property.setStyle(element, styleObject);
    }
  }

  /**
   * get/set a property of an element from a container created by silex
   * @param element            created by silex, either a text box, image, ...
   * @param  propertyName          the property name
   * @param  opt_propertyValue     the value for this propertyName
   * @param  opt_applyToContent    apply to the element or to its
   *     ".silex-element-content" element
   * example: element.setProperty(imgElement, 'style', 'top: 5px; left: 30px;')
   */
  setProperty(
      element: HTMLElement, propertyName: string,
      opt_propertyValue?: string, opt_applyToContent?: boolean) {
    if (opt_applyToContent) {
      element = this.getContentNode(element);
    }
    if (opt_propertyValue != null ) {
      element.setAttribute(propertyName, (opt_propertyValue as string));
    } else {
      element.removeAttribute(propertyName);
    }
  }

  /**
   * @param url    URL of the image chosen by the user
   */
  setBgImage(element: HTMLElement, url: string) {
    if (url) {
      this.setStyle(element, 'background-image', Url.addUrlKeyword(url));
    } else {
      this.setStyle(element, 'background-image');
    }

    // redraw tools
    this.model.body.refreshViews();
  }

  /**
   * get/set html from a container created by silex
   * @param element  created by silex, either a text box, image, ...
   * @return  the html content
   */
  getInnerHtml(element: HTMLElement): string {
    let innerHTML = this.getContentNode(element).innerHTML;

    // put back executable scripts
    innerHTML = Dom.reactivateScripts(innerHTML);
    return innerHTML;
  }

  /**
   * get/set element from a container created by silex
   * @param element  created by silex, either a text box, image, ...
   * @param innerHTML the html content
   */
  setInnerHtml(element: HTMLElement, innerHTML: string) {
    // get the container of the html content of the element
    const contentNode = this.getContentNode(element);

    // deactivate executable scripts
    innerHTML = Dom.deactivateScripts(innerHTML);

    // set html
    contentNode.innerHTML = innerHTML;
  }

  /**
   * get/set element from a container created by silex
   * @param element  created by silex, either a text box, image, ...
   * @return  the element which holds the content, i.e. a div, an image, ...
   */
  getContentNode(element: HTMLElement): HTMLElement {
    const content: HTMLElement = element.querySelector(':scope > .' + Constants.ELEMENT_CONTENT_CLASS_NAME);
    if (!content && [Constants.TYPE_IMAGE, Constants.TYPE_HTML, Constants.TYPE_SECTION].indexOf(this.getType(element)) > -1) {
      console.warn('This element is supposed to have a content container', element);
    }
    return content || element;
  }

  /**
   * move the element up/down the DOM
   */
  move(element: HTMLElement, direction: DomDirection) {
    // do not move a section's container content, but the section itself
    element = this.noSectionContent(element);
    switch (direction) {
      case DomDirection.UP:
        const nextSibling = this.getNextElement(element, true);
        if (nextSibling) {
          // insert after
          element.parentElement.insertBefore(nextSibling, element);
        }
        break;
      case DomDirection.DOWN:
        const prevSibling = this.getNextElement(element, false);
        if (prevSibling) {
          // insert before
          element.parentElement.insertBefore(prevSibling, element.nextSibling);
        }
        break;
      case DomDirection.TOP:
        element.parentElement.appendChild(element);
        break;
      case DomDirection.BOTTOM:
        element.parentElement.insertBefore(
            element, element.parentElement.childNodes[0]);
        break;
    }
    this.view.stageWrapper.redraw();
  }

  /**
   * get the previous or next element in the DOM, which is a Silex element
   * @param forward if true look for the next element, if false for the previous
   */
  getNextElement(element: HTMLElement, forward: boolean): HTMLElement {
    let node = (element as Node);
    while (node = forward ? node.nextSibling : node.previousSibling) {
      if (node.nodeType === 1) {
        const el = (node as HTMLElement);

        // candidates are the elements which are visible in the current page, or
        // visible everywhere (not paged)
        if (this.getType(el) != null  &&
            (this.model.page.isInPage(el) ||
             this.model.page.getPagesForElement(el).length === 0)) {
          return el;
        }
      }
    }
    return null;
  }

  /**
   * set/get the image URL of an image element
   * @param element  container created by silex which contains an image
   * @return  the url of the image
   */
  getImageUrl(element: HTMLElement): string {
    let url = '';
    if (element.getAttribute(Constants.TYPE_ATTR) === Constants.TYPE_IMAGE) {
      // get the image tag
      const img = this.getContentNode(element);
      if (img) {
        url = img.getAttribute('src');
      } else {
        console.error(
            'The image could not be retrieved from the element.', element);
      }
    } else {
      console.error('The element is not an image.', element);
    }
    return url;
  }

  /**
   * set/get the image URL of an image element
   * @param element  container created by silex which contains an image
   * @param url  the url of the image
   * @param opt_callback the callback to be notified when the image is loaded
   * @param opt_errorCallback the callback to be notified of errors
   */
  async setImageUrl(
      element: HTMLElement, url: string,
      opt_callback?: ((p1: HTMLElement, p2: HTMLElement) => void),
      opt_errorCallback?: ((p1: HTMLElement, p2: string) => void)) {
    if (element.getAttribute(Constants.TYPE_ATTR) === Constants.TYPE_IMAGE) {
      // get the image tag
      const img = this.getContentNode(element) as HTMLImageElement;
      if (img) {
        // add loading asset
        element.classList.add(Constants.LOADING_ELEMENT_CSS_CLASS);

        // remove previous img tag
        const imgTags = Array.from(element.querySelectorAll('img.' + Constants.ELEMENT_CONTENT_CLASS_NAME));
        imgTags.forEach((imgTag: HTMLImageElement) => {
          imgTag.parentElement.removeChild(imgTag);
        });

        try {
          // load the new image
          const loadedImg: HTMLImageElement = await SilexElement.loadImage(url);

          // update element size
          this.setStyle(element, 'width', loadedImg.naturalWidth + 'px', true);
          this.setStyle(element, this.getHeightStyleName(element), loadedImg.naturalHeight + 'px', true);

          // callback
          if (opt_callback) {
            opt_callback(element, loadedImg);
          }

          // add the image to the element
          element.appendChild(loadedImg);

          // add a marker to find the inner content afterwards, with
          // getContent
          loadedImg.classList.add(Constants.ELEMENT_CONTENT_CLASS_NAME);

          // remove loading asset
          element.classList.remove(Constants.LOADING_ELEMENT_CSS_CLASS);

          // redraw tools
          this.model.body.refreshViews();
          this.view.stageWrapper.redraw();
        } catch (e) {
          console.error('An error occured while loading the image.', element, e);

          // callback
          if (opt_errorCallback) {
            opt_errorCallback(element, 'An error occured while loading the image.');
          }
        }
      } else {
        console.error(
            'The image could not be retrieved from the element.', element);
        if (opt_errorCallback) {
          opt_errorCallback(
              element, 'The image could not be retrieved from the element.');
        }
      }
    } else {
      console.error('The element is not an image.', element);
      if (opt_errorCallback) {
        opt_errorCallback(element, 'The element is not an image.');
      }
    }
  }

  /**
   * remove a DOM element and its styles as well as its children's
   * @param element   the element to remove
   */
  removeElement(rootElement: HTMLElement) {
    // never delete sections container content, but the section itself
    rootElement = this.noSectionContent(rootElement);

    const children = Array.from(rootElement.querySelectorAll(`.${Constants.EDITABLE_CLASS_NAME}`));

    children.concat([rootElement])
    .forEach((element: HTMLElement) => {
      // check this is allowed, i.e. an element inside the stage container
      if (this.model.body.getBodyElement() !== element && !!element.parentElement) {
        // remove style and component data
        this.model.property.setElementComponentData(element);
        this.model.property.setStyle(element, null, true);
        this.model.property.setStyle(element, null, false);

        // remove the element
        element.parentElement.removeChild(element);

        // update stage store
        this.view.stageWrapper.removeElement(element);
      } else {
        // could not delete element because it is not in the stage element
        // this happens when you select an element and its children and delete them all
      }
    });
    // update all metrics in case this new element has moved other elements
    this.view.stageWrapper.redraw();
  }

  /**
   * append an element to the stage
   * handles undo/redo
   */
  addElement(container: HTMLElement, element: HTMLElement, opt_offset: number = 0) {
    // for sections, force body
    if (this.isSection(element) && container !== this.model.body.getBodyElement()) {
      // container = this.model.body.getBodyElement();
      throw new Error('Cannot add a section to other than the body');
    }
    if (opt_offset > 0) {
      const styleObject = this.model.property.getStyle(element, false);
      // styleObject.top = (parseInt(styleObject.top) + opt_offset) + 'px';
      styleObject.left = (parseInt(styleObject.left) + opt_offset) + 'px';
      this.model.property.setStyle(element, styleObject, false);
    }
    container.appendChild(element);

    // resize the body
    // call the method defined in front-end.js
    // this will resize the body according to its content
    // it will also trigger a "silex.resize" event
    // tslint:disable:no-string-literal
    this.model.file.getContentWindow()['silex'].resizeBody();

    // update stage store
    this.view.stageWrapper.addElement(element);

    // update all metrics in case this new element has moved other elements
    this.view.stageWrapper.redraw();
  }

  /**
   * add an element at the center of the stage
   * and move it into the container beneeth it
   * @param element    the element to add
   * @param opt_offset an offset to apply to its position (x and y)
   */
  addElementDefaultPosition(element: HTMLElement, opt_offset: number = 0) {
    // find the container (main background container or the stage)
    const stageSize = getUiElements().stage.getBoundingClientRect();
    const bb = this.model.property.getBoundingBox([element]);
    const posX = Math.round((stageSize.width / 2) - (bb.width / 2));
    const posY = Math.round((stageSize.height / 2) - (bb.height / 2));
    const container = this.view.stageWrapper.getDropZone(posX, posY, element) || this.model.body.getBodyElement();
    // take the scroll into account (drop at (100, 100) from top left corner of the window, not the stage)
    const bbContainer = container.getBoundingClientRect();
    const offsetX = Math.round((bbContainer.width / 2) - (bb.width / 2));
    const offsetY = Math.round((bbContainer.height / 2) - (bb.height / 2));

    // add to stage
    this.addElement(container, element);

    // apply the style (force desktop style, not mobile)
    const styleObject = this.model.property.getStyle(element, false);
    styleObject.top = opt_offset + offsetY + 'px';
    styleObject.left = opt_offset + offsetX + 'px';
    this.model.property.setStyle(element, styleObject, false);
  }

  /**
   * init the element depending on its type
   */
  initElement(element: HTMLElement) {
    // default style
    const defaultStyle: any = {};
    defaultStyle.width = SilexElement.INITIAL_ELEMENT_SIZE + 'px';
    defaultStyle[this.getHeightStyleName(element)] = SilexElement.INITIAL_ELEMENT_SIZE + 'px';

    // init the element depending on its type
    switch (this.getType(element)) {
      case Constants.TYPE_CONTAINER:
      case Constants.TYPE_HTML:
        if (!this.isSection(element)) {
          defaultStyle['background-color'] = 'rgb(255, 255, 255)';
        }
        break;
      case Constants.TYPE_TEXT:
      case Constants.TYPE_IMAGE:
        break;
    }

    // special case of section content
    if (this.isSectionContent(element)) {
      // no bg color for the content container
      defaultStyle['background-color'] = '';

      // no width either, it will take the .website-width
      // the default one from front-end.css or the one in the settings
      defaultStyle.width = '';
    }

    // default style to the element style
    // keep the style if there is one, usually set by component::initComponent
    const finalStyle = this.model.property.getStyle(element, false) || {};
    for (const name in defaultStyle) {
      finalStyle[name] = finalStyle[name] || defaultStyle[name];
    }

    // apply the style (force desktop style, not mobile)
    this.model.property.setStyle(element, finalStyle, false);

    // add the element to the stage
    if (this.isSection(element)) {
      this.addElement(this.model.body.getBodyElement(), element);
    } else if (this.isSectionContent(element)) {
      this.addElement(element.parentElement, element);
    } else {
      if (!this.isElementContent(element)) {
        // add to the stage at the right position
        // and in the right container
        this.addElementDefaultPosition(element);
      }
    }

    // send the scroll to the target
    setTimeout(() => {
      this.view.stageWrapper.center([element]);
    }, 0);
  }

  /**
   * element creation
   * create a DOM element, attach it to this container
   * and returns a new component for the element
   * @param type  the type of the element to create,
   *    see TYPE_* constants of the class @see silex.model.Element
   * @return   the newly created element
   */
  createElement(type: string): HTMLElement {
    // create the element
    let element = null;
    switch (type) {
      // container
      case Constants.TYPE_CONTAINER:
        element = this.createContainerElement();
        break;

      // section
      case Constants.TYPE_SECTION:
        element = this.createSectionElement();
        break;

      // text
      case Constants.TYPE_TEXT:
        element = this.createTextElement();
        break;

      // HTML box
      case Constants.TYPE_HTML:
        element = this.createHtmlElement();
        break;

      // Image
      case Constants.TYPE_IMAGE:
        element = this.createImageElement();
        break;
    }
    // init the element
    element.classList.add(Constants.EDITABLE_CLASS_NAME);
    this.model.property.initSilexId(element, this.model.file.getContentDocument());

    // add css class for Silex styles
    element.classList.add(type);

    // return the element
    return element;
  }

  /**
   * element creation method for a given type
   * called from createElement
   */
  createContainerElement(): HTMLElement {
    // create the conatiner
    const element = this.model.file.getContentDocument().createElement('div');
    element.setAttribute(Constants.TYPE_ATTR, Constants.TYPE_CONTAINER);
    return element;
  }

  createElementWithContent(className: string): HTMLElement {
    // create the element
    const element = this.model.file.getContentDocument().createElement('div');
    element.setAttribute(Constants.TYPE_ATTR, className);

    // create the container for text content
    const content = this.model.file.getContentDocument().createElement('div');

    // add empty content
    element.appendChild(content);

    // add a marker to find the inner content afterwards, with getContent
    content.classList.add(Constants.ELEMENT_CONTENT_CLASS_NAME);

    // done
    return element;
  }

  /**
   * element creation method for a given type
   * called from createElement
   */
  createSectionElement(): HTMLElement {
    // create the element
    const element = this.model.file.getContentDocument().createElement('div');
    element.setAttribute(Constants.TYPE_ATTR, Constants.TYPE_CONTAINER);
    element.classList.add(Constants.PREVENT_DRAGGABLE_CLASS_NAME);
    element.classList.add(Constants.PREVENT_RESIZABLE_CLASS_NAME);
    element.classList.add(Constants.TYPE_CONTAINER);

    // content element is both a container and a content element
    const content = this.createElement(Constants.TYPE_CONTAINER);
    content.classList.add(Constants.ELEMENT_CONTENT_CLASS_NAME);
    content.classList.add(Constants.TYPE_CONTAINER_CONTENT);
    content.classList.add(Constants.WEBSITE_WIDTH_CLASS_NAME);
    content.classList.add(Constants.PREVENT_DRAGGABLE_CLASS_NAME);
    element.appendChild(content);
    this.initElement(content);

    // done
    return element;
  }

  /**
   * element creation method for a given type
   * called from createElement
   */
  createTextElement(): HTMLElement {
    // create the element
    const element = this.createElementWithContent(Constants.TYPE_TEXT);

    // add default content
    const content = this.getContentNode(element);
    content.innerHTML = '<p>New text box</p>';

    // add normal class for default text formatting
    // sometimes there is only in text node in content
    // e.g. whe select all + remove formatting
    content.classList.add('normal');

    // done
    return element;
  }

  /**
   * element creation method for a given type
   * called from createElement
   */
  createHtmlElement(): HTMLElement {
    // create the element
    const element = this.model.file.getContentDocument().createElement('div');
    element.setAttribute(Constants.TYPE_ATTR, Constants.TYPE_HTML);

    // create the container for html content
    const htmlContent = this.model.file.getContentDocument().createElement('div');
    htmlContent.innerHTML = '<p>New HTML box</p>';
    element.appendChild(htmlContent);

    // add a marker to find the inner content afterwards, with getContent
    htmlContent.classList.add(Constants.ELEMENT_CONTENT_CLASS_NAME);
    return element;
  }

  /**
   * element creation method for a given type
   * called from createElement
   */
  createImageElement(): HTMLElement {
    // create the element
    const element = this.model.file.getContentDocument().createElement('div');
    element.setAttribute(Constants.TYPE_ATTR, Constants.TYPE_IMAGE);
    return element;
  }

  /**
   * set/get a "silex style link" on an element
   * @param opt_link an URL
   *         or an internal link (beginning with #!)
   *         or null to remove the link
   */
  setLink(element: HTMLElement, opt_link?: string) {
    if (opt_link) {
      element.setAttribute(Constants.LINK_ATTR, opt_link);
    } else {
      element.removeAttribute(Constants.LINK_ATTR);
    }
  }

  /**
   * set/get a "silex style link" on an element
   */
  getLink(element: HTMLElement): string {
    return element.getAttribute(Constants.LINK_ATTR);
  }

  /**
   * get a name to display for this type
   */
  getDisplayName(element: HTMLElement): string {
    if (this.isSectionContent(element)) {
      return 'Section Container';
    } else if (this.isSection(element)) {
      return 'Section';
    }
    const silexType = this.getType(element);
    switch (silexType) {
      case Constants.TYPE_TEXT: return 'Text';
      case Constants.TYPE_IMAGE: return 'Image';
      case Constants.TYPE_CONTAINER: return 'Container';
      case Constants.TYPE_HTML: return 'Html';
      // case Constants.TYPE_CONTAINER_CONTENT: return 'Container';
      // case Constants.TYPE_SECTION: return 'Section';
      default: return silexType;
    }
  }

  /**
   * get/set class name of the element of a container created by silex
   * remove all silex internal classes
   * @param element   created by silex, either a text box, image, ...
   * @return           the value for this styleName
   */
  getClassName(element: HTMLElement): string {
    const pages = this.model.page.getPages();
    let componentCssClasses = [];
    if (this.model.component.isComponent(element)) {
      const templateName =
          (this.model.property.getElementComponentData(
               element).templateName as TemplateName);
      componentCssClasses = this.model.component.getCssClasses(templateName);
    }
    return element.className.split(' ')
    .filter((name) => {
      if (name === '' ||
          Constants.SILEX_CLASS_NAMES.indexOf(name) > -1 ||
          pages.indexOf(name) > -1 ||
          componentCssClasses.indexOf(name) > -1 ||
          this.model.property.getSilexId(element) === name) {
        return false;
      }
      return true;
    })
    .join(' ');
  }

  /**
   * get/set class name of the element of a container created by silex
   * remove all silex internal classes
   * @param element   created by silex, either a text box, image, ...
   * @param opt_className  the class names, or null to reset
   */
  setClassName(element: HTMLElement, opt_className?: string) {
    // compute class names to keep, no matter what
    // i.e. the one which are in element.className + in Silex internal classes
    const pages = this.model.page.getPages();
    const classNamesToKeep =
        element.className.split(' ').map((name) => {
          if (Constants.SILEX_CLASS_NAMES.indexOf(name) > -1 ||
              pages.indexOf(name) > -1 ||
              this.model.property.getSilexId(element) === name) {
            return name;
          }
        });

    // reset element class name
    element.className = classNamesToKeep.join(' ');
    if (opt_className) {
      // apply classes from opt_className
      opt_className.split(' ').forEach((name) => {
        name = name.trim();
        if (name && name !== '') {
          element.classList.add(name);
        }
      });
    }
  }

  /**
   * get the name of the style to be used to set the height of the element
   * returns 'height' or 'minHeight' depending on the element type
   * @return 'height' or 'minHeight' depending on the element type
   */
  getHeightStyleName(element: HTMLElement): string {
    if (element.classList.contains(Constants.SILEX_USE_HEIGHT_NOT_MINHEIGHT)) {
      return 'height';
    }
    return 'min-height';
  }
}
