/*************************************************************
 *
 *  Copyright (c) 2018-2026 The MathJax Consortium
 *
 *  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.
 */

/**
 * @file  Implements the SvgMtable wrapper for the MmlMtable object
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import { SVG } from '../../svg.js';
import { SvgWrapper, SvgWrapperClass } from '../Wrapper.js';
import { SvgWrapperFactory } from '../WrapperFactory.js';
import {
  SvgCharOptions,
  SvgVariantData,
  SvgDelimiterData,
  SvgFontData,
  SvgFontDataClass,
} from '../FontData.js';
import {
  CommonMtable,
  CommonMtableClass,
  CommonMtableMixin,
} from '../../common/Wrappers/mtable.js';
import { MmlNode } from '../../../core/MmlTree/MmlNode.js';
import { MmlMtable } from '../../../core/MmlTree/MmlNodes/mtable.js';
import { SvgMtrNTD } from './mtr.js';
import { SvgMtdNTD } from './mtd.js';
import { OptionList } from '../../../util/Options.js';
import { StyleJson } from '../../../util/StyleJson.js';

const CLASSPREFIX = 'mjx-';

/*****************************************************************/
/**
 * The SvgMtable interface for the SVG Mtable wrapper
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export interface SvgMtableNTD<N, T, D>
  extends
    SvgWrapper<N, T, D>,
    CommonMtable<
      N,
      T,
      D,
      SVG<N, T, D>,
      SvgWrapper<N, T, D>,
      SvgWrapperFactory<N, T, D>,
      SvgWrapperClass<N, T, D>,
      SvgCharOptions,
      SvgVariantData,
      SvgDelimiterData,
      SvgFontData,
      SvgFontDataClass,
      SvgMtrNTD<N, T, D>
    > {
  /**
   * The column for labels
   */
  labels: N;
}

/**
 * The SvgMtableClass interface for the SVG Mtable wrapper
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export interface SvgMtableClass<N, T, D>
  extends
    SvgWrapperClass<N, T, D>,
    CommonMtableClass<
      N,
      T,
      D,
      SVG<N, T, D>,
      SvgWrapper<N, T, D>,
      SvgWrapperFactory<N, T, D>,
      SvgWrapperClass<N, T, D>,
      SvgCharOptions,
      SvgVariantData,
      SvgDelimiterData,
      SvgFontData,
      SvgFontDataClass
    > {
  new (
    factory: SvgWrapperFactory<N, T, D>,
    node: MmlNode,
    parent?: SvgWrapper<N, T, D>
  ): SvgMtableNTD<N, T, D>;
}

/*****************************************************************/

