import type { Wall, WallOpening, ElectricalItem, FurnitureItem, Point } from '@house-plan-maker/shared';
import { rotatedAnchorOffsetToCenter } from '@house-plan-maker/shared';
import { wallLength, wallStartEnd } from './wallUtils';

// ── Projection axis (canonical orientation for elevation views) ──

/**
 * Pick a canonical orientation for the projection X axis so that
 * left-to-right in the elevation view matches the viewer's perspective
 * when standing **inside** the room looking at the wall:
 *
 *   NORTH wall → left = west,  right = east  → +X axis
 *   SOUTH wall → left = east,  right = west  → −X axis
 *   EAST  wall → left = north, right = south → +Y axis
 *   WEST  wall → left = south, right = north → −Y axis
 *
 * Diagonal / OTHER walls keep their natural start→end orientation.
 *
 * @returns the canonical start, end, length and whether the axis is flipped
 *          relative to the wall's stored start→end.
 */
export interface ProjectionAxis {
  readonly start: Point;
  readonly end: Point;
  readonly length: number;
  /** True when the canonical axis runs opposite the wall's stored start→end. */
  readonly flipped: boolean;
}

export function getProjectionAxis(wall: Wall): ProjectionAxis {
  const { start, end } = wallStartEnd(wall);
  const length = wallLength(wall);
  const dx = end.x - start.x;
  const dy = end.y - start.y;
  const ax = Math.abs(dx);
  const ay = Math.abs(dy);
  const isHorizontal = ax >= ay;

  // SOUTH and WEST walls need the negative axis direction (the viewer
  // faces the negative direction and sees the positive side on the left).
  // NORTH and EAST walls need the positive axis direction (current default).
  // OTHER / diagonal walls keep start→end as-is.
  let flipped: boolean;
  if (wall.direction === 'SOUTH' || wall.direction === 'WEST') {
    flipped = isHorizontal ? dx > 0 : dy > 0;
  } else {
    flipped = isHorizontal ? dx < 0 : dy < 0;
  }

  if (flipped) {
    return { start: end, end: start, length, flipped: true };
  }
  return { start, end, length, flipped: false };
}

// ── Constants ──

/** Standard door height in meters. */
export const STANDARD_DOOR_HEIGHT = 2.1;

/** Default elevation for electrical items when elevationFromFloor is null. */
export const DEFAULT_ELEVATIONS: Record<string, number> = {
  OUTLET: 0.3,
  SWITCH: 1.2,
  LIGHT_WALL: 2.0,
  JUNCTION_BOX: 0.3,
};

/** Electrical types that are always wall-mounted. */
export const WALL_MOUNTED_ELECTRICAL_TYPES = new Set([
  'OUTLET',
  'SWITCH',
  'LIGHT_WALL',
  'JUNCTION_BOX',
]);

/** Wall-mounted furniture types that should appear in projection. */
const WALL_MOUNTED_FURNITURE = new Set(['SHELF', 'BOOKCASE', 'WARDROBE']);

// ── Types ──

/** A projected item's position in the wall elevation coordinate system. */
export interface ProjectedPosition {
  /** Horizontal position along the wall from start, in meters. */
  readonly alongWall: number;
  /** Vertical position from the floor, in meters. */
  readonly fromFloor: number;
}

/** A projected rectangle in wall elevation coordinates. */
export interface ProjectedRect {
  /** Left edge position along wall, in meters. */
  readonly x: number;
  /** Bottom edge distance from floor, in meters. */
  readonly y: number;
  /** Width along wall, in meters. */
  readonly width: number;
  /** Height from bottom to top, in meters. */
  readonly height: number;
}

/** Information about an opening projected onto a wall. */
export interface ProjectedOpening {
  readonly opening: WallOpening;
  readonly rect: ProjectedRect;
}

/** Information about an electrical item projected onto a wall. */
export interface ProjectedElectrical {
  readonly item: ElectricalItem;
  readonly position: ProjectedPosition;
  readonly elevation: number;
  /**
   * True when the wall's stored start→end direction is opposite the
   * canonical projection axis (see `getProjectionAxis`). The renderer uses
   * this to mirror the horizontal anchor label: "left" in the canonical
   * projection view corresponds to "right" in the wall's natural frame
   * (which is what 3D uses). Without this flag, a left-anchored outlet on
   * a flipped wall would appear on opposite sides of the 3D and projection
   * views.
   */
  readonly axisFlipped: boolean;
}

/** Information about a furniture item projected onto a wall. */
export interface ProjectedFurniture {
  readonly item: FurnitureItem;
  readonly rect: ProjectedRect;
}

/** Plinth segment (may be interrupted by door openings). */
export interface PlinthSegment {
  readonly x: number;
  readonly width: number;
  readonly height: number;
}

