# -*- coding: utf-8 -*-
import os
import re
import argparse
import win32com.client
from win32com.client import constants


def terminate():
    input("\nPress ENTER to terminate>>\n")
    exit()

def parse_args():
    parser = argparse.ArgumentParser(description="PowerPointノート処理（字幕追加 or {{key}}置換）")
    parser.add_argument("infile", help="入力PowerPointファイル（.pptx）")
    parser.add_argument("-o", "--outfile", help="出力ファイル名（省略時は -note-added.pptx / -replaced.pptx）")
    parser.add_argument("--mode", choices=["add", "replace"], default="add",
                        help="add: ノートを字幕テキストボックスとして追加 / replace: 置換ファイル＋ノート(supertitle)で{{key}}置換")
    parser.add_argument("--replace", "-R", type=str,
                        help="[mode=replace] 置換用のTOML風 key=value ファイル（例: title=Hello）")

    # add モード用の見た目設定
    parser.add_argument("--box_margin", type=int, default=50, help="字幕ボックスの左右余白（pt）")
    parser.add_argument("--box_height", type=int, default=60, help="字幕ボックスの高さ（pt）")
    parser.add_argument("--font_name", default="メイリオ", help="フォント名")
    parser.add_argument("--font_size", type=int, default=12, help="フォントサイズ（pt）")
    parser.add_argument("--font_color", default="884444", help="文字色（16進RGB）")
    parser.add_argument("--bgcolor", default="00dddd", help="背景色（16進RGB）")
    parser.add_argument("--bgalpha", type=float, default=0.6, help="背景の透明度（0.0〜1.0）")

    args = parser.parse_args()

    if not args.outfile:
        base, ext = os.path.splitext(args.infile)
        if args.mode == "add":
            args.outfile = f"{base}-note-added.pptx"
        else:
            args.outfile = f"{base}-replaced.pptx"

    return args

# -----------------------------
# 共通: ノート本文取得
# -----------------------------
def get_notes_text(slide):
    """スライドのノート本文（Placeholder Body）を返す。無ければ空文字"""
    try:
        # ppPlaceholderBody=2
        # text = slide.NotesPage.Shapes.Placeholders(2).TextFrame.TextRange.Text.strip() # 変更前
        text = slide.NotesPage.Shapes.Placeholders(2).TextFrame.TextRange.Text
        return text # 変更後：clean_text()の呼び出しを削除
    except Exception:
        return ""
        
# -----------------------------
# add: ノートを字幕ボックスとして追加
# -----------------------------
def add_subtitles_with_transparency(args):
    print()
    print(f"Add subtitles (mode={args.mode}):")
    print(f"{args.infile=}")
    print(f"{args.outfile=}")
    print(f"{args.box_margin=}")
    print(f"{args.box_height=}")
    print(f"{args.font_name=}")
    print(f"{args.font_size=}")
    print(f"{args.font_color=}")
    print(f"{args.bgcolor=}")
    print(f"{args.bgalpha=}")

    ppt = win32com.client.Dispatch("PowerPoint.Application")
    ppt.Visible = True

    args.infile = os.path.abspath(args.infile)
    args.outfile = os.path.abspath(args.outfile)
    print(f"\n{args.infile=}")
    print(f"{args.outfile=}")

    presentation = ppt.Presentations.Open(args.infile, WithWindow=False)

    slide_width = presentation.PageSetup.SlideWidth
    slide_height = presentation.PageSetup.SlideHeight

    textbox_width = slide_width - 2 * args.box_margin
    textbox_left = args.box_margin
    textbox_top = slide_height - args.box_height - 20  # 下部に配置（余白20pt）
    textbox_height = args.box_height

    font_rgb = int(args.font_color, 16)
    bg_rgb = int(args.bgcolor, 16)

    for slide in presentation.Slides:
        note_text = get_notes_text(slide)
        if not note_text:
            continue

#空行を削除
        lines = [line for line in note_text.splitlines() if line.strip()] # 変更後
        note_text = '\n'.join(lines)

        shape = slide.Shapes.AddTextbox(
            Orientation=1,
            Left=textbox_left,
            Top=textbox_top,
            Width=textbox_width,
            Height=textbox_height
        )
        shape.TextFrame.WordWrap = True
        shape.TextFrame.TextRange.Text = note_text

        text_range = shape.TextFrame.TextRange
        text_range.Font.Name = args.font_name
        text_range.Font.Size = args.font_size
        text_range.Font.Color.RGB = font_rgb
        text_range.ParagraphFormat.Alignment = 1  # ppAlignLeft

        shape.Fill.ForeColor.RGB = bg_rgb
        shape.Fill.Transparency = args.bgalpha
        shape.Fill.Visible = True
        shape.Fill.Solid()

    try:
        presentation.SaveAs(args.outfile)
    except:
        print("\nError: ファイル[{args.outfile}]への保存に失敗しました\n")
        presentation.Close()
        return 0

    presentation.Close()
    ppt.Quit()
    print(f"保存しました: {args.outfile}")

