/*
 * format-dashboard-card.ts
 *
 * Copyright (C) 2020-2022 Posit Software, PBC
 */

import { Document, Element, Node } from "../../core/deno-dom.ts";
import { recursiveApplyFillClasses } from "./format-dashboard-layout.ts";
import {
  applyAttributes,
  applyClasses,
  attrToCardBodyStyle,
  attrToStyle,
  DashboardMeta,
  ensureCssUnits,
  hasFlowLayout,
  kCardClass,
  kFillAttr,
  kLayoutFill,
  kLayoutFlow,
  kValueboxClass,
  makeEl,
  processAndRemoveAttr,
} from "./format-dashboard-shared.ts";
import { makeSidebar } from "./format-dashboard-sidebar.ts";

// The html to generate the expand button
const kExpandBtnHtml = `
<bslib-tooltip placement="auto" bsoptions="[]" data-require-bs-version="5" data-require-bs-caller="tooltip()">
    <template>Expand</template>
    <span class="bslib-full-screen-enter badge rounded-pill">
        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" style="height:1em;width:1em;" aria-hidden="true" role="img"><path d="M20 5C20 4.4 19.6 4 19 4H13C12.4 4 12 3.6 12 3C12 2.4 12.4 2 13 2H21C21.6 2 22 2.4 22 3V11C22 11.6 21.6 12 21 12C20.4 12 20 11.6 20 11V5ZM4 19C4 19.6 4.4 20 5 20H11C11.6 20 12 20.4 12 21C12 21.6 11.6 22 11 22H3C2.4 22 2 21.6 2 21V13C2 12.4 2.4 12 3 12C3.6 12 4 12.4 4 13V19Z"></path></svg>
    </span>
</bslib-tooltip>
`;

// Card classes
const kCardBodyClass = "card-body";
const kCardHeaderClass = "card-header";
const kCardFooterClass = "card-footer";
const kCardTitleClass = "card-title";
const kCardToolbarClass = "card-toolbar";

const kCardSidebarClass = "card-sidebar";

// Tabset classes
const kTabsetClass = "tabset";

// Tabset BS values
const kTabsetIdPrefix = "card-tabset-";

// Card attributes (our options are expressed using these attributes)
const kAttrTitle = "data-title";
const kAttrExpandable = "data-expandable";
const kAttrMaxHeight = "data-max-height";
const kAttrMinHeight = "data-min-height";
const kAttrHeight = "data-height";
const kAttrPadding = "data-padding";

// BSLib Attributes
const kAttrFullScreen = "data-full-screen";

// BSLib Card Classes
const kBsCardClasses = ["bslib-card", "html-fill-container"];
const kBsCardScriptInitAttrs = { ["data-bslib-card-init"]: "" };

const kBsTabsetCardHeaderClasses = ["bslib-navs-card-title"];
const kQuartoHideTitleClass = "dashboard-card-no-title";

// BSLib Card Attributes
const kBsCardAttributes: Record<string, string> = {
  ["data-bslib-card-init"]: "",
  ["data-require-bs-caller"]: "card()",
};

// How to process card attributes (card attributes express options that the
// user has provided via markdown) - this converts them into their final rendered
// form (e.g. turn a height attribute into a css style enforcing height)
const cardAttrHandlers = (doc: Document, dashboardMeta: DashboardMeta) => {
  return [
    {
      attr: kAttrExpandable,
      handle: (el: Element, attrValue: string) => {
        if (attrValue !== "false") {
          const shellEl = doc.createElement("DIV");
          shellEl.innerHTML = kExpandBtnHtml;
          for (const childEl of shellEl.children) {
            el.appendChild(childEl);
          }
          el.setAttribute(kAttrFullScreen, "false");
        }
      },
      defaultValue: (el: Element) => {
        if (el.classList.contains(kValueboxClass) || hasFlowLayout(el)) {
          return "false";
        } else {
          return dashboardMeta.expandable ? "true" : "false";
        }
      },
    },
    {
      attr: kAttrMaxHeight,
      handle: ensureCssUnits(attrToStyle("max-height")),
    },
    { attr: kAttrMinHeight, handle: ensureCssUnits(attrToStyle("min-height")) },
    { attr: kAttrHeight, handle: ensureCssUnits(attrToStyle("height")) },
    {
      attr: kAttrPadding,
      handle: ensureCssUnits(attrToCardBodyStyle("padding")),
    },
  ];
};