// ── Coordinate Mapping ──

/** Scale factor for converting meters to projection-view pixels. */
export function projectionScale(
  wallLengthM: number,
  wallHeightM: number,
  viewWidth: number,
  viewHeight: number,
  padding: number = 40,
): number {
  const availableWidth = viewWidth - padding * 2;
  const availableHeight = viewHeight - padding * 2;
  if (wallLengthM <= 0 || wallHeightM <= 0) return 1;
  return Math.min(availableWidth / wallLengthM, availableHeight / wallHeightM);
}

/** Convert projection coordinates (meters) to view pixels. */
export function projectionToPixel(
  alongWall: number,
  fromFloor: number,
  wallHeightM: number,
  scale: number,
  padding: number = 40,
): { x: number; y: number } {
  return {
    x: padding + alongWall * scale,
    // Y is inverted: floor is at bottom, ceiling at top
    y: padding + (wallHeightM - fromFloor) * scale,
  };
}

/** Convert view pixels back to projection coordinates (meters). Inverse of projectionToPixel. */
export function pixelToProjection(
  pixelX: number,
  pixelY: number,
  wallHeightM: number,
  scale: number,
  padding: number = 40,
): { alongWall: number; fromFloor: number } {
  return {
    alongWall: (pixelX - padding) / scale,
    fromFloor: wallHeightM - (pixelY - padding) / scale,
  };
}

// ── Opening Projection ──

/** Get all openings that belong to a given wall, projected into wall elevation coords. */
export function projectOpenings(
  wall: Wall,
  openings: readonly WallOpening[],
): readonly ProjectedOpening[] {
  const axis = getProjectionAxis(wall);
  const wallLen = axis.length;
  return openings
    .filter((o) => o.wallId === wall.id)
    .map((opening) => {
      const halfWidth = opening.width / 2;
      // Map storage position (relative to wall.start) to projection position
      // (relative to canonical start). When the axis is flipped, mirror it.
      const projectedCenter = axis.flipped
        ? wallLen - opening.positionAlongWall
        : opening.positionAlongWall;
      const leftEdge = projectedCenter - halfWidth;

      const isDoor = opening.type === 'DOOR';
      const fromFloor = isDoor ? 0 : opening.elevationFromFloor;
      const height = isDoor ? STANDARD_DOOR_HEIGHT : opening.height;

      return {
        opening,
        rect: {
          x: Math.max(0, leftEdge),
          y: fromFloor,
          width: Math.min(opening.width, wallLen - Math.max(0, leftEdge)),
          height,
        },
      };
    });
}

// ── Electrical Projection ──

/** Project wall-mounted electrical items onto a wall's elevation. */
export function projectElectricalItems(
  wall: Wall,
  electricalItems: readonly ElectricalItem[],
  allWalls?: readonly Wall[],
): readonly ProjectedElectrical[] {
  const axis = getProjectionAxis(wall);
  const { start, end, length: wallLen } = axis;

  if (wallLen === 0) return [];

  const dx = (end.x - start.x) / wallLen;
  const dy = (end.y - start.y) / wallLen;

  const PROXIMITY_THRESHOLD = 0.3;

  /** Perpendicular distance from an item to a wall's line, clamped to the
   *  wall's span so a corner-adjacent item doesn't count as "on" the
   *  neighbouring wall. */
  const perpDistToWall = (item: ElectricalItem, w: Wall): number => {
    const a = getProjectionAxis(w);
    if (a.length === 0) return Infinity;
    const wdx = (a.end.x - a.start.x) / a.length;
    const wdy = (a.end.y - a.start.y) / a.length;
    const vx = item.x - a.start.x;
    const vy = item.y - a.start.y;
    const along = vx * wdx + vy * wdy;
    if (along < -0.05 || along > a.length + 0.05) return Infinity;
    return Math.abs(vx * -wdy + vy * wdx);
  };

  // Build a set of live wall ids. A wallId that doesn't appear here is
  // stale (wall ids are reassigned on bulk-save), so we treat the item as
  // orphan and fall back to proximity instead of dropping it.
  const liveWallIds = allWalls ? new Set(allWalls.map((w) => w.id)) : null;

  return electricalItems
    .filter((item) => {
      // Explicit match wins.
      if (item.wallId === wall.id) return true;
      // Explicit mismatch to a LIVE wall: trust the author, not us.
      if (item.wallId && (!liveWallIds || liveWallIds.has(item.wallId))) return false;

      // Orphan or stale-wallId item: fall back to proximity, but assign
      // it to the SINGLE closest wall so a corner item doesn't project
      // onto both adjacent walls.
      if (!WALL_MOUNTED_ELECTRICAL_TYPES.has(item.type)) return false;
      const thisDist = perpDistToWall(item, wall);
      if (thisDist >= PROXIMITY_THRESHOLD) return false;
      if (allWalls && allWalls.length > 1) {
        for (const other of allWalls) {
          if (other.id === wall.id) continue;
          const otherDist = perpDistToWall(item, other);
          if (otherDist < thisDist - 1e-6) return false;
        }
      }
      return true;
    })
    .map((item) => {
      // Project item position onto wall axis
      const vx = item.x - start.x;
      const vy = item.y - start.y;
      const alongWall = vx * dx + vy * dy;

      const elevation =
        item.elevationFromFloor ?? DEFAULT_ELEVATIONS[item.type] ?? 1.0;

      return {
        item,
        position: { alongWall: Math.max(0, Math.min(wallLen, alongWall)), fromFloor: elevation },
        elevation,
        axisFlipped: axis.flipped,
      };
    });
}

