// ============================================================
// LINE Flex Message — Renderer
// Consumes a Flex JSON object and renders to DOM, faithful to LINE's
// own behavior (size/spacing tokens, baseline alignment, button styles).
//
// What's supported:
//  - Containers: bubble (header/hero/body/footer), carousel
//  - Components: box (vertical/horizontal/baseline), text, image, icon,
//    button, separator, spacer, filler
//  - Attrs: size, weight, color, align, gravity, wrap, maxLines, decoration,
//    spacing, margin, paddingAll/Top/Bottom/Start/End, backgroundColor,
//    borderColor, borderWidth, cornerRadius, flex, action, contents
//  - Action types: uri, message, postback, datetimepicker, clipboard
//
// What we intentionally DON'T support (matches LINE limits):
//  - position: absolute, offset* layout (not used in any wireframe here)
//  - styles.header.separator (we render header bg differently for clarity)
// ============================================================

const { useState } = React;

// LINE flex semantics → CSS flex shorthand.
// LINE: flex=0 means "use width / natural size, do not grow".
// CSS:  `flex: 0` resolves to `0 1 0%` → basis=0 → item collapses.
// Map LINE flex=0 → CSS `0 0 auto` (no grow, no shrink, use width/intrinsic).
// Map LINE flex=N(>0) → CSS `N 1 0%` (grow proportionally).
function toCssFlex(v) {
  if (v == null) return undefined;
  if (v === 0) return '0 0 auto';
  return `${v} 1 0%`;
}

// --------------------- Public component ---------------------

window.FlexBubble = function FlexBubble({ node, onAction, isCarouselItem = false }) {
  if (!node) return null;
  if (node.type === 'carousel') {
    return (
      <div className="flex-carousel" style={{
        display: 'flex',
        gap: 8,
        alignItems: 'stretch',
        overflowX: 'auto',
        paddingBottom: 4,
        scrollbarWidth: 'thin',
        maxWidth: '100%',
      }}>
        {node.contents.map((b, i) => (
          <FlexBubble key={i} node={b} onAction={onAction} isCarouselItem />
        ))}
      </div>
    );
  }
  const size = node.size || 'mega';
  const width = window.LINE_BUBBLE_WIDTH[size];
  return (
    <div className="flex-bubble" style={{
      width,
      flexShrink: 0,
      background: '#ffffff',
      borderRadius: 12,
      overflow: 'hidden',
      boxShadow: isCarouselItem
        ? '0 1px 3px rgba(0,0,0,0.10)'
        : '0 2px 8px rgba(0,0,0,0.15)',
      display: 'flex',
      flexDirection: 'column',
      fontFamily: "-apple-system, 'PingFang TC', 'Noto Sans TC', sans-serif",
      lineHeight: 1.4,
      color: '#111',
    }}>
      {node.header && <FlexSection node={node.header} kind="header" onAction={onAction} bubbleWidth={width} />}
      {node.hero && <FlexNode node={node.hero} onAction={onAction} bubbleWidth={width} isHero />}
      {node.body && <FlexSection node={node.body} kind="body" onAction={onAction} bubbleWidth={width} />}
      {node.footer && <FlexSection node={node.footer} kind="footer" onAction={onAction} bubbleWidth={width} />}
    </div>
  );
};

// section default paddings (LINE behavior: header/body/footer each have 20px default padding).
// When user specifies paddingAll/Top/... explicitly on the section, we want THEIR value to apply
// — not the section default. So:
//   - no user override → outer wrapper applies 20px default, inner Box suppresses its own padding
//   - user override    → outer wrapper applies 0, inner Box applies user padding
function FlexSection({ node, kind, onAction, bubbleWidth }) {
  const def = node.paddingAll == null && node.paddingTop == null
    && node.paddingBottom == null && node.paddingStart == null
    && node.paddingEnd == null;
  const defaultPad = def ? window.LINE_SPACE.xl : 0;
  return (
    <div style={{
      padding: defaultPad,
      background: node.backgroundColor,
      borderTop: kind !== 'header' && node.separator ? '1px solid #ebebeb' : undefined,
    }}>
      <FlexNode
        node={{ ...node, _suppressOuterStyle: def }}
        onAction={onAction}
        bubbleWidth={bubbleWidth}
        rootOfSection
      />
    </div>
  );
}

// --------------------- Node dispatch ---------------------

function FlexNode({ node, onAction, bubbleWidth, marginBefore, isInHorizontal, isHero, rootOfSection }) {
  if (!node) return null;
  const props = { node, onAction, bubbleWidth, marginBefore, isInHorizontal, isHero, rootOfSection };
  switch (node.type) {
    case 'box': return <Box {...props} />;
    case 'text': return <Txt {...props} />;
    case 'image': return <Img {...props} />;
    case 'icon': return <Ico {...props} />;
    case 'button': return <Btn {...props} />;
    case 'separator': return <Sep {...props} />;
    case 'spacer': return <Spc {...props} />;
    case 'filler': return <div style={{ flex: 1 }} />;
    default: return null;
  }
}

