"""
Macrolactone Fragmenter - 大环内酯侧链断裂和分析器

专门用于 12-20 元环大环内酯的：
- 环原子编号（羰基C为1，酯键O为2）
- 侧链断裂（单个位置或全部）
- 分子可视化（带编号）
- 碎片可视化
- 数据导出（JSON、DataFrame、数据库）

Author: Auto-generated
Date: 2025-11-07
"""

from typing import Optional, Union, List, Tuple, Dict
from pathlib import Path
import pandas as pd
from rdkit import Chem
from rdkit.Chem import Descriptors, AllChem, Draw
from rdkit.Chem.Draw import rdMolDraw2D
from IPython.display import display, SVG

from .fragment_dataclass import Fragment, MoleculeFragments
from .ring_visualization import (
    get_macrolactone_numbering,
    identify_side_chains,
    extract_side_chain_fragment,
    cleave_side_chain_at_position,
    draw_mol_with_ring_numbering
)


class MacrolactoneFragmenter:
    """
    大环内酯（Macrolactone）侧链断裂和分析器
    
    专门用于 12-20 元环大环内酯的：
    - 环原子编号（羰基C为1，酯键O为2）
    - 侧链断裂（单个位置或全部）
    - 分子可视化（带编号）
    - 碎片可视化
    - 数据导出（JSON、DataFrame、数据库）
    
    使用场景：
        >>> # 处理16元环大环内酯
        >>> fragmenter = MacrolactoneFragmenter(ring_size=16)
        >>> result = fragmenter.process_molecule(smiles)
        >>> 
        >>> # 可视化
        >>> svg = fragmenter.visualize_molecule(result.parent_smiles)
        >>> display(SVG(svg))
        >>> 
        >>> # 保存结果
        >>> fragmenter.save_to_json(result, "output.json")
        >>> 
        >>> # 批量处理
        >>> results = fragmenter.process_csv("molecules.csv", smiles_column="smiles")
    
    编号规则：
        - 位置1: 羰基碳（C=O中的C，在环上）
        - 位置2: 酯键氧（环上的O）
        - 位置3-N: 从位置2开始，按照O→C反方向顺序编号环上剩余原子
    
    Attributes:
        ring_size: 环的大小（12-20）
        smarts_pattern: 用于识别大环内酯的SMARTS模式
    """
    
    # 预设常用SMARTS模式（通用酯键模式适用于12-20元环）
    DEFAULT_SMARTS = {
        # 使用通用酯键模式，适用于所有12-20元环大环内酯
        # 格式：[ring_size][C](=O)[O] 表示环上的碳连接羰基和氧
        12: "[C](=O)[O]",  # 通用酯键模式
        13: "[C](=O)[O]",  # 通用酯键模式
        14: "[C](=O)[O]",  # 通用酯键模式
        15: "[C](=O)[O]",  # 通用酯键模式
        16: "[C](=O)[O]",  # 通用酯键模式（原模式："O=C1CCCCCCCC(=O)OCC/C=C/C=C/1"）
        17: "[C](=O)[O]",  # 通用酯键模式
        18: "[C](=O)[O]",  # 通用酯键模式
        19: "[C](=O)[O]",  # 通用酯键模式
        20: "[C](=O)[O]",  # 通用酯键模式
    }
    
    def __init__(
        self,
        ring_size: int = 16,
        custom_smarts: Optional[str] = None
    ):
        """
        初始化大环内酯断裂器
        
        Args:
            ring_size: 环的大小（12-20），默认16
            custom_smarts: 自定义SMARTS模式（可选）
                          如果不提供，使用预设模式
        
        Raises:
            ValueError: 如果ring_size不在12-20范围内
            ValueError: 如果custom_smarts无效
            ValueError: 如果ring_size没有预设SMARTS且未提供custom_smarts
        """
        # 验证ring_size
        if not (12 <= ring_size <= 20):
            raise ValueError(
                f"Ring size must be between 12 and 20, got {ring_size}"
            )
        
        self.ring_size = ring_size
        
        # 确定SMARTS模式
        if custom_smarts is not None:
            # 使用自定义SMARTS
            self.smarts_pattern = custom_smarts
            # 验证SMARTS有效性
            test_mol = Chem.MolFromSmarts(self.smarts_pattern)
            if test_mol is None:
                raise ValueError(
                    f"Invalid SMARTS pattern: {self.smarts_pattern}"
                )
        else:
            # 使用预设SMARTS
            if ring_size not in self.DEFAULT_SMARTS:
                raise ValueError(
                    f"No default SMARTS pattern for ring size {ring_size}. "
                    f"Available sizes: {list(self.DEFAULT_SMARTS.keys())}. "
                    f"Please provide custom_smarts parameter."
                )
            self.smarts_pattern = self.DEFAULT_SMARTS[ring_size]
    
    # ============ 核心处理方法 ============
    
    def process_molecule(
        self,
        mol_input: Union[str, Chem.Mol],
        parent_id: Optional[str] = None
    ) -> MoleculeFragments:
        """
        一站式处理：编号 + 断裂所有侧链
        
        Args:
            mol_input: SMILES字符串或RDKit Mol对象
            parent_id: 可选的分子ID（如果不提供，自动生成）
        
        Returns:
            MoleculeFragments对象，包含所有断裂的侧链
        
        Raises:
            ValueError: 如果分子解析失败
            ValueError: 如果不是有效的大环内酯（找不到环或酯键）
        """
        # 解析输入
        mol, smiles = self._parse_mol_input(mol_input)
        
        # 自动生成parent_id
        if parent_id is None:
            parent_id = f"macro_{self.ring_size}_{id(mol)}"
        
        # 断裂所有侧链
        mol_fragments = self.cleave_all_sidechains(mol, parent_id)
        
        return mol_fragments
    
    def number_ring(
        self,
        mol: Chem.Mol
    ) -> Tuple[List[int], Dict[int, int], int, int]:
        """
        对环原子进行编号
        
        编号规则：
        - 位置1: 羰基碳（C=O中的C）
        - 位置2: 酯键氧（环上的O）
        - 位置3-N: 按照O→C反方向顺序编号
        
        Args:
            mol: RDKit分子对象
        
        Returns:
            Tuple of:
                - ring_atoms: 环原子索引列表
                - ring_numbering: 原子索引到位置编号的映射 {atom_idx: position}
                - carbonyl_carbon: 羰基碳原子索引
                - ester_oxygen: 酯键氧原子索引
        
        Raises:
            ValueError: 如果找不到指定大小的环
            ValueError: 如果找不到酯键（不是有效的大环内酯）
        """
        ring_atoms, ring_numbering, ordered_atoms, carbonyl_carbon, ester_oxygen = \
            get_macrolactone_numbering(mol, ring_size=self.ring_size)
        
        if ring_numbering is None:
            raise ValueError(
                f"Failed to number the ring. "
                f"This molecule may not be a valid {self.ring_size}-membered macrolactone. "
                f"Expected to find a {self.ring_size}-membered ring with an ester bond."
            )
        
        return ring_atoms, ring_numbering, carbonyl_carbon, ester_oxygen
    
    def cleave_at_position(
        self,
        mol: Chem.Mol,
        position: int,
        parent_id: Optional[str] = None
    ) -> List[Fragment]:
        """
        在指定位置断裂侧链
        
        Args:
            mol: RDKit分子对象
            position: 环上位置编号（1到ring_size）
            parent_id: 母分子ID（可选）
        
        Returns:
            该位置的所有侧链碎片列表（通常0-1个Fragment）
            如果该位置没有侧链，返回空列表
        
        Raises:
            ValueError: 如果position超出范围
            ValueError: 如果分子编号失败
        """
        # 验证position
        if not (1 <= position <= self.ring_size):
            raise ValueError(
                f"Position must be between 1 and {self.ring_size}, got {position}"
            )
        
        # 获取编号
        ring_atoms, ring_numbering, carbonyl_carbon, ester_oxygen = self.number_ring(mol)
        
        # 获取SMILES
        parent_smiles = Chem.MolToSmiles(mol)
        if parent_id is None:
            parent_id = f"macro_{self.ring_size}_{id(mol)}"
        
        # 断裂指定位置
        fragment_smiles_list = cleave_side_chain_at_position(
            mol, ring_atoms, ring_numbering, position
        )
        
        # 创建Fragment对象
        fragments = []
        for i, frag_smiles in enumerate(fragment_smiles_list):
            if frag_smiles:
                frag_mol = Chem.MolFromSmiles(frag_smiles)
                if frag_mol:
                    fragment = Fragment(
                        fragment_smiles=frag_smiles,
                        parent_smiles=parent_smiles,
                        cleavage_position=position,
                        fragment_id=f"{parent_id}_pos{position}_{i}",
                        parent_id=parent_id,
                        atom_count=frag_mol.GetNumAtoms(),
                        molecular_weight=round(Descriptors.MolWt(frag_mol), 2)
                    )
                    fragments.append(fragment)
        
        return fragments
    
    def cleave_all_sidechains(
        self,
        mol: Chem.Mol,
        parent_id: Optional[str] = None,
        skip_positions: Optional[List[int]] = None
    ) -> MoleculeFragments:
        """
        断裂所有侧链（默认位置3到ring_size）
        
        Args:
            mol: RDKit分子对象
            parent_id: 母分子ID（可选）
            skip_positions: 要跳过的位置列表（默认跳过1和2，即酯键位置）
        
        Returns:
            MoleculeFragments对象，包含所有断裂的侧链
        
        Raises:
            ValueError: 如果分子编号失败
        """
        # 获取编号
        ring_atoms, ring_numbering, carbonyl_carbon, ester_oxygen = self.number_ring(mol)
        
        # 获取SMILES
        parent_smiles = Chem.MolToSmiles(mol)
        if parent_id is None:
            parent_id = f"macro_{self.ring_size}_{id(mol)}"
        
        # 创建MoleculeFragments对象
        mol_fragments = MoleculeFragments(
            parent_id=parent_id,
            parent_smiles=parent_smiles,
            ring_numbering={str(k): v for k, v in ring_numbering.items()}
        )
        
        # 确定要跳过的位置（默认跳过1和2）
        if skip_positions is None:
            skip_positions = [1, 2]
        
        # 断裂所有侧链
        fragment_counter = 0
        for position in range(1, self.ring_size + 1):
            if position in skip_positions:
                continue
            
            fragment_smiles_list = cleave_side_chain_at_position(
                mol, ring_atoms, ring_numbering, position
            )
            
            for frag_smiles in fragment_smiles_list:
                if frag_smiles:
                    frag_mol = Chem.MolFromSmiles(frag_smiles)
                    if frag_mol:
                        fragment = Fragment(
                            fragment_smiles=frag_smiles,
                            parent_smiles=parent_smiles,
                            cleavage_position=position,
                            fragment_id=f"{parent_id}_frag_{fragment_counter}",
                            parent_id=parent_id,
                            atom_count=frag_mol.GetNumAtoms(),
                            molecular_weight=round(Descriptors.MolWt(frag_mol), 2)
                        )
                        mol_fragments.add_fragment(fragment)
                        fragment_counter += 1
        
        return mol_fragments
    
    # ============ 可视化方法 ============
    
    def visualize_molecule(
        self,
        mol_input: Union[str, Chem.Mol],
        show_numbering: bool = True,
        size: Tuple[int, int] = (800, 800),
        title: str = "",
        save_path: Optional[Union[str, Path]] = None,
        dpi: int = 600
    ) -> str:
        """
        可视化分子结构
        
        Args:
            mol_input: SMILES字符串或RDKit Mol对象
            show_numbering: 是否显示环原子编号
            size: 图像大小（宽，高）
            title: 图像标题
            save_path: 保存PNG的路径（可选）。如果提供，保存PNG图片
            dpi: PNG图片的DPI（仅在save_path提供时有效）
        
        Returns:
            SVG字符串（可在Jupyter中用display(SVG(...))显示）
        
        Raises:
            ValueError: 如果分子解析失败
            ValueError: 如果分子编号失败（show_numbering=True时）
        """
        # 解析输入
        mol, smiles = self._parse_mol_input(mol_input)
        
        if show_numbering:
            # 获取编号
            ring_atoms, ring_numbering, _, _ = self.number_ring(mol)
            
            # 生成SVG
            svg = draw_mol_with_ring_numbering(
                mol,
                ring_numbering=ring_numbering,
                ring_atoms=ring_atoms,
                size=size,
                title=title
            )
        else:
            # 不显示编号，只高亮环原子
            try:
                ring_atoms, _, _, _ = self.number_ring(mol)
                
                # 确保有2D坐标
                AllChem.Compute2DCoords(mol)
                
                drawer = rdMolDraw2D.MolDraw2DSVG(size[0], size[1])
                drawer.SetFontSize(6)
                
                draw_options = drawer.drawOptions()
                draw_options.addAtomIndices = False
                
                # 高亮环原子
                highlight_atoms = list(ring_atoms)
                atom_colors = {idx: (0.8, 0.9, 1.0) for idx in ring_atoms}
                
                drawer.DrawMolecule(
                    mol,
                    highlightAtoms=highlight_atoms,
                    highlightAtomColors=atom_colors
                )
                
                drawer.FinishDrawing()
                svg = drawer.GetDrawingText()
            except:
                # 如果编号失败，只显示基本分子
                AllChem.Compute2DCoords(mol)
                drawer = rdMolDraw2D.MolDraw2DSVG(size[0], size[1])
                drawer.DrawMolecule(mol)
                drawer.FinishDrawing()
                svg = drawer.GetDrawingText()
        
        # 如果提供了save_path，保存PNG
        if save_path is not None:
            self._save_png(mol, save_path, size, dpi, show_numbering)
        
        return svg
    
    def visualize_fragment(
        self,
        fragment: Fragment,
        size: Tuple[int, int] = (400, 400),
        save_path: Optional[Union[str, Path]] = None,
        dpi: int = 600
    ) -> str:
        """
        可视化单个碎片
        
        Args:
            fragment: Fragment对象
            size: 图像大小
            save_path: 保存PNG的路径（可选）
            dpi: PNG图片的DPI
        
        Returns:
            SVG字符串
        
        Raises:
            ValueError: 如果碎片SMILES无效
        """
        frag_mol = Chem.MolFromSmiles(fragment.fragment_smiles)
        if frag_mol is None:
            raise ValueError(f"Invalid fragment SMILES: {fragment.fragment_smiles}")
        
        # 确保有2D坐标
        AllChem.Compute2DCoords(frag_mol)
        
        # 生成SVG
        drawer = rdMolDraw2D.MolDraw2DSVG(size[0], size[1])
        drawer.SetFontSize(6)
        
        title = f"Position {fragment.cleavage_position}"
        drawer.DrawMolecule(frag_mol)
        drawer.FinishDrawing()
        svg = drawer.GetDrawingText()
        
        # 如果提供了save_path，保存PNG
        if save_path is not None:
            save_path = Path(save_path)
            save_path.parent.mkdir(parents=True, exist_ok=True)
            
            # 计算实际尺寸
            scale_factor = dpi / 72.0
            actual_width = int(size[0] * scale_factor)
            actual_height = int(size[1] * scale_factor)
            
            drawer_png = rdMolDraw2D.MolDraw2DCairo(actual_width, actual_height)
            font_size = max(6.0, min(40.0, 12.0 * scale_factor))
            drawer_png.SetFontSize(font_size)
            
            drawer_png.DrawMolecule(frag_mol)
            drawer_png.FinishDrawing()
            
            png_data = drawer_png.GetDrawingText()
            with open(save_path, 'wb') as f:
                f.write(png_data)
        
        return svg
    
    def visualize_all_fragments(
        self,
        mol_fragments: MoleculeFragments,
        max_display: int = 10,
        save_dir: Optional[Union[str, Path]] = None,
        dpi: int = 600
    ):
        """
        可视化所有碎片（在Jupyter中显示）
        
        Args:
            mol_fragments: MoleculeFragments对象
            max_display: 最多显示的碎片数量（防止输出过多）
            save_dir: 保存PNG图片的目录（可选）。如果提供，所有碎片图片会保存到此目录
            dpi: PNG图片的DPI
        
        Note:
            此方法会直接在Jupyter中显示图片，不返回值
        """
        fragments = mol_fragments.fragments[:max_display]
        
        if save_dir is not None:
            save_dir = Path(save_dir)
            save_dir.mkdir(parents=True, exist_ok=True)
        
        for i, frag in enumerate(fragments, 1):
            print(f"\n{'='*60}")
            print(f"Fragment {i}/{len(fragments)}")
            print(f"Position: {frag.cleavage_position}")
            print(f"SMILES: {frag.fragment_smiles}")
            print(f"Atoms: {frag.atom_count}, MW: {frag.molecular_weight}")
            print('='*60)
            
            # 确定保存路径
            frag_save_path = None
            if save_dir is not None:
                frag_save_path = save_dir / f"{frag.fragment_id}.png"
            
            # 生成SVG并显示
            svg = self.visualize_fragment(frag, save_path=frag_save_path, dpi=dpi)
            display(SVG(svg))
        
        if len(mol_fragments.fragments) > max_display:
            print(f"\n... and {len(mol_fragments.fragments) - max_display} more fragments")
    
    # ============ 数据导出方法 ============
    
    def save_to_json(
        self,
        mol_fragments: MoleculeFragments,
        filepath: Union[str, Path]
    ):
        """
        保存到JSON文件
        
        Args:
            mol_fragments: MoleculeFragments对象
            filepath: JSON文件路径
        """
        filepath = Path(filepath)
        filepath.parent.mkdir(parents=True, exist_ok=True)
        mol_fragments.to_json_file(str(filepath))
    
    def to_dataframe(
        self,
        mol_fragments: MoleculeFragments
    ) -> pd.DataFrame:
        """
        转换为DataFrame（方便分析）
        
        Args:
            mol_fragments: MoleculeFragments对象
        
        Returns:
            包含所有碎片信息的DataFrame，列包括：
            - fragment_id
            - parent_id
            - parent_smiles
            - cleavage_position
            - fragment_smiles
            - atom_count
            - molecular_weight
        """
        data = []
        for frag in mol_fragments.fragments:
            data.append({
                'fragment_id': frag.fragment_id,
                'parent_id': frag.parent_id,
                'parent_smiles': frag.parent_smiles,
                'cleavage_position': frag.cleavage_position,
                'fragment_smiles': frag.fragment_smiles,
                'atom_count': frag.atom_count,
                'molecular_weight': frag.molecular_weight
            })
        
        return pd.DataFrame(data)
    
    # ============ 批处理方法 ============
    
    def process_csv(
        self,
        csv_path: Union[str, Path],
        smiles_column: str = "smiles",
        id_column: Optional[str] = "unique_id",
        max_rows: Optional[int] = None
    ) -> List[MoleculeFragments]:
        """
        批量处理CSV文件中的分子
        
        Args:
            csv_path: CSV文件路径
            smiles_column: SMILES列名
            id_column: ID列名（可选）。如果为None，自动生成ID
            max_rows: 最多处理的行数（可选）。用于测试或限制处理量
        
        Returns:
            MoleculeFragments对象列表
        
        Raises:
            FileNotFoundError: 如果CSV文件不存在
            ValueError: 如果CSV中没有指定的列
        """
        csv_path = Path(csv_path)
        if not csv_path.exists():
            raise FileNotFoundError(f"CSV file not found: {csv_path}")
        
        # 读取CSV
        df = pd.read_csv(csv_path)
        
        # 验证列存在
        if smiles_column not in df.columns:
            raise ValueError(
                f"Column '{smiles_column}' not found in CSV. "
                f"Available columns: {df.columns.tolist()}"
            )
        
        if id_column is not None and id_column not in df.columns:
            raise ValueError(
                f"Column '{id_column}' not found in CSV. "
                f"Available columns: {df.columns.tolist()}"
            )
        
        # 限制行数
        if max_rows is not None:
            df = df.head(max_rows)
        
        # 批量处理
        results = []
        failed_count = 0
        
        for idx, row in df.iterrows():
            smiles = row[smiles_column]
            mol_id = row[id_column] if id_column is not None else f"mol_{idx}"
            
            try:
                mol_fragments = self.process_molecule(smiles, parent_id=mol_id)
                results.append(mol_fragments)
            except Exception as e:
                failed_count += 1
                print(f"Failed to process {mol_id}: {e}")
                continue
        
        print(f"\nProcessed {len(results)} molecules successfully")
        if failed_count > 0:
            print(f"Failed: {failed_count} molecules")
        
        return results
    
    def batch_to_dataframe(
        self,
        results: List[MoleculeFragments]
    ) -> pd.DataFrame:
        """
        将批处理结果转换为单个DataFrame
        
        Args:
            results: MoleculeFragments对象列表
        
        Returns:
            包含所有分子、所有碎片的DataFrame
        """
        all_data = []
        for mol_fragments in results:
            df = self.to_dataframe(mol_fragments)
            all_data.append(df)
        
        if all_data:
            return pd.concat(all_data, ignore_index=True)
        else:
            return pd.DataFrame()
    
    # ============ 数据库接口（预留SQLModel）============
    
    def save_to_database(
        self,
        mol_fragments: MoleculeFragments,
        session  # SQLModel Session
    ):
        """
        保存到数据库（预留接口）
        
        预期Schema:
        
        ```python
        from sqlmodel import SQLModel, Field, Relationship
        from typing import Optional, List
        
        class MacrolactoneParent(SQLModel, table=True):
            \"\"\"母分子表\"\"\"
            id: Optional[int] = Field(default=None, primary_key=True)
            parent_id: str = Field(index=True, unique=True)
            parent_smiles: str
            ring_size: int
            ring_numbering: str  # JSON字符串
            
            # 关系
            fragments: List["FragmentSidechain"] = Relationship(back_populates="parent")
        
        class FragmentSidechain(SQLModel, table=True):
            \"\"\"侧链碎片表\"\"\"
            id: Optional[int] = Field(default=None, primary_key=True)
            fragment_id: str = Field(index=True)
            parent_id: str = Field(foreign_key="macrolactoneparent.parent_id")
            cleavage_position: int
            fragment_smiles: str
            atom_count: int
            molecular_weight: float
            
            # 关系
            parent: Optional[MacrolactoneParent] = Relationship(back_populates="fragments")
        ```
        
        使用示例:
        ```python
        from sqlmodel import create_engine, Session
        
        engine = create_engine("postgresql://user:pass@localhost/db")
        
        with Session(engine) as session:
            fragmenter.save_to_database(mol_fragments, session)
        ```
        
        Args:
            mol_fragments: MoleculeFragments对象
            session: SQLModel Session对象
        
        TODO: 实现SQLModel集成
        """
        raise NotImplementedError(
            "Database integration not yet implemented. "
            "Will use SQLModel with schema: MacrolactoneParent + FragmentSidechain. "
            "See method docstring for expected schema."
        )
    
    @classmethod
    def load_from_database(
        cls,
        parent_id: str,
        session  # SQLModel Session
    ) -> MoleculeFragments:
        """
        从数据库加载（预留接口）
        
        Args:
            parent_id: 母分子ID
            session: SQLModel Session对象
        
        Returns:
            MoleculeFragments对象
        
        TODO: 实现SQLModel集成
        """
        raise NotImplementedError("Database loading not yet implemented")
    
    # ============ 私有辅助方法 ============
    
    def _parse_mol_input(
        self,
        mol_input: Union[str, Chem.Mol]
    ) -> Tuple[Chem.Mol, str]:
        """
        解析分子输入（SMILES或Mol对象）
        
        Args:
            mol_input: SMILES字符串或RDKit Mol对象
        
        Returns:
            Tuple of (RDKit Mol对象, SMILES字符串)
        
        Raises:
            ValueError: 如果输入无效
        """
        if isinstance(mol_input, str):
            # SMILES字符串
            mol = Chem.MolFromSmiles(mol_input)
            if mol is None:
                raise ValueError(f"Invalid SMILES: {mol_input}")
            smiles = mol_input
        elif isinstance(mol_input, Chem.Mol):
            # RDKit Mol对象
            mol = mol_input
            smiles = Chem.MolToSmiles(mol)
        else:
            raise ValueError(
                f"mol_input must be SMILES string or RDKit Mol object, "
                f"got {type(mol_input)}"
            )
        
        return mol, smiles
    
    def _save_png(
        self,
        mol: Chem.Mol,
        save_path: Union[str, Path],
        size: Tuple[int, int],
        dpi: int,
        show_numbering: bool
    ):
        """
        保存分子为PNG图片
        
        Args:
            mol: RDKit分子对象
            save_path: 保存路径
            size: 图像大小
            dpi: DPI
            show_numbering: 是否显示编号
        """
        save_path = Path(save_path)
        save_path.parent.mkdir(parents=True, exist_ok=True)
        
        # 计算实际尺寸
        scale_factor = dpi / 72.0
        actual_width = int(size[0] * scale_factor)
        actual_height = int(size[1] * scale_factor)
        
        drawer = rdMolDraw2D.MolDraw2DCairo(actual_width, actual_height)
        
        # 设置字体大小
        font_size = max(6.0, min(40.0, 12.0 * scale_factor))
        drawer.SetFontSize(font_size)
        
        draw_options = drawer.drawOptions()
        draw_options.addAtomIndices = False
        
        if show_numbering:
            try:
                ring_atoms, ring_numbering, _, _ = self.number_ring(mol)
                
                # 高亮环原子
                highlight_atoms = list(ring_atoms)
                atom_colors = {idx: (0.8, 0.9, 1.0) for idx in ring_atoms}
                
                # 设置编号
                mol_copy = Chem.Mol(mol)
                for atom_idx in ring_atoms:
                    if atom_idx in ring_numbering:
                        atom = mol_copy.GetAtomWithIdx(atom_idx)
                        atom.SetProp("atomNote", str(ring_numbering[atom_idx]))
                
                drawer.DrawMolecule(
                    mol_copy,
                    highlightAtoms=highlight_atoms,
                    highlightAtomColors=atom_colors
                )
            except:
                # 如果编号失败，只绘制基本分子
                drawer.DrawMolecule(mol)
        else:
            drawer.DrawMolecule(mol)
        
        drawer.FinishDrawing()
        
        # 保存PNG
        png_data = drawer.GetDrawingText()
        with open(save_path, 'wb') as f:
            f.write(png_data)
    
    def __repr__(self) -> str:
        return (
            f"MacrolactoneFragmenter(ring_size={self.ring_size}, "
            f"smarts='{self.smarts_pattern[:50]}...')"
        )