// ── Furniture Projection ──

/**
 * Project the ROTATED half-extents of a furniture item onto the wall axis
 * and the wall-perpendicular axis.
 *
 * Rotation pivots around the rectangle centre, so the local axes become:
 *   u = ( cos r,  sin r)   // width direction in world coords
 *   v = (-sin r,  cos r)   // depth direction in world coords
 *
 * Projecting (w/2)·u and (d/2)·v onto the wall direction (and its
 * perpendicular) gives the support of the rotated bounding box along each
 * axis. This is what the user sees as the silhouette in 3D and top-down
 * views, so the projection view on the wall elevation must match it.
 *
 * History: an earlier iteration used unrotated extents under the rule
 * "rotation only affects visuals and hitbox, positioning uses the initial
 * coordinate system". That turned out to be wrong for projection because
 * projection-onto-a-wall IS a visual operation — it literally asks "what
 * does the rotated shape look like from this wall?" — so rotated extents
 * are the correct answer.
 */
function rotatedHalfExtents(
  item: FurnitureItem,
  wallDirX: number,
  wallDirY: number,
): { halfAlong: number; halfPerp: number } {
  const r = ((item.rotation ?? 0) * Math.PI) / 180;
  const cos = Math.cos(r);
  const sin = Math.sin(r);
  const halfW = item.width / 2;
  const halfD = item.depth / 2;

  // Rotated half-axis vectors in world space.
  const ux = cos * halfW;
  const uy = sin * halfW;
  const vx = -sin * halfD;
  const vy = cos * halfD;

  // Wall-perpendicular unit vector (rotated 90° CCW from the wall direction).
  const perpX = -wallDirY;
  const perpY = wallDirX;

  const halfAlong =
    Math.abs(ux * wallDirX + uy * wallDirY) + Math.abs(vx * wallDirX + vy * wallDirY);
  const halfPerp =
    Math.abs(ux * perpX + uy * perpY) + Math.abs(vx * perpX + vy * perpY);

  return { halfAlong, halfPerp };
}

/**
 * Distance from the nearest edge of a furniture item to a wall. Uses the
 * ROTATED bounding box so that a rotated item parallel to the wall is
 * picked up by the proximity filter when its edge is close, not when its
 * unrotated footprint happens to touch.
 */
function furnitureEdgeDistanceToWall(
  item: FurnitureItem,
  wall: Wall,
): number {
  const { start, end } = wallStartEnd(wall);
  const wallLen = wallLength(wall);
  if (wallLen === 0) return Infinity;

  const dx = (end.x - start.x) / wallLen;
  const dy = (end.y - start.y) / wallLen;

  // (item.x, item.y) is the anchored point on the rotated bounding box.
  // Convert to box centre.
  const offset = rotatedAnchorOffsetToCenter(
    item.positionAnchor,
    item.width,
    item.depth,
    item.rotation,
  );
  const cx = item.x + offset.dx;
  const cy = item.y + offset.dy;

  // Vector from wall start to item centre
  const dxC = cx - start.x;
  const dyC = cy - start.y;

  // Perpendicular distance from centre to wall line
  const centerDist = Math.abs(dxC * (-dy) + dyC * dx);

  const { halfPerp } = rotatedHalfExtents(item, dx, dy);
  const edgeDist = Math.max(0, centerDist - halfPerp);

  // Along-wall extent: the item's CENTRE must lie within this wall's span
  // (with a small tolerance). Using the rotated half-width as tolerance
  // here picked up items sitting on perpendicular neighbour walls whose
  // corner happened to be close to this wall's extended line — they then
  // got projected into the elevation view as if they belonged to this wall.
  const alongWallCenter = dxC * dx + dyC * dy;
  const alongTol = 0.1;
  if (alongWallCenter < -alongTol || alongWallCenter > wallLen + alongTol) {
    return Infinity;
  }

  return edgeDist;
}