// How to process card body attributes (card attributes express options that the
// user has provided via markdown) - this converts them into their final rendered
// form (e.g. turn a height attribute into a css style enforcing height)
const cardBodyAttrHandlers = () => {
  return [
    {
      attr: kAttrMaxHeight,
      handle: ensureCssUnits(attrToStyle("max-height")),
    },
    { attr: kAttrMinHeight, handle: ensureCssUnits(attrToStyle("min-height")) },
    { attr: kAttrHeight, handle: ensureCssUnits(attrToStyle("height")) },
  ];
};

export function processCards(doc: Document, dashboardMeta: DashboardMeta) {
  // We need to process cards specially
  const cardNodes = doc.body.querySelectorAll(`.${kCardClass}`);
  let cardCount = 0;
  for (const cardNode of cardNodes) {
    cardCount++;
    const cardEl = cardNode as Element;

    // Sort the children
    const cardBodyEls: Element[] = [];
    let cardHeaderEl = undefined;
    let cardSidebarEl = undefined;
    for (const cardChildEl of cardEl.children) {
      if (cardChildEl.classList.contains(kCardBodyClass)) {
        cardBodyEls.push(cardChildEl);
      } else if (cardChildEl.classList.contains(kCardHeaderClass)) {
        cardHeaderEl = cardChildEl;
      } else if (cardChildEl.classList.contains(kCardSidebarClass)) {
        cardSidebarEl = cardChildEl;
      }
    }

    // Process the header
    if (cardHeaderEl) {
      // Loose text gets grouped into a div for alignment purposes
      // Always place this element first no matter what else is going on
      const looseText: Node[] = [];

      // See if there is a toolbar in the header
      const cardToolbarEl = cardHeaderEl.querySelector(`.${kCardToolbarClass}`);

      const isText = (node: Node) => node.nodeType === Node.TEXT_NODE;
      const isEmphasis = (node: Node) => node.nodeName === "EM";
      const isBold = (node: Node) => node.nodeName === "STRONG";
      const isMath = (node: Node) =>
        node.nodeName === "SPAN" &&
        (node as Element).classList.contains("math") &&
        (node as Element).classList.contains("inline");

      for (const headerChildNode of Array.from(cardHeaderEl.childNodes)) {
        if (
          (isText(headerChildNode) ||
            isEmphasis(headerChildNode) ||
            isBold(headerChildNode) ||
            isMath(headerChildNode)) &&
          headerChildNode.textContent.trim() !== ""
        ) {
          looseText.push(headerChildNode);
          headerChildNode.parentNode?.removeChild(headerChildNode);
        }
      }

      if (looseText.length > 0) {
        // Inject the text into a div that we can use for layout
        const classes = [kCardTitleClass];

        const titleTextDiv = makeEl("DIV", { classes }, doc);
        const innerSpan = makeEl("SPAN", {
          attributes: { style: "display: inline" },
        }, doc);
        titleTextDiv.appendChild(innerSpan);
        for (const node of looseText) {
          innerSpan.appendChild(node);
        }
        if (cardToolbarEl) {
          cardToolbarEl.insertBefore(titleTextDiv, cardToolbarEl.firstChild);
        } else {
          cardHeaderEl.insertBefore(titleTextDiv, cardHeaderEl.firstChild);
        }
      } else {
        cardHeaderEl.classList.add(kQuartoHideTitleClass);
      }
    }

    // Add card attributes
    applyClasses(cardEl, kBsCardClasses);
    applyAttributes(cardEl, kBsCardAttributes);

    // If this is a tabset, we need to do more
    const tabSetId = cardEl.classList.contains(kTabsetClass)
      ? `${kTabsetIdPrefix}${cardCount}`
      : undefined;
    if (tabSetId) {
      // Fix up the header
      if (cardHeaderEl) {
        convertToTabsetHeader(tabSetId, cardHeaderEl, cardBodyEls, doc);
      }
      // Convert the body to tabs
      convertToTabs(tabSetId, cardEl, cardBodyEls, cardSidebarEl, doc);
    } else {
      // Process a card sidebar, if present
      if (cardSidebarEl) {
        cardBodyEls.forEach((el) => el.remove);
        // TODO: Make a cooler id if possible
        const sidebarId = `card-${cardCount}-card-sidebar`;
        const sidebarContainerEl = makeSidebar(
          sidebarId,
          cardSidebarEl,
          cardBodyEls,
          doc,
        );
        cardEl.appendChild(sidebarContainerEl);
      }
    }

    // Process card attributes
    for (const cardAttrHandler of cardAttrHandlers(doc, dashboardMeta)) {
      const defaultValue = cardAttrHandler.defaultValue
        ? cardAttrHandler.defaultValue(cardEl)
        : undefined;
      processAndRemoveAttr(
        cardEl,
        cardAttrHandler.attr,
        cardAttrHandler.handle,
        defaultValue,
      );
    }

    // Process card body attributes
    for (const cardBodyEl of cardBodyEls) {
      const layout = cardBodyLayout(cardBodyEl);
      if (layout === kLayoutFlow) {
        cardBodyEl.classList.add(kLayoutFlow);
      }

      for (const cardBodyAttrHandler of cardBodyAttrHandlers()) {
        processAndRemoveAttr(
          cardBodyEl,
          cardBodyAttrHandler.attr,
          cardBodyAttrHandler.handle,
        );
        if (!tabSetId) {
          // If this was converted to tab, this will already be taken care of
          recursiveApplyFillClasses(cardBodyEl);
        }
      }
    }

    // Initialize the cards
    cardEl.appendChild(initCardScript(doc));
  }
}