/**
 * The SvgMtable wrapper class for the MmlMtable class
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export const SvgMtable = (function <N, T, D>(): SvgMtableClass<N, T, D> {
  const Base = CommonMtableMixin<
    N,
    T,
    D,
    SVG<N, T, D>,
    SvgWrapper<N, T, D>,
    SvgWrapperFactory<N, T, D>,
    SvgWrapperClass<N, T, D>,
    SvgCharOptions,
    SvgVariantData,
    SvgDelimiterData,
    SvgFontData,
    SvgFontDataClass,
    SvgMtrNTD<N, T, D>,
    SvgMtableClass<N, T, D>
  >(SvgWrapper);

  // @ts-expect-error Avoid message about base constructors not having the same
  // type (they should both be SvgWrapper<N, T, D>, but are thought of as
  // different by typescript)
  return class SvgMtable extends Base implements SvgMtableNTD<N, T, D> {
    /**
     * @override
     */
    public static kind = MmlMtable.prototype.kind;

    /**
     * @override
     */
    public static styles: StyleJson = {
      'g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line]':
        {
          'stroke-width': '70px',
          fill: 'none',
        },
      'g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame]':
        {
          'stroke-width': '70px',
          fill: 'none',
        },
      'g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed':
        {
          'stroke-dasharray': '140',
        },
      'g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted':
        {
          'stroke-linecap': 'round',
          'stroke-dasharray': '0,140',
        },
      'g[data-mml-node="mtable"] > g > svg': {
        overflow: 'visible',
      },
    };

    /**
     * @override
     */
    public labels: N;

    /******************************************************************/

    /**
     * @param {N} svg  The container in which to place the rows
     */
    protected placeRows(svg: N) {
      const equal = this.node.attributes.get('equalrows') as boolean;
      const { H, D } = this.getTableData();
      const HD = this.getEqualRowHeight();
      const rSpace = this.getRowHalfSpacing();
      const rLines = [this.fLine, ...this.rLines, this.fLine];
      let y = this.getBBox().h - rLines[0];
      for (let i = 0; i < this.numRows; i++) {
        const row = this.childNodes[i] as SvgMtrNTD<N, T, D>;
        [row.H, row.D] = this.getRowHD(equal, HD, H[i], D[i]);
        [row.tSpace, row.bSpace] = [rSpace[i], rSpace[i + 1]];
        [row.tLine, row.bLine] = [rLines[i], rLines[i + 1]];
        row.toSVG([svg]);
        row.place(0, y - rSpace[i] - row.H);
        y -= rSpace[i] + row.H + row.D + rSpace[i + 1] + rLines[i + 1];
      }
    }

    /**
     * @param {boolean} equal   True for equal-height rows
     * @param {number} HD       The height of equal-height rows
     * @param {number} H        The natural height of the row
     * @param {number} D        The natural depth of the row
     * @returns {number[]}      The (possibly scaled) height and depth to use
     */
    protected getRowHD(
      equal: boolean,
      HD: number,
      H: number,
      D: number
    ): [number, number] {
      return equal ? [(HD + H - D) / 2, (HD - H + D) / 2] : [H, D];
    }

    /******************************************************************/

    /**
     * @override
     */
    public handleColor() {
      super.handleColor();
      const rect = this.firstChild();
      if (rect) {
        this.adaptor.setAttribute(rect, 'width', this.fixed(this.getWidth()));
      }
    }

    /**
     * Add vertical lines between columns
     *
     * @param {N} svg   The container for the table
     */
    protected handleColumnLines(svg: N) {
      if (this.node.attributes.get('columnlines') === 'none') return;
      const lines = this.getColumnAttributes('columnlines');
      if (!lines) return;
      const cSpace = this.getColumnHalfSpacing();
      const cLines = this.cLines;
      const cWidth = this.getComputedWidths();
      let x = this.fLine;
      for (let i = 0; i < lines.length; i++) {
        x += cSpace[i] + cWidth[i] + cSpace[i + 1];
        if (lines[i] !== 'none') {
          this.adaptor.append(svg, this.makeVLine(x, lines[i], cLines[i]));
        }
        x += cLines[i];
      }
    }

    /**
     * Add horizontal lines between rows
     *
     * @param {N} svg   The container for the table
     */
    protected handleRowLines(svg: N) {
      if (this.node.attributes.get('rowlines') === 'none') return;
      const lines = this.getRowAttributes('rowlines');
      if (!lines) return;
      const equal = this.node.attributes.get('equalrows') as boolean;
      const { H, D } = this.getTableData();
      const HD = this.getEqualRowHeight();
      const rSpace = this.getRowHalfSpacing();
      const rLines = this.rLines;
      let y = this.getBBox().h - this.fLine;
      for (let i = 0; i < lines.length; i++) {
        const [rH, rD] = this.getRowHD(equal, HD, H[i], D[i]);
        y -= rSpace[i] + rH + rD + rSpace[i + 1];
        if (lines[i] !== 'none') {
          this.adaptor.append(svg, this.makeHLine(y, lines[i], rLines[i]));
        }
        y -= rLines[i];
      }
    }

    /**
     * Add a frame to the mtable, if needed
     *
     * @param {N} svg   The container for the table
     */
    protected handleFrame(svg: N) {
      if (this.frame && this.fLine) {
        const { h, d, w } = this.getBBox();
        const style = this.node.attributes.get('frame') as string;
        this.adaptor.append(svg, this.makeFrame(w, h, d, style));
      }
    }

    /**
     * @param {N} svg The svg element
     * @returns {number}   The x-adjustement needed to handle the true size of percentage-width tables
     */
    protected handlePWidth(svg: N): number {
      if (!this.pWidth) {
        return 0;
      }
      const { w, L, R } = this.getBBox();
      const W = L + this.pWidth + R;
      const align = this.getAlignShift()[0];
      const max = Math.max(
        this.isTop ? W : 0,
        this.container.getWrapWidth(this.containerI)
      );
      const CW = max - L - R;
      const dw = w - (this.pWidth > CW ? CW : this.pWidth);
      const dx = align === 'left' ? 0 : align === 'right' ? dw : dw / 2;
      if (dx) {
        const table = this.svg('g', {}, this.adaptor.childNodes(svg));
        this.place(dx, 0, table);
        this.adaptor.append(svg, table);
      }
      return dx;
    }

    /******************************************************************/

    /**
     * @param {string} style   The line style whose class is to be obtained
     * @returns {string}       The class name for the style
     */
    protected lineClass(style: string): string {
      return CLASSPREFIX + style;
    }

    /**
     * @param {number} w       The width of the frame
     * @param {number} h       The height of the frame
     * @param {number} d       The depth of the frame
     * @param {string} style   The border style for the frame
     * @returns {N}            The SVG element for the frame
     */
    protected makeFrame(w: number, h: number, d: number, style: string): N {
      const t = this.fLine;
      return this.svg(
        'rect',
        this.setLineThickness(t, style, {
          'data-frame': true,
          class: this.lineClass(style),
          width: this.fixed(w - t),
          height: this.fixed(h + d - t),
          x: this.fixed(t / 2),
          y: this.fixed(t / 2 - d),
        })
      );
    }

    /**
     * @param {number} x       The x location of the line
     * @param {string} style   The border style for the line
     * @param {number} t       The line thickness
     * @returns {N}            The SVG element for the line
     */
    protected makeVLine(x: number, style: string, t: number): N {
      const { h, d } = this.getBBox();
      const dt = style === 'dotted' ? t / 2 : 0;
      const X = this.fixed(x + t / 2);
      return this.svg(
        'line',
        this.setLineThickness(t, style, {
          'data-line': 'v',
          class: this.lineClass(style),
          x1: X,
          y1: this.fixed(dt - d),
          x2: X,
          y2: this.fixed(h - dt),
        })
      );
    }

    /**
     * @param {number} y       The y location of the line
     * @param {string} style   The border style for the line
     * @param {number} t       The line thickness
     * @returns {N}            The SVG element for the line
     */
    protected makeHLine(y: number, style: string, t: number): N {
      const w = this.getBBox().w;
      const dt = style === 'dotted' ? t / 2 : 0;
      const Y = this.fixed(y - t / 2);
      return this.svg(
        'line',
        this.setLineThickness(t, style, {
          'data-line': 'h',
          class: this.lineClass(style),
          x1: this.fixed(dt),
          y1: Y,
          x2: this.fixed(w - dt),
          y2: Y,
        })
      );
    }

    /**
     * @param {number} t                The thickness of the line
     * @param {string} style            The border style for the line
     * @param {OptionList} properties   The list of properties to modify
     * @returns {OptionList}            The modified properties
     */
    protected setLineThickness(
      t: number,
      style: string,
      properties: OptionList
    ): OptionList {
      if (t !== 0.07) {
        properties['stroke-thickness'] = this.fixed(t);
        if (style !== 'solid') {
          properties['stroke-dasharray'] =
            (style === 'dotted' ? '0,' : '') + this.fixed(2 * t);
        }
      }
      return properties;
    }

    /******************************************************************/

    /**
     * Handle addition of labels to the table
     *
     * @param {N} svg       The container for the table contents
     * @param {N} _parent   The parent containing the the table
     * @param {number} dx   The adjustement for percentage width tables
     */
    protected handleLabels(svg: N, _parent: N, dx: number) {
      if (!this.hasLabels) return;
      const labels = this.labels;
      const attributes = this.node.attributes;
      //
      //  Set the side for the labels
      //
      const side = attributes.get('side') as string;
      //
      // Add the labels to the table
      //
      this.spaceLabels();
      //
      // Handle top-level table to make it adapt to container size
      //   but place subtables explicitly
      //
      this.isTop
        ? this.topTable(svg, labels, side)
        : this.subTable(svg, labels, side, dx);
    }

    /**
     * Add spacing elements between the label rows to align them with the rest of the table
     */
    protected spaceLabels() {
      const adaptor = this.adaptor;
      const h = this.getBBox().h;
      const L = this.getTableData().L;
      const space = this.getRowHalfSpacing();
      //
      //  Start with frame size and add in spacing, height and depth,
      //    and line thickness for each non-labeled row.
      //
      let y = h - this.fLine;
      let current = adaptor.firstChild(this.labels) as N;
      for (let i = 0; i < this.numRows; i++) {
        const row = this.childNodes[i] as SvgMtrNTD<N, T, D>;
        if (row.node.isKind('mlabeledtr')) {
          const cell = row.childNodes[0] as SvgMtdNTD<N, T, D>;
          y -= space[i] + row.H;
          row.placeCell(cell, {
            x: 0,
            y: y,
            w: L,
            lSpace: 0,
            rSpace: 0,
            lLine: 0,
            rLine: 0,
          });
          y -= row.D + space[i + 1] + this.rLines[i];
          current = adaptor.next(current) as N;
        } else {
          y -= space[i] + row.H + row.D + space[i + 1] + this.rLines[i];
        }
      }
    }

    /**
     * Handles tables with labels so that the label will move with the size of the container
     *
     * @param {N} svg         The SVG container for the table
     * @param {N} labels      The group of labels
     * @param {string} side   The side alignment (left or right)
     */
    protected topTable(svg: N, labels: N, side: string) {
      const adaptor = this.adaptor;
      const { h, d, w, L, R } = this.getBBox();
      const W = L + (this.pWidth || w) + R;
      const LW = this.getTableData().L;
      const [, align, shift] = this.getPadAlignShift(side);
      const dx =
        shift + (align === 'right' ? -W : align === 'center' ? -W / 2 : 0) + L;
      const matrix = 'matrix(1 0 0 -1 0 0)';
      const scale = `scale(${this.jax.fixed((this.font.params.x_height * 1000) / this.metrics.ex, 2)})`;
      const transform = `translate(0 ${this.fixed(h)}) ${matrix} ${scale}`;
      const table = this.svg(
        'svg',
        {
          'data-table': true,
          preserveAspectRatio:
            align === 'left'
              ? 'xMinYMid'
              : align === 'right'
                ? 'xMaxYMid'
                : 'xMidYMid',
          viewBox: `${this.fixed(-dx)} ${this.fixed(-h)} 1 ${this.fixed(h + d)}`,
        },
        [this.svg('g', { transform: matrix }, adaptor.childNodes(svg))]
      );
      labels = this.svg(
        'svg',
        {
          'data-labels': true,
          preserveAspectRatio: side === 'left' ? 'xMinYMid' : 'xMaxYMid',
          viewBox: [
            side === 'left' ? 0 : this.fixed(LW),
            this.fixed(-h),
            1,
            this.fixed(h + d),
          ].join(' '),
        },
        [labels]
      );
      adaptor.append(
        svg,
        this.svg('g', { transform: transform }, [table, labels])
      );
      this.place(-L, 0, svg); // remove spacing for L, which is added by the parent during appending
    }

    /**
     * @param {N} svg         The SVG container for the table
     * @param {N} labels      The group of labels
     * @param {string} side   The side alignment (left or right)
     * @param {number} dx     The adjustement for percentage width tables
     */
    protected subTable(svg: N, labels: N, side: string, dx: number) {
      const adaptor = this.adaptor;
      const { w, L, R } = this.getBBox();
      const W = L + (this.pWidth || w) + R;
      const labelW = this.getTableData().L;
      const align = this.getAlignShift()[0];
      const CW = Math.max(W, this.container.getWrapWidth(this.containerI));
      this.place(
        side === 'left'
          ? (align === 'left'
              ? 0
              : align === 'right'
                ? W - CW + dx
                : (W - CW) / 2 + dx) - L
          : (align === 'left'
              ? CW
              : align === 'right'
                ? W + dx
                : (CW + W) / 2 + dx) -
              L -
              labelW,
        0,
        labels
      );
      adaptor.append(svg, labels);
    }

    /**
     * @override
     */
    constructor(
      factory: SvgWrapperFactory<N, T, D>,
      node: MmlNode,
      parent: SvgWrapper<N, T, D> = null
    ) {
      super(factory, node, parent);
      const def: OptionList = { 'data-labels': true };
      if (this.isTop) {
        def.transform = 'matrix(1 0 0 -1 0 0)';
      }
      this.labels = this.svg('g', def);
    }

    /**
     * @override
     */
    public toSVG(parents: N[]) {
      const svg = this.standardSvgNodes(parents)[0];
      this.placeRows(svg);
      this.handleColumnLines(svg);
      this.handleRowLines(svg);
      this.handleFrame(svg);
      const dx = this.handlePWidth(svg);
      this.handleLabels(svg, parents[0], dx);
    }
  };
})<any, any, any>();
