# src/markdown_helper.py
from __future__ import annotations
from imgui_bundle import imgui_md, imgui, immapp, imgui_color_text_edit as ed
import webbrowser
import os
import re
from pathlib import Path
from typing import Optional, Dict, Callable

class MarkdownRenderer:
 """
    Hybrid Markdown renderer that uses imgui_md for text/headers
    and ImGuiColorTextEdit for syntax-highlighted code blocks.
    """
 def __init__(self):
  self.options = imgui_md.MarkdownOptions()
  # Base path for fonts (Inter family)
  self.options.font_options.font_base_path = "fonts/Inter"
  self.options.font_options.regular_size = 16.0
  
  # Configure callbacks
  self.options.callbacks.on_open_link = self._on_open_link
  
  # Cache for TextEditor instances to maintain state
  self._editor_cache: Dict[tuple[str, int], ed.TextEditor] = {}
  self._max_cache_size = 100
  
  # Optional callback for custom local link handling (e.g., opening in IDE)
  self.on_local_link: Optional[Callable[[str], None]] = None
  
  # Language mapping for ImGuiColorTextEdit
  self._lang_map = {
   "python": ed.TextEditor.LanguageDefinitionId.python,
   "py": ed.TextEditor.LanguageDefinitionId.python,
   "json": ed.TextEditor.LanguageDefinitionId.json,
   "cpp": ed.TextEditor.LanguageDefinitionId.cpp,
   "c++": ed.TextEditor.LanguageDefinitionId.cpp,
   "c": ed.TextEditor.LanguageDefinitionId.c,
   "lua": ed.TextEditor.LanguageDefinitionId.lua,
   "sql": ed.TextEditor.LanguageDefinitionId.sql,
   "cs": ed.TextEditor.LanguageDefinitionId.cs,
   "c#": ed.TextEditor.LanguageDefinitionId.cs,
  }

 def _on_open_link(self, url: str) -> None:
  """Handle link clicks in Markdown."""
  if url.startswith("http"):
   webbrowser.open(url)
  else:
   # Try to handle as a local file path
   try:
    p = Path(url)
    if p.exists():
     if self.on_local_link:
      self.on_local_link(str(p.absolute()))
     else:
      # Fallback to OS default handler
      webbrowser.open(str(p.absolute()))
    else:
     print(f"Link target does not exist: {url}")
   except Exception as e:
    print(f"Error opening link {url}: {e}")

 def render(self, text: str, context_id: str = "default") -> None:
  """Render Markdown text with code block interception."""
  if not text:
   return

  # Split into markdown and code blocks
  parts = re.split(r'(```[\s\S]*?```)', text)
  
  block_idx = 0
  for part in parts:
   if part.startswith('```') and part.endswith('```'):
    self._render_code_block(part, context_id, block_idx)
    block_idx += 1
   elif part.strip():
    imgui_md.render(part)

 def render_unindented(self, text: str) -> None:
  """Render Markdown text with automatic unindentation."""
  imgui_md.render_unindented(text)

 def render_code(self, code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
  """Render a code block directly with syntax highlighting."""
  # Wrap in fake markdown markers for the internal renderer
  self._render_code_block(f"```{lang}\n{code}```", context_id, block_idx)

 def _render_code_block(self, block: str, context_id: str, block_idx: int) -> None:
  """Render a code block using TextEditor for syntax highlighting."""
  lines = block.strip('`').split('\n')
  lang_tag = lines[0].strip().lower() if lines else ""
  
  # Heuristic to separate lang tag from code
  if lang_tag and lang_tag not in self._lang_map and not self._is_likely_lang_tag(lang_tag):
   lang_tag = ""
   code = '\n'.join(lines)
  else:
   code = '\n'.join(lines[1:]) if len(lines) > 1 else ""

  if not lang_tag:
   lang_tag = self.detect_language(code)

  # Cache management
  if len(self._editor_cache) > self._max_cache_size:
   # Simple LRU-ish: just clear it all if it gets too big
   self._editor_cache.clear()

  cache_key = (context_id, block_idx)
  if cache_key not in self._editor_cache:
   editor = ed.TextEditor()
   editor.set_read_only_enabled(True)
   editor.set_show_line_numbers_enabled(True)
   self._editor_cache[cache_key] = editor
  
  editor = self._editor_cache[cache_key]
  
  # Sync text and language
  lang_id = self._lang_map.get(lang_tag, ed.TextEditor.LanguageDefinitionId.none)
  target_text = code + "\n"
  
  if editor.get_text() != target_text:
   editor.set_text(code)
   editor.set_language_definition(lang_id)
  elif editor.get_language_definition_name().lower() != lang_tag:
   # get_language_definition_name might not match exactly but good enough check
   editor.set_language_definition(lang_id)

  # Dynamic height calculation
  line_count = code.count('\n') + 1
  line_height = imgui.get_text_line_height()
  height = (line_count * line_height) + 20
  height = min(max(height, 40), 500)
  
  editor.render(f"##code_{context_id}_{block_idx}", a_size=imgui.ImVec2(0, height))

 def _is_likely_lang_tag(self, tag: str) -> bool:
  return bool(re.match(r'^[a-zA-Z0-9+#-]+$', tag)) and len(tag) < 15

 def detect_language(self, code: str) -> str:
  if "def " in code or "import " in code:
   return "python"
  if "{" in code and '"' in code and ":" in code:
   return "json"
  if "$" in code and ("{" in code or "if" in code):
   return "powershell"
  return ""

 def clear_cache(self) -> None:
  self._editor_cache.clear()

# Global instance
_renderer: Optional[MarkdownRenderer] = None

def get_renderer() -> MarkdownRenderer:
 global _renderer
 if _renderer is None:
  _renderer = MarkdownRenderer()
 return _renderer

def render(text: str, context_id: str = "default") -> None:
 get_renderer().render(text, context_id)

def render_unindented(text: str) -> None:
 get_renderer().render_unindented(text)

def render_code(code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
 get_renderer().render_code(code, lang, context_id, block_idx)