// --------------------- box ---------------------

function Box({ node, onAction, bubbleWidth, marginBefore, isInHorizontal, rootOfSection }) {
  const layout = node.layout || 'vertical';
  const isBaseline = layout === 'baseline';
  const isHoriz = layout === 'horizontal' || isBaseline;

  const padAll = node.paddingAll != null ? window.resolveSpace(node.paddingAll) : 0;
  const padTop    = node.paddingTop    != null ? window.resolveSpace(node.paddingTop)    : padAll;
  const padBottom = node.paddingBottom != null ? window.resolveSpace(node.paddingBottom) : padAll;
  const padStart  = node.paddingStart  != null ? window.resolveSpace(node.paddingStart)  : padAll;
  const padEnd    = node.paddingEnd    != null ? window.resolveSpace(node.paddingEnd)    : padAll;

  // Style without padding override if section already applied default padding
  const padStyle = node._suppressOuterStyle ? {} : {
    paddingTop: padTop, paddingBottom: padBottom, paddingLeft: padStart, paddingRight: padEnd,
  };

  const style = {
    display: 'flex',
    flexDirection: isHoriz ? 'row' : 'column',
    alignItems: isBaseline
      ? 'baseline'
      : (mapAlignItems(node.alignItems) || (isHoriz ? 'center' : 'stretch')),
    justifyContent: mapJustify(node.justifyContent) || 'flex-start',
    backgroundColor: node.backgroundColor,
    borderRadius: node.cornerRadius != null ? window.resolveSpace(node.cornerRadius) || numberPx(node.cornerRadius) : undefined,
    border: node.borderWidth ? `${numberPx(node.borderWidth)}px solid ${node.borderColor || 'transparent'}` : undefined,
    // LINE default for box child of horizontal layout is flex:0 (not 1).
    // Text / image / icon default to flex:1; box defaults to flex:0.
    flex: toCssFlex(node.flex != null ? node.flex : (isInHorizontal ? 0 : undefined)),
    width: node.width,
    height: node.height,
    cursor: node.action ? 'pointer' : undefined,
    position: 'relative',
    ...padStyle,
  };

  if (isInHorizontal && marginBefore) {
    style.marginLeft = marginBefore;
  } else if (!isInHorizontal && marginBefore) {
    style.marginTop = marginBefore;
  }

  const spacing = node.spacing != null ? window.resolveSpace(node.spacing) : 0;
  const kids = (node.contents || []).filter(Boolean).map((child, i) => {
    const childMargin = child.margin != null
      ? window.resolveSpace(child.margin)
      : (i === 0 ? 0 : spacing);
    return (
      <FlexNode
        key={i}
        node={child}
        onAction={onAction}
        bubbleWidth={bubbleWidth}
        marginBefore={childMargin}
        isInHorizontal={isHoriz}
      />
    );
  });

  const handleClick = () => node.action && onAction && onAction(node.action);

  return (
    <div style={style} onClick={handleClick} className={node.action ? 'flex-pressable' : ''}>
      {kids}
    </div>
  );
}

function mapAlignItems(v) {
  if (v === 'flex-start' || v === 'start') return 'flex-start';
  if (v === 'flex-end' || v === 'end') return 'flex-end';
  if (v === 'center') return 'center';
  return undefined;
}
function mapJustify(v) {
  if (v === 'flex-start' || v === 'start') return 'flex-start';
  if (v === 'flex-end' || v === 'end') return 'flex-end';
  if (v === 'center') return 'center';
  if (v === 'space-between') return 'space-between';
  if (v === 'space-around') return 'space-around';
  if (v === 'space-evenly') return 'space-evenly';
  return undefined;
}
function numberPx(v) {
  if (typeof v === 'number') return v;
  if (typeof v === 'string' && v.endsWith('px')) return parseFloat(v);
  return window.resolveSpace(v) || 0;
}

// --------------------- text ---------------------

function Txt({ node, marginBefore, isInHorizontal, onAction }) {
  const size = window.resolveTextSize(node.size || 'md');
  const weight = node.weight === 'bold' ? 700 : 400;
  const color = node.color || '#111111';
  const align = node.align || (isInHorizontal ? 'start' : 'start');
  const wrap = node.wrap === true;
  const maxLines = node.maxLines;
  const decoration = node.decoration; // none/underline/line-through

  const style = {
    fontSize: size,
    fontWeight: weight,
    color,
    textAlign: mapAlign(align),
    lineHeight: 1.4,
    flex: toCssFlex(node.flex != null ? node.flex : (isInHorizontal ? 1 : undefined)),
    margin: 0,
    cursor: node.action ? 'pointer' : undefined,
    textDecoration: decoration && decoration !== 'none' ? decoration : undefined,
    whiteSpace: wrap ? 'pre-wrap' : 'nowrap',
    overflow: 'hidden',
    textOverflow: wrap ? 'clip' : 'ellipsis',
    minWidth: 0,
  };
  if (isInHorizontal && marginBefore) style.marginLeft = marginBefore;
  if (!isInHorizontal && marginBefore) style.marginTop = marginBefore;
  if (maxLines && wrap) {
    Object.assign(style, {
      display: '-webkit-box',
      WebkitLineClamp: maxLines,
      WebkitBoxOrient: 'vertical',
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      whiteSpace: 'normal',
    });
  }

  const handle = () => node.action && onAction && onAction(node.action);

  return (
    <p style={style} onClick={handle} className={node.action ? 'flex-pressable' : ''}>
      {node.text}
    </p>
  );
}
function mapAlign(a) {
  if (a === 'start') return 'left';
  if (a === 'end') return 'right';
  if (a === 'center') return 'center';
  return a;
}

