import type { JSXElement } from '../../support/jsx-runtime.ts';
import type { RenderProvider, RenderState } from '../types.ts';
import type { RenderContext } from './context.ts';
import { classString, combinePropsToString, getChildren, isOfType, visit } from './common.ts';

export const SUMMARY_STYLE = Object.entries({
  display: 'none',
  'font-size': '1px',
  color: '#333333',
  'line-height': '1px',
  'max-height': '0px',
  'max-width': '0px',
  opacity: '0',
  overflow: 'hidden'
}).map(([key, value]) => `${key}: ${value}`).join('; ');

const allowedProps = new Set([
  'className', 'id', 'dir', 'name', 'src',
  'alt', 'href', 'title', 'height', 'target',
  'width', 'style', 'align', 'valign'
]);

const propsToString = combinePropsToString.bind(null, allowedProps);

const standardInline = async ({ recurse, node }: RenderState<JSXElement, RenderContext>): Promise<string> =>
  `<${node.type} ${propsToString(node.props)}>${await recurse()}</${node.type}>`;

const standard = async (state: RenderState<JSXElement, RenderContext>): Promise<string> => `${await standardInline(state)}\n`;
const standardFull = async (state: RenderState<JSXElement, RenderContext>): Promise<string> => `\n${await standardInline(state)}\n`;

