#include <metal_stdlib>
using namespace metal;

#define M_PI_F 3.14159265358979323846f

struct VertexOut {
  float4 position [[position]];
  float2 uv;
  float4 color;
};

struct ShapeParams {
  float4 fill_color;
  float4 stroke_color;
  float2 size;
  float radius;
  float feather;
  float stroke_thickness;
  float stroke_align;
  int shape_type;      // 0=rect (rounded via radius), 1=circle, 2=ellipse, 3=pill
  int corner_style;
  float dash_len;
  float gap_len;
  float dash_offset;
  int cap_type;        // 0=butt, 1=square-ish, 2=round-ish
  float4 uv_transform;
  float uv_rotate;
  int has_texture;
  float opacity;
  float _pad;
};

// -------- SDFs --------

float sdf_rounded_rect(float2 p, float2 b, float r) {
  float2 q = abs(p) - b + r;
  return min(max(q.x, q.y), 0.0f) + length(max(q, 0.0f)) - r;
}

float sdf_circle(float2 p, float r) {
  return length(p) - r;
}

float sdf_ellipse(float2 p, float2 ab) {
  float2 pn = p / ab;
  float d = length(pn) - 1.0f;
  return d * min(ab.x, ab.y);
}

float sdf_pill(float2 p, float2 b) {
  float r = min(b.x, b.y);
  float2 q = abs(p) - float2(b.x - r, b.y - r);
  return length(max(q, 0.0f)) + min(max(q.x, q.y), 0.0f) - r;
}

float compute_sdf(float2 p, float2 half_size, int shape_type, float radius) {
  if (shape_type == 0) {
    float r = min(radius, min(half_size.x, half_size.y));
    return sdf_rounded_rect(p, half_size, r);
  }
  else if (shape_type == 1) {
    float r = min(half_size.x, half_size.y);
    return sdf_circle(p, r);
  }
  else if (shape_type == 2) {
    return sdf_ellipse(p, half_size);
  }
  else {
    return sdf_pill(p, half_size);
  }
}

// -------- Helpers --------

float wrap_angle_0_2pi(float a) {
  float w = fmod(a, 2.0f * M_PI_F);
  if (w < 0.0f) w += 2.0f * M_PI_F;
  return w;
}

// Project arbitrary p to ellipse boundary (stable and cheap)
// ab are ellipse radii (half_size.x, half_size.y)
float2 ellipse_project_boundary(float2 p, float2 ab) {
  float2 safe_ab = max(ab, float2(1e-6f));
  float2 pn = p / safe_ab;
  float len_pn = length(pn);
  if (len_pn < 1e-6f) return float2(ab.x, 0.0f);
  float2 dir = pn / len_pn;
  return dir * ab;
}

// Numeric arc length from 0..theta for ellipse with radii a,b using midpoint rule.
float ellipse_arclen_0_theta(float a, float b, float theta) {
  const int N = 12;
  float t = 0.0f;
  float dt = theta / float(N);
  float sum = 0.0f;
  for (int i = 0; i < N; i++) {
    float tm = t + 0.5f * dt;
    float s = sin(tm);
    float c = cos(tm);
    float ds = sqrt(a * a * s * s + b * b * c * c);
    sum += ds * dt;
    t += dt;
  }
  return sum;
}

float ellipse_perimeter_ramanujan(float a, float b) {
  float apb = a + b;
  float h = (a - b) * (a - b) / max(apb * apb, 1e-6f);
  return M_PI_F * apb * (1.0f + (3.0f * h) / (10.0f + sqrt(max(4.0f - 3.0f * h, 1e-6f))));
}