# -----------------------------
# replace: 置換ファイル読み込み（TOML風 key=value）
# -----------------------------
def parse_toml_style(filepath):
    """
    とても寛容な key=value 形式を読み込む。
    - 行頭/行末の空白OK
    - # コメントOK
    - 値のクォートはあってもなくてもOK（両端の ' " は剥がす）
    戻り値: dict {key: value}
    """
    if not filepath:
        return {}
    if not os.path.isfile(filepath):
        print(f"Warning: replace ファイルが見つかりません: {filepath}")
        return {}

    pattern = re.compile(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*(?:#.*)?$')
    result = {}
    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#") or line.startswith("["):
                continue
            m = pattern.match(line)
            if not m:
                print(f"警告: 無視された行: {line[:80]}")
                continue
            key, val = m.group(1), m.group(2)
            if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
                val = val[1:-1]
            result[key] = val
    return result

# -----------------------------
# replace: スライド内の {{key}} を部分置換（書式/アニメ保持）
# -----------------------------
def clean_text(text):
    # strip()で両端の空白を削除し、空行を除去
    # lines = [line.strip() for line in text.splitlines() if line.strip()] # 変更前
    # note_text = "\n".join(lines)                                         # 変更前

    # PowerPointのノートテキストは "\r\n" や末尾の改行を含むことがあるため、
    # splitlines() で行に分割し、空白文字のみの行や空行を除去します。
    lines = [line for line in text.splitlines() if line.strip()]
    note_text = "\n".join(lines)
    
    return note_text

def replace_placeholders_on_slide(slide, base_map):
    """
    base_map: dict（置換キー→値）。この関数内で各スライドのノートを 'supertitle' に追加する。
    返り値: 置換件数（ヒット数）
    """
    # スライド固有の 'supertitle' をセット（ファイル側の 'supertitle' 指定より優先）
    current_map = dict(base_map)
    raw_notes = get_notes_text(slide)
    current_map['supertitle'] = clean_text(raw_notes)

    replaced = 0
    replaced += _replace_in_shapes(slide.Shapes, current_map)

    return replaced

def _replace_in_shapes(shapes, kv):
    """Shapes コレクションを走査（グループ/表セルも対応）して置換数を返す"""
    total = 0
    for shp in shapes:
        total += _replace_in_shape_deep(shp, kv)
    return total

def _replace_in_shape_deep(shp, kv):
    """単一 Shape に対して、グループ／表／通常テキストの順に部分置換"""
    cnt = 0
    try:
        # グループ
        if shp.Type == 6:  # msoGroup
            cnt += _replace_in_shapes(shp.GroupItems, kv)
    except Exception:
        pass

    try:
        # 表セル
        if shp.HasTable:
            tbl = shp.Table
            for r in range(1, tbl.Rows.Count + 1):
                for c in range(1, tbl.Columns.Count + 1):
                    cell_shape = tbl.Cell(r, c).Shape
                    cnt += _replace_in_textframe(cell_shape, kv)
    except Exception:
        pass

    # 通常テキスト
    cnt += _replace_in_textframe(shp, kv)

    return cnt

def _replace_in_textframe(shp, kv):
    """TextFrame の中で {{key}} を“部分置換”する（書式・アニメを壊さない）"""

    try:
        if not shp.HasTextFrame:
            return 0
        if not shp.TextFrame.HasText:
            return 0
    except Exception:
        return 0

    tr_all = shp.TextFrame.TextRange
    count = 0

    # すべてのキーについて順に置換（単純な {{key}} マッチ）
    for key, value in kv.items():
        placeholder = "{{" + key + "}}"
        after_pos = 0
        while True:
            try:
                tr_hit = tr_all.Find(FindWhat=placeholder, After=after_pos,
                                     MatchCase=False, WholeWords=False)
            except Exception:
                tr_hit = None
            if tr_hit is None:
                break
            # 部分置換：一致範囲の Text を直接書き換える
            tr_hit.Text = value
            count += 1
            # 次検索（現在ヒットの末尾から）
            after_pos = tr_hit.Start + tr_hit.Length - 1

    return count

# -----------------------------
# replace モードの本体
# -----------------------------
def replace_placeholders_with_file(args):
    print()
    print(f"Replace placeholderss (mode={args.mode}):")
    print(f"{args.infile=}")
    print(f"{args.outfile=}")

    # 置換key=value を読み込み
    base_map = parse_toml_style(args.replace) if args.replace else {}
    print(f"\n置換マップ読み込み: {len(base_map)} 件（+ 各スライドの supertitle を付与）")

    ppt = win32com.client.Dispatch("PowerPoint.Application")
    ppt.Visible = True

    args.infile = os.path.abspath(args.infile)
    args.outfile = os.path.abspath(args.outfile)
    print(f"{args.infile=}")
    print(f"{args.outfile=}")

    pres = ppt.Presentations.Open(args.infile, WithWindow=False)

    total = 0
    for slide in pres.Slides:
        total += replace_placeholders_on_slide(slide, base_map)

    try:
        pres.SaveAs(args.outfile)
    except:
        print("\nError: ファイル[{args.outfile}]への保存に失敗しました\n")
        presentation.Close()
        return 0
        
    pres.Close()
    ppt.Quit()
    print(f"置換完了: {total} 箇所 / 保存しました: {args.outfile}")
    print( "　注：ノートを置換する場合は {{supertitle}} を使ってください")


# -----------------------------
# main
# -----------------------------
if __name__ == "__main__":
    args = parse_args()
    if args.mode == "add":
        add_subtitles_with_transparency(args)
    else:
        replace_placeholders_with_file(args)

    terminate()
    