export const Html: RenderProvider<RenderContext> = {
  finalize: async (html, context, isRoot = false) => {
    html = html
      .replace(/(<[/](?:a)>)([A-Za-z0-9$])/g, (_, tag, value) => `${tag} ${value}`)
      .replace(/(<[uo]l>)(<li>)/g, (_, a, b) => `${a} ${b}`);

    if (isRoot) {
      const wrapper = await context.loader.read('/email/inky.wrapper.html');

      // Get Subject
      const headerTop: string[] = [];
      const bodyTop: string[] = [];

      // Force summary to top, and title to head
      const final = wrapper
        .replace('<!-- BODY -->', html)
        .replace(/<title>.*?<\/title>/, a => { headerTop.push(a); return ''; })
        .replace(/<span[^>]+id="summary"[^>]*>(.*?)<\/span>/sm, a => { bodyTop.push(a); return ''; })
        .replace(/<head( [^>]*)?>/, tag => `${tag}\n${headerTop.join('\n')}`)
        .replace(/<body[^>]*>/, tag => `${tag}\n${bodyTop.join('\n')}`);

      // Allow tag suffixes/prefixes via comments
      html = final
        .replace(/\s*<!--\s*[$]:([^ -]+)\s*-->\s*(<\/[^>]+>)/g, (_, suffix, tag) => `${tag}${suffix}`)
        .replace(/(<[^\/][^>]+>)\s*<!--\s*[#]:([^ ]+)\s*-->\s*/g, (_, tag, prefix) => `${prefix}${tag}`);
    }

    return html;
  },

  For: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
  If: async ({ recurse, props }) => `{{#${props.attr}}}${await recurse()}{{/${props.attr}}}`,
  Unless: async ({ recurse, props }) => `{{^${props.attr}}}${await recurse()}{{/${props.attr}}}`,
  Value: async ({ props }) => props.raw ? `{{{${props.attr}}}}` : `{{${props.attr}}}`,

  br: async () => '<br>\n',
  hr: async (node) => `<table ${propsToString(node.props)}><th></th></table>`,
  strong: standardInline, em: standardInline, p: standardFull,
  h1: standardFull, h2: standardFull, h3: standardFull, h4: standardFull,
  li: standard, ol: standardFull, ul: standardFull,
  table: standardFull, thead: standard, tr: standard, td: standard, th: standard, tbody: standard, center: standard, img: standardInline,
  title: standard,
  div: standard, span: standardInline, small: standardInline,
  a: async ({ recurse, props }) => `<a ${propsToString(props)}>${await recurse()}</a>`,

  InkyTemplate: ctx => ctx.recurse(),
  Title: async ({ recurse }) => `<title>${await recurse()}</title>`,
  Summary: async ({ recurse }) => `<span id="summary" style="${SUMMARY_STYLE}">${await recurse()}</span>`,

  Column: async ({ props, recurse, stack, node, context }): Promise<string> => {

    recurse();

    let expander = '';

    const parent = stack.at(-1)!;
    const siblings = getChildren(parent).filter(child => isOfType(child, 'Column'));
    const colCount = siblings.length || 1;

    if (parent) {
      const parentNode: (typeof parent) & { columnVisited?: boolean } = parent;
      if (!parentNode.columnVisited) {
        parentNode.columnVisited = true;
        if (siblings.length) {
          siblings[0].props.className = classString(siblings[0].props.className ?? '', 'first');
          siblings.at(-1)!.props.className = classString(siblings.at(-1)!.props.className ?? '', 'last');
        }
      }
    } else {
      props.className = classString(props.className ?? '', 'first', 'last');
    }

    // Check for sizes. If no attribute is provided, default to small-12. Divide evenly for large columns
    const smallSize = node.props.small ?? context.columnCount;
    const largeSize = node.props.large ?? node.props.small ?? Math.trunc(context.columnCount / colCount);

    // If the column contains a nested row, the .expander class should not be used
    if (largeSize === context.columnCount && !props.noExpander) {
      let hasRow = false;
      visit(node, (child) => {
        if (isOfType(child, 'Row')) {
          return hasRow = true;
        }
      });
      if (!hasRow) {
        expander = '\n<th class="expander"></th>';
      }
    }

    const classes: string[] = [`small-${smallSize}`, `large-${largeSize}`, 'columns'];
    if (props.smallOffset) {
      classes.push(`small-offset-${props.smallOffset}`);
    }
    if (props.hideSmall) {
      classes.push('hide-for-small');
    }
    if (props.largeOffset) {
      classes.push(`large-offset-${props.largeOffset}`);
    }
    if (props.hideLarge) {
      classes.push('hide-for-large');
    }

    // Final HTML output
    return `
<th ${propsToString(node.props, classes)}>
  <table>
    <tbody>
      <tr>
        <th>${await recurse()}</th>${expander}
      </tr>
    </tbody>
  </table>
</th>`;
  },

  HLine: async ({ props }) => `
<table ${propsToString(props, ['h-line'])}>
  <tbody>
    <tr><th>&nbsp;</th></tr>
  </tbody>
</table>`,

  Row: async ({ recurse, node }): Promise<string> => `
<table ${propsToString(node.props, ['row'])}>
  <tbody>
    <tr>${await recurse()}</tr>
  </tbody>
</table>`,

  Button: async ({ recurse, props, createState }): Promise<string> => {
    const { href, target, ...rest } = props;
    let inner = await recurse();
    let expander = '';

    // If we have the href attribute we can create an anchor for the inner of the button;
    if (href) {
      const linkProps = { href, target };
      if (props.expanded) {
        Object.assign(linkProps, { align: 'center', className: 'float-center' });
      }
      inner = `<a ${propsToString(linkProps)}>${inner}</a>`;
    }

    // If the button is expanded, it needs a <center> tag around the content
    if (props.expanded) {
      inner = await Html.Center(createState('Center', { children: [inner] }));
      rest.className = classString(rest.className ?? '', 'expand');
      expander = '\n<td class="expander"></td>';
    }

    // The .button class is always there, along with any others on the <button> element
    return `
<table ${propsToString(rest, ['button'])}>
  <tbody>
    <tr>
      <td>
        <table>
          <tbody>
            <tr>
              <td>
                ${inner}
              </td>
            </tr>
          </tbody>
        </table>
      </td>${expander}
    </tr>
  </tbody>
</table>
      ${await Html.Spacer(createState('Spacer', { size: 16 }))}`;
  },

  Container: async ({ recurse, props }): Promise<string> => `
<table align="center" ${propsToString(props, ['container'])}>
  <tbody>
    <tr><td>${await recurse()}</td></tr>
  </tbody>
</table>`,

  BlockGrid: async ({ recurse, props }): Promise<string> => `
<table ${propsToString(props, ['block-grid', props.up ? `up-${props.up}` : ''])}>
  <tbody>
    <tr>${await recurse()}</tr>
  </tbody>
</table>`,

  Menu: async ({ recurse, node, props }): Promise<string> => {
    let hasItem = false;
    visit(node, (child) => {
      if (isOfType(child, 'Item')) {
        return hasItem = true;
      } else if ((child.type === 'td' || child.type === 'th') && child.props.className?.includes('menu-item')) {
        return hasItem = true;
      }
    });

    let inner = await recurse();

    if (!hasItem && inner) {
      inner = `<th class="menu-item">${inner}</th>`;
    }

    return `
<table ${propsToString(props, ['menu'])}>
  <tbody>
    <tr>
      <td>
        <table>
          <tbody>
            <tr>
              ${inner}
            </tr>
          </tbody>
        </table>
      </td>
    </tr>
  </tbody>
</table>`;
  },

  Item: async ({ recurse, props }): Promise<string> => {
    const { href, target, ...parentAttrs } = props;
    return `
       <th ${propsToString(parentAttrs, ['menu-item'])}>
         <a ${propsToString({ href, target })}>${await recurse()}</a>
       </th>`;
  },

  Center: async ({ props, recurse, node }): Promise<string> => {
    for (const child of getChildren(node)) {
      Object.assign(child.props, {
        align: 'center',
        className: classString(child.props.className, 'float-center')
      });
    }

    visit(node, child => {
      if (isOfType(child, 'Item')) {
        child.props.className = classString(child.props.className, 'float-center');
      }
      return;
    });

    return `
<center ${propsToString(props)}>
  ${await recurse()}
</center>
    `;
  },

  Callout: async ({ recurse, props }): Promise<string> => {

    const innerProps: JSXElement['props'] = { className: props.className };
    delete props.className;

    return `
<table ${propsToString(props, ['callout'])}>
  <tbody>
    <tr>
      <th ${propsToString(innerProps, ['callout-inner'])}>
        ${await recurse()}
      </th>
      <th class="expander"></th>
    </tr>
  </tbody>
</table>`;
  },

  Spacer: async ({ props }): Promise<string> => {
    const html: string[] = [];
    const buildSpacer = (size: number | string, extraClass: string = ''): string =>
      `
<table ${propsToString(props, ['spacer', extraClass])}>
  <tbody>
    <tr>
      <td height="${size}px" style="font-size:${size}px;line-height:${size}px;">&nbsp;</td>
    </tr>
  </tbody>
</table>
      `;

    const small = props.small ?? undefined;
    const large = props.large ?? undefined;

    if (small || large) {
      if (small) {
        html.push(buildSpacer(small, 'hide-for-large'));
      }
      if (large) {
        html.push(buildSpacer(large, 'show-for-large'));
      }
    } else {
      html.push(buildSpacer(props.size || 16));
    }

    return html.join('\n');
  },

  Wrapper: async ({ recurse, node }) => `
<table align="center" ${propsToString(node.props, ['wrapper'])}>
  <tbody>
    <tr>
      <td class="wrapper-inner">
        ${await recurse()}
      </td>
    </tr>
  </tbody>
</table>`
};