/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import addClassName from "../../../../compat/add_class_name";
import isNonEmptyString from "../../../../utils/is_non_empty_string";
import objectAssign from "../../../../utils/object_assign";
import type { IStyleList, IStyleObject } from "../get_styling";
import { getStylingAttributes } from "../get_styling";
import { getParentDivElements, isLineBreakElement, isSpanElement } from "../xml_utils";
import applyExtent from "./apply_extent";
import applyFontSize from "./apply_font_size";
import applyLineHeight from "./apply_line_height";
import applyOrigin from "./apply_origin";
import applyPadding from "./apply_padding";
import generateCSSTextOutline from "./generate_css_test_outline";
import ttmlColorToCSSColor from "./ttml_color_to_css_color";

// Styling which can be applied to <span> from any level upper.
// Added here as an optimization
const SPAN_LEVEL_ATTRIBUTES = [
  "color",
  "direction",
  "display",
  "fontFamily",
  "fontSize",
  "fontStyle",
  "fontWeight",
  "textDecoration",
  "textOutline",
  "unicodeBidi",
  "visibility",
  "wrapOption",
];

// TODO
// tts:showBackground (applies to region)
// tts:zIndex (applies to region)

/**
 * Apply style set for a singular text span of the current cue.
 * @param {HTMLElement} element - The text span
 * @param {Object} style - The style to apply
 */