// --------------------- image ---------------------

function Img({ node, marginBefore, isInHorizontal, bubbleWidth, isHero, onAction }) {
  const w = isHero ? bubbleWidth : window.resolveImgSize(node.size);
  const aspectRatio = node.aspectRatio
    ? node.aspectRatio.replace(':', ' / ')
    : (isHero ? '1.51 / 1' : undefined);
  const style = {
    width: typeof w === 'number' ? w : w,
    aspectRatio,
    objectFit: node.aspectMode === 'cover' ? 'cover' : 'contain',
    background: node.backgroundColor || '#f0f0f0',
    display: 'block',
    cursor: node.action ? 'pointer' : undefined,
  };
  if (isInHorizontal && marginBefore) style.marginLeft = marginBefore;
  if (!isInHorizontal && marginBefore) style.marginTop = marginBefore;
  const handle = () => node.action && onAction && onAction(node.action);
  // Use img element so users can drop in real urls.
  if (node.url) {
    return <img src={node.url} style={style} alt="" onClick={handle} />;
  }
  // Placeholder colored box
  return (
    <div style={{
      ...style,
      background: node.backgroundColor || '#e6e6e6',
      color: '#888',
      fontSize: 11,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }} onClick={handle}>
      {node.placeholder || 'image'}
    </div>
  );
}

// --------------------- icon ---------------------

function Ico({ node, marginBefore, isInHorizontal }) {
  const w = window.resolveIconSize(node.size);
  const style = {
    width: w,
    height: w,
    display: 'inline-block',
    flexShrink: 0,
    alignSelf: 'center',
  };
  if (isInHorizontal && marginBefore) style.marginLeft = marginBefore;
  if (!isInHorizontal && marginBefore) style.marginTop = marginBefore;
  if (node.url) {
    return <img src={node.url} style={style} alt="" />;
  }
  return <span style={{ ...style, background: '#ccc', borderRadius: 2 }} />;
}

// --------------------- button ---------------------

function Btn({ node, marginBefore, isInHorizontal, onAction }) {
  const style = node.style || 'link'; // link / primary / secondary
  const height = window.LINE_BTN_HEIGHT[node.height || 'md'];
  const action = node.action || {};
  const label = action.label || '';

  let bg, fg, border;
  if (style === 'primary') {
    bg = node.color || '#06c755'; // LINE green default
    fg = '#ffffff';
    border = 'transparent';
  } else if (style === 'secondary') {
    bg = node.color || '#dcdfe5';
    fg = '#000000DE';
    border = 'transparent';
  } else { // link
    bg = 'transparent';
    fg = node.color || '#42659a';
    border = 'transparent';
  }

  const css = {
    background: bg,
    color: fg,
    border: `1px solid ${border}`,
    borderRadius: 6,
    height,
    minHeight: height,
    padding: '0 10px',
    fontSize: 14,
    fontWeight: 500,
    cursor: 'pointer',
    fontFamily: 'inherit',
    width: '100%',
    flex: toCssFlex(isInHorizontal ? (node.flex != null ? node.flex : 1) : undefined),
    whiteSpace: 'nowrap',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  };
  if (isInHorizontal && marginBefore) css.marginLeft = marginBefore;
  if (!isInHorizontal && marginBefore) css.marginTop = marginBefore;
  return (
    <button style={css} className="flex-btn" onClick={() => onAction && onAction(action)}>
      {label}
    </button>
  );
}

// --------------------- separator ---------------------

function Sep({ node, marginBefore, isInHorizontal }) {
  const color = node.color || '#ebebeb';
  const style = isInHorizontal
    ? { width: 1, alignSelf: 'stretch', background: color, marginLeft: marginBefore || 0 }
    : { height: 1, alignSelf: 'stretch', background: color, marginTop: marginBefore || 0 };
  return <div style={style} />;
}

// --------------------- spacer (deprecated but supported) ---------------------

function Spc({ node }) {
  const s = window.resolveSpace(node.size || 'md');
  return <span style={{ display: 'inline-block', width: s, height: s }} />;
}

// Make pressables show a press feedback
const sheet = document.createElement('style');
sheet.textContent = `
.flex-pressable:active { opacity: 0.6; }
.flex-btn:active { filter: brightness(0.94); }
.flex-btn { transition: filter 80ms ease-out; }
`;
document.head.appendChild(sheet);
