import { CSSProperties } from 'react';
import { escapeAttr, safeUrl } from './escape';

const camelToKebab = (str: string): string =>
  str.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());

const URL_RE = /url\(\s*(['"]?)([\s\S]*?)\1\s*\)/gi;

// Outside of a url(...) reference, a `;` is never legitimate (declarations
// are separated by it) -- stray semicolons are how a breakout injects a
// second property -- and a raw `"` would close the `style="..."` attribute
// early. Inside url('...') the content has already been made safe via
// escapeAttr(safeUrl(...)), including any `;` required by data-URI syntax
// (`data:<mime>;base64,<payload>`), so this must never be applied there.
const sanitizeBreakoutChars = (s: string): string => s.replace(/;/g, '').replace(/"/g, '&quot;');

/**
 * Sanitizes a single CSS declaration value so it can never terminate the
 * `style="..."` attribute early, inject an extra declaration via a stray
 * `;`, or smuggle a `javascript:`/`vbscript:`/`data:text/html` URL through
 * a `url(...)` reference. Legitimate multi-part values (box-shadow,
 * gradients, etc.) that contain none of these characters pass through
 * unchanged, and legitimate `;`-containing data-URIs inside url(...) are
 * preserved intact.
 */
function sanitizeCssValue(raw: string): string {
  let out = '';
  let lastIndex = 0;
  URL_RE.lastIndex = 0;
  let m: RegExpExecArray | null;
  while ((m = URL_RE.exec(raw)) !== null) {
    // Sanitize breakout characters only in the segment before this url(...)
    // reference -- never inside the reference itself.
    out += sanitizeBreakoutChars(raw.slice(lastIndex, m.index));
    // Neutralize the url(...) reference: validate/strip the scheme and
    // re-wrap in single quotes with the contents escaped for attribute
    // safety. This is already fully safe, `;` and all.
    const inner = m[2];
    out += `url('${escapeAttr(safeUrl(inner.trim()))}')`;
    lastIndex = URL_RE.lastIndex;
  }
  out += sanitizeBreakoutChars(raw.slice(lastIndex));
  return out;
}

export function cssPropsToString(style: CSSProperties | undefined): string {
  if (!style) return '';
  return Object.entries(style)
    .filter(([, v]) => v !== undefined && v !== null && v !== '')
    .map(([k, v]) => `${camelToKebab(k)}:${typeof v === 'string' ? sanitizeCssValue(v) : v}`)
    .join(';');
}

export function mergeStyles(...styles: (CSSProperties | undefined)[]): CSSProperties {
  return Object.assign({}, ...styles.filter(Boolean));
}