/** Project furniture items that are near a wall into elevation coords.
 *
 * When `allWalls` is provided, each furniture item is assigned to the
 * single wall that minimizes its edge-distance (its "home" wall) — items
 * that are closer to a different wall are excluded from this wall's
 * projection even if they happen to be within `wallThreshold` of it. This
 * removes neighbour-wall leakage in corners (e.g. a wardrobe on the east
 * wall that sits flush in the NE corner used to show up on the north wall
 * projection too).
 */
export function projectFurnitureItems(
  wall: Wall,
  furnitureItems: readonly FurnitureItem[],
  wallThreshold: number = 0.15,
  allWalls?: readonly Wall[],
): readonly ProjectedFurniture[] {
  const axis = getProjectionAxis(wall);
  const { start, end, length: wallLen } = axis;
  if (wallLen === 0) return [];

  const dx = (end.x - start.x) / wallLen;
  const dy = (end.y - start.y) / wallLen;

  return furnitureItems
    .filter((item) => {
      const dist = furnitureEdgeDistanceToWall(item, wall);
      if (dist >= wallThreshold) return false;
      if (allWalls && allWalls.length > 1) {
        // Must be the *closest* wall to avoid corner leakage.
        for (const other of allWalls) {
          if (other.id === wall.id) continue;
          const otherDist = furnitureEdgeDistanceToWall(item, other);
          if (otherDist < dist - 1e-6) return false;
        }
      }
      return true;
    })
    .map((item) => {
      // Convert anchored (x, y) to rotated bounding-box centre.
      const offset = rotatedAnchorOffsetToCenter(
        item.positionAnchor,
        item.width,
        item.depth,
        item.rotation,
      );
      const cx = item.x + offset.dx;
      const cy = item.y + offset.dy;
      const dxC = cx - start.x;
      const dyC = cy - start.y;
      const alongWallCenter = dxC * dx + dyC * dy;

      // Silhouette width on the wall = rotated half-extent along the wall
      // direction, doubled. Matches what the 3D view shows.
      const { halfAlong } = rotatedHalfExtents(item, dx, dy);
      const projectedWidth = halfAlong * 2;
      const fromFloor = item.elevationFromFloor ?? 0;

      return {
        item,
        rect: {
          x: alongWallCenter - halfAlong,
          y: fromFloor,
          width: projectedWidth,
          height: item.height,
        },
      };
    });
}

// ── Plinth Segments ──

/** Compute plinth segments, interrupted at door openings. */
export function computePlinthSegments(
  wall: Wall,
  openings: readonly WallOpening[],
  plinthHeight: number,
): readonly PlinthSegment[] {
  const axis = getProjectionAxis(wall);
  const wallLen = axis.length;
  if (wallLen <= 0 || plinthHeight <= 0) return [];

  // Collect door gaps (sorted by canonical projection position)
  const doors = openings
    .filter((o) => o.wallId === wall.id && o.type === 'DOOR')
    .map((o) => {
      const projectedCenter = axis.flipped ? wallLen - o.positionAlongWall : o.positionAlongWall;
      return {
        start: Math.max(0, projectedCenter - o.width / 2),
        end: Math.min(wallLen, projectedCenter + o.width / 2),
      };
    })
    .sort((a, b) => a.start - b.start);

  if (doors.length === 0) {
    return [{ x: 0, width: wallLen, height: plinthHeight }];
  }

  const segments: PlinthSegment[] = [];
  let cursor = 0;

  for (const door of doors) {
    if (door.start > cursor) {
      segments.push({
        x: cursor,
        width: door.start - cursor,
        height: plinthHeight,
      });
    }
    cursor = door.end;
  }

  if (cursor < wallLen) {
    segments.push({
      x: cursor,
      width: wallLen - cursor,
      height: plinthHeight,
    });
  }

  return segments;
}

/** Get an i18n key for a wall direction label. */
export function wallDirectionKey(wall: Wall): string {
  switch (wall.direction) {
    case 'NORTH': return 'wall.north';
    case 'SOUTH': return 'wall.south';
    case 'EAST': return 'wall.east';
    case 'WEST': return 'wall.west';
    default: return 'wall.other';
  }
}

/** Get a human-readable label for a wall direction (legacy, non-i18n). */
export function wallDirectionLabel(wall: Wall): string {
  switch (wall.direction) {
    case 'NORTH': return 'North Wall';
    case 'SOUTH': return 'South Wall';
    case 'EAST': return 'East Wall';
    case 'WEST': return 'West Wall';
    default: return `Wall ${wall.id.slice(0, 6)}`;
  }
}