export function isFlowCard(el: Element) {
  const layouts = cardBodyLayouts(el);
  return layouts.every((layout) => {
    return layout === kLayoutFlow;
  });
}

function cardBodyLayouts(el: Element) {
  // Find card-bodies and inspect card bodies to see
  // what is up
  const cardBodyNodes = el.querySelectorAll(`.${kCardBodyClass}`);
  const layouts: string[] = [];
  for (const cardBodyNode of cardBodyNodes) {
    layouts.push(cardBodyLayout(cardBodyNode as Element));
  }
  return layouts;
}

function cardBodyLayout(cardBodyEl: Element) {
  const explicitFill = cardBodyEl.getAttribute(kFillAttr);
  if (explicitFill !== null) {
    // If there is an explicitly specified layout, use that
    return explicitFill !== "false" ? kLayoutFill : kLayoutFlow;
  } else if (shinyInputs(cardBodyEl)) {
    // If the card only contains shiny inputs, that is a flow layout
    return kLayoutFlow;
  } else {
    // Otherwise assume this is a flow
    return kLayoutFill;
  }
}

function shinyInputs(cardBodyEl: Element) {
  for (const childEl of cardBodyEl.children) {
    if (!childEl.classList.contains("cell-output")) {
      return false;
    }

    if (childEl.childElementCount < 1) {
      return false;
    }

    const firstChildEl = childEl.children.item(0);
    if (!firstChildEl.classList.contains("shiny-input-container")) {
      return false;
    }
  }
  return true;
}

function initCardScript(doc: Document) {
  const scriptInitEl = doc.createElement("SCRIPT");
  applyAttributes(scriptInitEl, kBsCardScriptInitAttrs);
  scriptInitEl.innerText = "bslib.Card.initializeAllCards();";
  return scriptInitEl;
}