// Rounded-rect boundary projection (for stable dash phase across stroke width)
float2 rounded_rect_project_boundary(float2 p, float2 half_size, float r) {
  float2 inner = max(half_size - r, float2(0.0f));
  float2 ap = abs(p);

  if (ap.x > inner.x && ap.y > inner.y && r > 0.0f) {
    float sx = (p.x < 0.0f) ? -1.0f : 1.0f;
    float sy = (p.y < 0.0f) ? -1.0f : 1.0f;
    float2 c = float2(inner.x * sx, inner.y * sy);
    float2 v = p - c;
    float lv = length(v);
    if (lv < 1e-6f) return c + float2(r, 0.0f);
    return c + v * (r / lv);
  }

  float dx = half_size.x - ap.x;
  float dy = half_size.y - ap.y;

  if (dx < dy) {
    float sx = (p.x < 0.0f) ? -1.0f : 1.0f;
    return float2(sx * half_size.x, clamp(p.y, -inner.y, inner.y));
  }
  float sy = (p.y < 0.0f) ? -1.0f : 1.0f;
  return float2(clamp(p.x, -inner.x, inner.x), sy * half_size.y);
}

// Rounded-rect perimeter distance along boundary, starting at +X mid, CCW.
float rounded_rect_dist_along(float2 pb, float2 half_size, float r) {
  float2 inner = max(half_size - r, float2(0.0f));
  float ix = inner.x;
  float iy = inner.y;

  float seg_right_up = iy;
  float seg_arc = 0.5f * M_PI_F * r;
  float seg_top = 2.0f * ix;
  float seg_left = 2.0f * iy;
  float seg_bottom = 2.0f * ix;

  float ax = abs(pb.x);
  float ay = abs(pb.y);

  // Right edge
  if (pb.x > 0.0f && ay <= iy + 1e-5f) {
    if (pb.y >= 0.0f) return pb.y;
    float tail = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom + seg_arc;
    return tail + (pb.y + iy);
  }

  // Top edge
  if (pb.y > 0.0f && ax <= ix + 1e-5f) {
    float s0 = seg_right_up + seg_arc;
    return s0 + (ix - pb.x);
  }

  // Left edge
  if (pb.x < 0.0f && ay <= iy + 1e-5f) {
    float s0 = seg_right_up + seg_arc + seg_top + seg_arc;
    return s0 + (iy - pb.y);
  }

  // Bottom edge
  if (pb.y < 0.0f && ax <= ix + 1e-5f) {
    float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc;
    return s0 + (pb.x + ix);
  }

  // Corners
  if (pb.x > 0.0f && pb.y > 0.0f) {
    float2 c = float2(ix, iy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = clamp(a, 0.0f, 0.5f * M_PI_F);
    return seg_right_up + r * local;
  }
  if (pb.x < 0.0f && pb.y > 0.0f) {
    float2 c = float2(-ix, iy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = clamp(M_PI_F - a, 0.0f, 0.5f * M_PI_F);
    float s0 = seg_right_up + seg_arc + seg_top;
    return s0 + r * local;
  }
  if (pb.x < 0.0f && pb.y < 0.0f) {
    float2 c = float2(-ix, -iy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = clamp(a - M_PI_F, 0.0f, 0.5f * M_PI_F);
    float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left;
    return s0 + r * local;
  }

  // Bottom-right
  {
    float2 c = float2(ix, -iy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = 0.0f;
    if (a >= 1.5f * M_PI_F) local = clamp(a - 1.5f * M_PI_F, 0.0f, 0.5f * M_PI_F);
    float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom;
    return s0 + r * local;
  }
}

// -------- Pill helpers --------

float pill_perimeter(float2 half_size) {
  float r = min(half_size.x, half_size.y);
  float major = max(half_size.x, half_size.y);
  float straight = max(major - r, 0.0f);
  return 4.0f * straight + 2.0f * M_PI_F * r;
}

float2 pill_project_boundary(float2 p, float2 half_size) {
  float r = min(half_size.x, half_size.y);
  bool horiz = (half_size.x >= half_size.y);
  if (horiz) {
    float cx = max(half_size.x - r, 0.0f);
    float2 c = float2((p.x >= 0.0f ? cx : -cx), 0.0f);
    float2 v = p - c;
    float lv = length(v);
    if (lv < 1e-6f) return c + float2(r, 0.0f);
    return c + v * (r / lv);
  }
  float cy = max(half_size.y - r, 0.0f);
  float2 c = float2(0.0f, (p.y >= 0.0f ? cy : -cy));
  float2 v = p - c;
  float lv = length(v);
  if (lv < 1e-6f) return c + float2(r, 0.0f);
  return c + v * (r / lv);
}

float pill_dist_along(float2 pb, float2 half_size) {
  float r = min(half_size.x, half_size.y);
  bool horiz = (half_size.x >= half_size.y);

  if (horiz) {
    float cx = max(half_size.x - r, 0.0f);

    if (pb.x > cx - 1e-5f) {
      float2 c = float2(cx, 0.0f);
      float2 v = pb - c;
      float a = wrap_angle_0_2pi(atan2(v.y, v.x));
      float local = clamp(a, 0.0f, M_PI_F);
      return r * local;
    }

    if (abs(pb.y - r) <= 1e-3f && abs(pb.x) <= cx + 1e-3f) {
      float s0 = M_PI_F * r;
      return s0 + (cx - pb.x);
    }

    if (pb.x < -cx + 1e-5f) {
      float2 c = float2(-cx, 0.0f);
      float2 v = pb - c;
      float a = wrap_angle_0_2pi(atan2(v.y, v.x));
      float local = clamp(a - M_PI_F, 0.0f, M_PI_F);
      float s0 = M_PI_F * r + 2.0f * cx;
      return s0 + r * local;
    }

    float s0 = M_PI_F * r + 2.0f * cx + M_PI_F * r;
    return s0 + (pb.x + cx);
  }

  float cy = max(half_size.y - r, 0.0f);

  if (pb.y > cy - 1e-5f) {
    float2 c = float2(0.0f, cy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = clamp(a, 0.0f, M_PI_F);
    return r * local;
  }

  if (abs(pb.x + r) <= 1e-3f && abs(pb.y) <= cy + 1e-3f) {
    float s0 = M_PI_F * r;
    return s0 + (cy - pb.y);
  }

  if (pb.y < -cy + 1e-5f) {
    float2 c = float2(0.0f, -cy);
    float2 v = pb - c;
    float a = wrap_angle_0_2pi(atan2(v.y, v.x));
    float local = clamp(a - M_PI_F, 0.0f, M_PI_F);
    float s0 = M_PI_F * r + 2.0f * cy;
    return s0 + r * local;
  }

  float s0 = M_PI_F * r + 2.0f * cy + M_PI_F * r;
  return s0 + (pb.y + cy);
}

// -------- Perimeter mapping for dashes --------

float perimeter_dist_along(float2 p, float2 half_size, int shape_type, float radius) {
  if (shape_type == 1) {
    float r = min(half_size.x, half_size.y);
    float2 pb = (length(p) < 1e-6f) ? float2(r, 0.0f) : normalize(p) * r;
    float theta = wrap_angle_0_2pi(atan2(pb.y, pb.x));
    return r * theta;
  }

  if (shape_type == 2) {
    float a = max(half_size.x, 1e-6f);
    float b = max(half_size.y, 1e-6f);
    float2 pb = ellipse_project_boundary(p, float2(a, b));
    float theta = wrap_angle_0_2pi(atan2(pb.y / b, pb.x / a));
    return ellipse_arclen_0_theta(a, b, theta);
  }

  if (shape_type == 3) {
    float2 pb = pill_project_boundary(p, half_size);
    return pill_dist_along(pb, half_size);
  }

  float r = min(radius, min(half_size.x, half_size.y));
  float2 pb = rounded_rect_project_boundary(p, half_size, r);
  return rounded_rect_dist_along(pb, half_size, r);
}

float perimeter_total(float2 half_size, int shape_type, float radius) {
  if (shape_type == 1) {
    float r = min(half_size.x, half_size.y);
    return 2.0f * M_PI_F * r;
  }
  if (shape_type == 2) {
    float a = max(half_size.x, 1e-6f);
    float b = max(half_size.y, 1e-6f);
    return ellipse_perimeter_ramanujan(a, b);
  }
  if (shape_type == 3) {
    return pill_perimeter(half_size);
  }
  float r = min(radius, min(half_size.x, half_size.y));
  float2 inner = max(half_size - r, float2(0.0f));
  return 4.0f * (inner.x + inner.y) + 2.0f * M_PI_F * r;
}

float dash_mask_perimeter(float dist_along, float perimeter,
                          float dash_len, float gap_len, float dash_offset,
                          int cap_type, float aa) {
  if (dash_len <= 0.0f) return 1.0f;

  float pattern_len = max(dash_len + gap_len, 1e-6f);
  float s = dist_along + dash_offset;

  float pwrap = max(perimeter, 1e-6f);
  float s_wrap = fmod(s, pwrap);
  if (s_wrap < 0.0f) s_wrap += pwrap;

  float pos = fmod(s_wrap, pattern_len);
  if (pos < 0.0f) pos += pattern_len;

  if (cap_type == 0) {
    return pos < dash_len ? 1.0f : 0.0f;
  }

  if (cap_type == 1) {
    float edge = dash_len;
    return 1.0f - smoothstep(edge - aa, edge + aa, pos);
  }

  float half_dash = dash_len * 0.5f;
  float center = half_dash;
  float d = abs(pos - center);
  return smoothstep(half_dash + aa, half_dash - aa, d);
}

// -------- Fragment --------

fragment float4 fragment_main(VertexOut in [[stage_in]],
                              constant ShapeParams& params [[buffer(0)]],
                              texture2d<float> fill_texture [[texture(0)]],
                              sampler smp [[sampler(0)]]) {
  float2 uv = in.uv;

  float2 p = (uv - 0.5f) * params.size;
  float2 half_size = params.size * 0.5f;

  float d = compute_sdf(p, half_size, params.shape_type, params.radius);

  float aa = max(params.feather, fwidth(d));

  float fill_alpha = 0.0f;
  float stroke_alpha = 0.0f;

  if (params.stroke_thickness > 0.0f) {
    float inner_offset, outer_offset;

    if (params.stroke_align < 0.25f) {
      inner_offset = -params.stroke_thickness;
      outer_offset = 0.0f;
    }
    else if (params.stroke_align > 0.75f) {
      inner_offset = 0.0f;
      outer_offset = params.stroke_thickness;
    }
    else {
      inner_offset = -params.stroke_thickness * 0.5f;
      outer_offset = params.stroke_thickness * 0.5f;
    }

    float fill_d = d - inner_offset;
    fill_alpha = 1.0f - smoothstep(-aa, aa, fill_d);

    float start = smoothstep(-aa, aa, d - inner_offset);
    float end = 1.0f - smoothstep(-aa, aa, d - outer_offset);
    float in_stroke = start * end;

    float perim = perimeter_total(half_size, params.shape_type, params.radius);
    float dist_along = perimeter_dist_along(p, half_size, params.shape_type, params.radius);
    float dash = dash_mask_perimeter(dist_along, perim,
                                     params.dash_len, params.gap_len, params.dash_offset,
                                     params.cap_type, aa);

    stroke_alpha = in_stroke * dash;
  }
  else {
    fill_alpha = 1.0f - smoothstep(-aa, aa, d);
  }

  float4 fill_col = params.fill_color;
  if (params.has_texture > 0) {
    float2 tex_uv = uv;
    tex_uv = tex_uv * params.uv_transform.xy + params.uv_transform.zw;

    if (abs(params.uv_rotate) > 0.001f) {
      float2 center = float2(0.5f, 0.5f);
      tex_uv -= center;
      float c = cos(params.uv_rotate);
      float s = sin(params.uv_rotate);
      tex_uv = float2(tex_uv.x * c - tex_uv.y * s, tex_uv.x * s + tex_uv.y * c);
      tex_uv += center;
    }

    float4 tex_col = fill_texture.sample(smp, tex_uv);
    fill_col *= tex_col;
  }

  float4 fill_out = fill_col;
  fill_out.a *= fill_alpha;

  float4 stroke_out = params.stroke_color;
  stroke_out.a *= stroke_alpha;

  float sa = stroke_out.a;
  float fa = fill_out.a;

  float3 rgb_premul = stroke_out.rgb * sa + fill_out.rgb * fa * (1.0f - sa);
  float a = sa + fa * (1.0f - sa);

  float4 result = (a > 1e-6f) ? float4(rgb_premul / a, a) : float4(0.0f);

  result *= in.color;
  result.a *= params.opacity;

  return result;
}