function applyTextStyle(
  element: HTMLElement,
  style: Partial<Record<string, string>>,
  shouldTrimWhiteSpace: boolean,
) {
  // applies to span
  const color = style.color;
  if (isNonEmptyString(color)) {
    element.style.color = ttmlColorToCSSColor(color);
  }

  // applies to body, div, p, region, span
  const backgroundColor = style.backgroundColor;
  if (isNonEmptyString(backgroundColor)) {
    element.style.backgroundColor = ttmlColorToCSSColor(backgroundColor);
  }

  // applies to span
  const textOutline = style.textOutline;
  if (isNonEmptyString(textOutline)) {
    const outlineData = textOutline.trim().replace(/\s+/g, " ").split(" ");
    const len = outlineData.length;
    if (len === 3) {
      const outlineColor = ttmlColorToCSSColor(outlineData[0]);
      const thickness = outlineData[1];
      element.style.textShadow = generateCSSTextOutline(outlineColor, thickness);
    } else if (isNonEmptyString(color) && len === 1) {
      const thickness = outlineData[0];
      element.style.textShadow = generateCSSTextOutline(color, thickness);
    } else if (len === 2) {
      const isFirstArgAColor = /^[#A-Z]/i.test(outlineData[0]);
      const isFirstArgANumber = /^[0-9]/.test(outlineData[0]);

      // XOR-ing to be sure we get what we have
      if (isFirstArgAColor !== isFirstArgANumber) {
        if (isFirstArgAColor) {
          const outlineColor = ttmlColorToCSSColor(outlineData[0]);
          const thickness = outlineData[1];
          element.style.textShadow = generateCSSTextOutline(outlineColor, thickness);
        } else if (isNonEmptyString(color)) {
          const thickness = outlineData[0];
          element.style.textShadow = generateCSSTextOutline(color, thickness);
        }
      }
    }
  }

  // applies to span
  const textDecoration = style.textDecoration;
  if (isNonEmptyString(textDecoration)) {
    switch (textDecoration) {
      case "noUnderline":
      case "noLineThrough":
      case "noOverline":
        element.style.textDecoration = "none";
        break;
      case "lineThrough":
        element.style.textDecoration = "line-through";
        break;
      default:
        element.style.textDecoration = textDecoration;
        break;
    }
  }

  // applies to span
  const fontFamily = style.fontFamily;
  if (isNonEmptyString(fontFamily)) {
    switch (fontFamily) {
      case "proportionalSansSerif":
        element.style.fontFamily = "Arial, Helvetica, Liberation Sans, sans-serif";
        break;

      // TODO monospace or sans-serif or font with both?
      case "monospaceSansSerif":
      case "sansSerif":
        element.style.fontFamily = "sans-serif";
        break;

      case "monospaceSerif":
      case "default":
        element.style.fontFamily = "Courier New, Liberation Mono, monospace";
        break;

      // TODO font with both?
      case "proportionalSerif":
        element.style.fontFamily = "serif";
        break;

      default:
        element.style.fontFamily = fontFamily;
    }
  }

  // applies to span
  const fontStyle = style.fontStyle;
  if (isNonEmptyString(fontStyle)) {
    element.style.fontStyle = fontStyle;
  }

  // applies to span
  const fontWeight = style.fontWeight;
  if (isNonEmptyString(fontWeight)) {
    element.style.fontWeight = fontWeight;
  }

  // applies to span
  const fontSize = style.fontSize;
  if (isNonEmptyString(fontSize)) {
    applyFontSize(element, fontSize);
  } else {
    addClassName(element, "proportional-style");
    element.setAttribute("data-proportional-font-size", "1");
  }

  // applies to p, span
  const direction = style.direction;
  if (isNonEmptyString(direction)) {
    element.style.direction = direction;
  }

  // applies to p, span
  const unicodeBidi = style.unicodeBidi;
  if (isNonEmptyString(unicodeBidi)) {
    switch (unicodeBidi) {
      case "bidiOverride":
        element.style.unicodeBidi = "bidi-override";
        break;
      case "embed":
        element.style.unicodeBidi = "embed";
        break;
      default:
        element.style.unicodeBidi = "normal";
    }
  }

  // applies to body, div, p, region, span
  const visibility = style.visibility;
  if (isNonEmptyString(visibility)) {
    element.style.visibility = visibility;
  }

  // applies to body, div, p, region, span
  const display = style.display;
  if (display === "none") {
    element.style.display = "none";
  }

  // applies to body, div, p, region, span
  const wrapOption = style.wrapOption;

  if (wrapOption === "noWrap") {
    if (shouldTrimWhiteSpace) {
      element.style.whiteSpace = "nowrap";
    } else {
      element.style.whiteSpace = "pre";
    }
  } else if (shouldTrimWhiteSpace) {
    element.style.whiteSpace = "normal";
  } else {
    element.style.whiteSpace = "pre-wrap";
  }
}

/**
 * Apply style for the general text track div.
 * @param {HTMLElement} element - The <div> the style will be applied on.
 * @param {Object} style - The general style object of the paragraph.
 */
function applyGeneralStyle(element: HTMLElement, style: Partial<Record<string, string>>) {
  // Set default text color. It can be overrided by text element color.
  element.style.color = "white";

  element.style.position = "absolute";

  // applies to tt, region
  const extent = style.extent;
  if (isNonEmptyString(extent)) {
    applyExtent(element, extent);
  }

  // applies to region
  const writingMode = style.writingMode;
  if (isNonEmptyString(writingMode)) {
    // TODO
  }

  // applies to region
  const overflow = style.overflow;
  element.style.overflow = isNonEmptyString(overflow) ? overflow : "hidden";

  // applies to region
  const padding = style.padding;
  if (isNonEmptyString(padding)) {
    applyPadding(element, padding);
  }

  // applies to region
  const origin = style.origin;
  if (isNonEmptyString(origin)) {
    applyOrigin(element, origin);
  }

  // applies to region
  const displayAlign = style.displayAlign;
  if (isNonEmptyString(displayAlign)) {
    element.style.display = "flex";
    element.style.flexDirection = "column";
    switch (displayAlign) {
      case "before":
        element.style.justifyContent = "flex-start";
        break;
      case "center":
        element.style.justifyContent = "center";
        break;
      case "after":
        element.style.justifyContent = "flex-end";
        break;
    }
  }

  // applies to region
  const opacity = style.opacity;
  if (isNonEmptyString(opacity)) {
    element.style.opacity = opacity;
  }

  // applies to body, div, p, region, span
  const visibility = style.visibility;
  if (isNonEmptyString(visibility)) {
    element.style.visibility = visibility;
  }

  // applies to body, div, p, region, span
  const display = style.display;
  if (display === "none") {
    element.style.display = "none";
  }
}

/**
 * Apply style set for a <p> element
 * @param {HTMLElement} element - The <p> element
 * @param {Object} style - The general style object of the paragraph.
 */
function applyPStyle(element: HTMLElement, style: Partial<Record<string, string>>) {
  element.style.margin = "0px";

  // Set on it the default font-size, more specific font sizes may then be set
  // on children elements.
  // Doing this on the parent <p> elements seems to fix some CSS issues we had
  // with too large inner line breaks spacing when the text track element was
  // too small, for some reasons.
  addClassName(element, "proportional-style");
  element.setAttribute("data-proportional-font-size", "1");

  // applies to body, div, p, region, span
  const paragraphBackgroundColor = style.backgroundColor;
  if (isNonEmptyString(paragraphBackgroundColor)) {
    element.style.backgroundColor = ttmlColorToCSSColor(paragraphBackgroundColor);
  }

  // applies to p
  const lineHeight = style.lineHeight;
  if (isNonEmptyString(lineHeight)) {
    applyLineHeight(element, lineHeight);
  }

  // applies to p
  const textAlign = style.textAlign;
  if (isNonEmptyString(textAlign)) {
    switch (textAlign) {
      case "center":
        element.style.textAlign = "center";
        break;
      case "left":
      case "start":
        // TODO check what start means (difference with left, writing direction?)
        element.style.textAlign = "left";
        break;
      case "right":
      case "end":
        // TODO check what end means (difference with right, writing direction?)
        element.style.textAlign = "right";
        break;
    }
  }
}

/**
 * Creates span of text for the given #text element, with the right style.
 *
 * TODO create text elements as string? Might help performances.
 * @param {Element} el - the #text element, which text content should be
 * displayed
 * @param {Object} style - the style object for the given text
 * @param {Boolean} shouldTrimWhiteSpace - True if the space should be
 * trimmed.
 * @returns {HTMLElement}
 */
function createTextElement(
  el: Node,
  style: Partial<Record<string, string>>,
  shouldTrimWhiteSpace: boolean,
): HTMLElement {
  const textElement = document.createElement("span");
  let textContent = el.textContent === null ? "" : el.textContent;

  if (shouldTrimWhiteSpace) {
    // 1. Trim leading and trailing whitespace.
    // 2. Collapse multiple spaces into one.
    let trimmed = textContent.trim();
    trimmed = trimmed.replace(/\s+/g, " ");
    textContent = trimmed;
  }

  const textNode = document.createTextNode(textContent);
  textElement.appendChild(textNode);
  textElement.className = "rxp-texttrack-span";

  applyTextStyle(textElement, style, shouldTrimWhiteSpace);
  return textElement;
}

/**
 * Generate every text elements to display in a given paragraph.
 * @param {Element} paragraph - The <p> tag.
 * @param {Array.<Object>} regions
 * @param {Array.<Object>} styles
 * @param {Object} paragraphStyle - The general style object of the paragraph.
 * @param {Boolean} shouldTrimWhiteSpace
 * @returns {Array.<HTMLElement>}
 */
function generateTextContent(
  paragraph: Element,
  regions: IStyleObject[],
  styles: IStyleObject[],
  paragraphStyle: Partial<Record<string, string>>,
  shouldTrimWhiteSpace: boolean,
): HTMLElement[] {
  /**
   * Recursive function, taking a node in argument and returning the
   * corresponding array of HTMLElement in order.
   * @param {Node} node - the node in question
   * @param {Object} style - the current state of the style for the node.
   * /!\ The style object can be mutated, provide a copy of it.
   * @param {Array.<Element>} spans - The spans parent of this node.
   * @param {Boolean} shouldTrimWhiteSpaceFromParent - True if the space should be
   * trimmed by default. From the parent xml:space parameter.
   * @returns {Array.<HTMLElement>}
   */
  function loop(
    node: Node,
    style: IStyleList,
    spans: Node[],
    shouldTrimWhiteSpaceFromParent: boolean,
  ): HTMLElement[] {
    const childNodes = node.childNodes;
    const elements: HTMLElement[] = [];
    for (let i = 0; i < childNodes.length; i++) {
      const currentNode = childNodes[i];
      if (currentNode.nodeName === "#text") {
        const { backgroundColor } = getStylingAttributes(
          ["backgroundColor"],
          spans,
          styles,
          regions,
        );
        if (isNonEmptyString(backgroundColor)) {
          style.backgroundColor = backgroundColor;
        } else {
          style.backgroundColor = "";
        }
        const el = createTextElement(currentNode, style, shouldTrimWhiteSpaceFromParent);
        elements.push(el);
      } else if (isLineBreakElement(currentNode)) {
        const br = document.createElement("BR");
        elements.push(br);
      } else if (
        isSpanElement(currentNode) &&
        currentNode.nodeType === Node.ELEMENT_NODE &&
        currentNode.childNodes.length > 0
      ) {
        const spaceAttribute = (currentNode as Element).getAttribute("xml:space");
        const shouldTrimWhiteSpaceOnSpan = isNonEmptyString(spaceAttribute)
          ? spaceAttribute === "default"
          : shouldTrimWhiteSpaceFromParent;

        // compute the new applyable style
        const newStyle = objectAssign(
          {},
          style,
          getStylingAttributes(SPAN_LEVEL_ATTRIBUTES, [currentNode], styles, regions),
        );

        elements.push(
          ...loop(
            currentNode,
            newStyle,
            [currentNode, ...spans],
            shouldTrimWhiteSpaceOnSpan,
          ),
        );
      }
    }
    return elements;
  }
  return loop(paragraph, objectAssign({}, paragraphStyle), [], shouldTrimWhiteSpace);
}

/**
 * @param {Element} paragraph
 * @param {Element} body
 * @param {Array.<Object>} regions
 * @param {Array.<Object>} styles
 * @param {Object} paragraphStyle
 * @param {Object}
 * @returns {HTMLElement}
 */
export default function createElement(
  paragraph: Element,
  body: Element | null,
  regions: IStyleObject[],
  styles: IStyleObject[],
  paragraphStyle: IStyleList,
  {
    cellResolution,
    shouldTrimWhiteSpace,
  }: {
    shouldTrimWhiteSpace: boolean;
    cellResolution: { columns: number; rows: number };
  },
): HTMLElement {
  const divs = getParentDivElements(paragraph);
  const parentElement = document.createElement("DIV");
  parentElement.className = "rxp-texttrack-region";
  parentElement.setAttribute("data-resolution-columns", String(cellResolution.columns));
  parentElement.setAttribute("data-resolution-rows", String(cellResolution.rows));

  applyGeneralStyle(parentElement, paragraphStyle);
  if (body !== null) {
    // applies to body, div, p, region, span
    const { bodyBackgroundColor } = getStylingAttributes(
      ["backgroundColor"],
      [...divs, body],
      styles,
      regions,
    );
    if (isNonEmptyString(bodyBackgroundColor)) {
      parentElement.style.backgroundColor = ttmlColorToCSSColor(bodyBackgroundColor);
    }
  }

  const pElement = document.createElement("p");
  pElement.className = "rxp-texttrack-p";
  applyPStyle(pElement, paragraphStyle);

  const textContent = generateTextContent(
    paragraph,
    regions,
    styles,
    paragraphStyle,
    shouldTrimWhiteSpace,
  );

  for (let i = 0; i < textContent.length; i++) {
    pElement.appendChild(textContent[i]);
  }

  // NOTE:
  // The following code is for the inclusion of div elements. This has no
  // advantage for now, and might only with future evolutions.
  // (This is only an indication of what the base of the code could look like).
  // if (divs.length) {
  //   let container = parentElement;
  //   for (let i = divs.length - 1; i >= 0; i--) {
  //     // TODO manage style at div level?
  //     // They are: visibility, display and backgroundColor
  //     // All these do not have any difference if applied to the <p> element
  //     // instead of the div.
  //     // The advantage might only be for multiple <p> elements dispatched
  //     // in multiple div Which we do not manage anyway for now.
  //     const divEl = document.createElement("DIV");
  //     divEl.className = "rxp-texttrack-div";
  //     container.appendChild(divEl);
  //     container = divEl;
  //   }
  //   container.appendChild(pElement);
  //   parentElement.appendChild(container);
  // } else {
  //   parentElement.appendChild(pElement);
  // }

  parentElement.appendChild(pElement);
  return parentElement;
}