function convertToTabsetHeader(
  tabSetId: string,
  cardHeaderEl: Element,
  cardBodyEls: Element[],
  doc: Document,
) {
  // Decorate it
  applyClasses(cardHeaderEl, kBsTabsetCardHeaderClasses);

  // Add the tab nav element
  const ulEl = doc.createElement("UL");
  applyClasses(ulEl, ["nav", "nav-tabs", "card-header-tabs"]);
  applyAttributes(ulEl, {
    role: "tablist",
    ["data-tabsetid"]: tabSetId,
  });

  let cardBodyCount = 0;
  for (const cardBodyEl of cardBodyEls) {
    cardBodyCount++;

    // If the user has provided a title, use that
    let cardBodyTitle = cardBodyEl.getAttribute(kAttrTitle);
    if (cardBodyTitle == null) {
      cardBodyTitle = `Tab ${cardBodyCount}`;
    }

    // Add the liEls for each tab
    const liEl = doc.createElement("LI");
    applyClasses(liEl, ["nav-item"]);
    applyAttributes(liEl, { role: "presentation" });

    const aEl = doc.createElement("A");
    applyAttributes(aEl, {
      href: `#${tabSetId}-${cardBodyCount}`,
      role: "tab",
      ["data-toggle"]: "tab",
      ["data-bs-toggle"]: "tab",
      ["data-value"]: cardBodyTitle,
      ["aria-selected"]: cardBodyCount === 1 ? "true" : "false",
    });

    const clz = ["nav-link"];
    if (cardBodyCount === 1) {
      clz.push("active");
    }
    applyClasses(aEl, clz);

    aEl.innerText = cardBodyTitle;
    liEl.appendChild(aEl);

    // Add the li
    ulEl.appendChild(liEl);
  }
  cardHeaderEl.appendChild(ulEl);
}

function findFooterEl(cardEl: Element) {
  for (const childEl of cardEl.children) {
    if (childEl.classList.contains(kCardFooterClass)) {
      return childEl;
    }
  }
}

function convertToTabs(
  tabSetId: string,
  cardEl: Element,
  cardBodyEls: Element[],
  cardSidebarEl: Element | undefined,
  doc: Document,
) {
  // Make sure we place this above the card footer
  const cardFooterEl = findFooterEl(cardEl);

  const tabContainerEl = tabSetId ? doc.createElement("DIV") : undefined;
  if (tabContainerEl) {
    tabContainerEl.classList.add("tab-content");
    tabContainerEl.setAttribute("data-tabset-id", tabSetId);

    if (cardFooterEl) {
      cardEl.insertBefore(tabContainerEl, cardFooterEl);
    } else {
      cardEl.appendChild(tabContainerEl);
    }
  }

  let cardBodyCount = 0;
  for (const cardBodyEl of cardBodyEls) {
    cardBodyCount++;

    for (const cardBodyAttrHandler of cardBodyAttrHandlers()) {
      processAndRemoveAttr(
        cardBodyEl,
        cardBodyAttrHandler.attr,
        cardBodyAttrHandler.handle,
      );
    }

    // Deal with tabs
    if (tabContainerEl) {
      const tabPaneEl = doc.createElement("DIV");
      tabPaneEl.classList.add("tab-pane");
      if (cardBodyCount === 1) {
        tabPaneEl.classList.add("active");
        tabPaneEl.classList.add("show");
      }
      tabPaneEl.setAttribute("role", "tabpanel");
      tabPaneEl.id = `${tabSetId}-${cardBodyCount}`;
      tabPaneEl.appendChild(cardBodyEl);
      tabContainerEl.appendChild(tabPaneEl);
    }
  }

  if (tabContainerEl) {
    recursiveApplyFillClasses(tabContainerEl);
  }

  // If there is a sidebar, wrap it around the tabset
  if (cardSidebarEl && tabContainerEl) {
    const sidebarId = `${tabSetId}-card-sidebar`;
    const sidebarContainerEl = makeSidebar(
      sidebarId,
      cardSidebarEl,
      [tabContainerEl],
      doc,
    );

    if (cardFooterEl) {
      cardEl.insertBefore(sidebarContainerEl, cardFooterEl);
    } else {
      cardEl.appendChild(sidebarContainerEl);
    }
  }
}
