import type { Editor } from "obsidian";

export function positionSelectionRewritePopover(root: HTMLElement, editor: Editor, viewWindow: Window, margin: number): boolean {
  if (!root.isConnected) return false;

  const view = editorViewFromEditor(editor);
  if (isViewHTMLElement(view?.dom, viewWindow) && !view.dom.isConnected) return false;

  const anchor = selectionRect(viewWindow) ?? editorCursorRect(editor, viewWindow) ?? root.ownerDocument.body.getBoundingClientRect();
  const size = root.getBoundingClientRect();
  const viewportWidth = viewWindow.innerWidth;
  const viewportHeight = viewWindow.innerHeight;
  const left = clamp(anchor.left, margin, viewportWidth - size.width - margin);
  const belowTop = anchor.bottom + margin;
  const aboveTop = anchor.top - size.height - margin;
  const top = belowTop + size.height <= viewportHeight - margin ? belowTop : clamp(aboveTop, margin, viewportHeight - size.height - margin);

  root.style.left = `${String(left)}px`;
  root.style.top = `${String(top)}px`;
  return true;
}

function selectionRect(viewWindow: Window): DOMRect | null {
  const selection = viewWindow.getSelection();
  if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null;
  const rect = selection.getRangeAt(0).getBoundingClientRect();
  return rect.width > 0 || rect.height > 0 ? rect : null;
}

function editorCursorRect(editor: Editor, viewWindow: Window): DOMRect | null {
  const view = editorViewFromEditor(editor);
  if (!view) return null;

  const offset = editor.posToOffset(editor.getCursor("to"));
  if (typeof view.coordsAtPos === "function") {
    return view.coordsAtPos(offset, 1) ?? view.coordsAtPos(offset, -1) ?? null;
  }
  return isViewHTMLElement(view.dom, viewWindow) ? view.dom.getBoundingClientRect() : null;
}

function editorViewFromEditor(editor: Editor): { coordsAtPos?: (pos: number, side?: -1 | 1) => DOMRect | null; dom?: unknown } | null {
  const candidate = editor as {
    cm?: unknown;
    editor?: { cm?: unknown };
  };
  if (isEditorView(candidate.cm)) return candidate.cm;
  if (isEditorView(candidate.editor?.cm)) return candidate.editor.cm;
  return null;
}

function isEditorView(value: unknown): value is { coordsAtPos?: (pos: number, side?: -1 | 1) => DOMRect | null; dom?: unknown } {
  return Boolean(value && typeof value === "object" && ("coordsAtPos" in value || "dom" in value));
}

function isViewHTMLElement(value: unknown, viewWindow: Window): value is HTMLElement {
  return value instanceof (viewWindow as Window & { HTMLElement: typeof HTMLElement }).HTMLElement;
}

function clamp(value: number, min: number, max: number): number {
  if (max < min) return min;
  return Math.min(Math.max(value, min), max);
}
