$ErrorActionPreference = "Stop"
Write-Host "Convert_DATA 2026-07-19"

# ============================================================
# Convert_DATA.ps1
# 旧PDFフォームで保存したFDF、またはVer1のPDFフォームを、新PDFフォーム用FDFに変換する。
#
# 配置:
#   データ移行プログラム/Convert_DATA.ps1
#   データ移行プログラム/Convert_DATA.bat
#   2026生活習慣病療養計画書.pdf
#
# 動作:
#   1. 変換するFDF/PDFを一覧画面で選ぶ。
#   2. 保存先フォルダを選ぶ。
#   3. 新PDF原本を保存先に form.pdf としてコピーする。
#   4. 変換後FDFは form.pdf を参照する。
# ============================================================

# ===== 基準フォルダ =====
$programFolder = $PSScriptRoot
$baseFolder = Split-Path -Parent $programFolder

if ([string]::IsNullOrWhiteSpace($baseFolder)) {
    $baseFolder = Split-Path -Parent $MyInvocation.MyCommand.Path
}

# ===== 新PDFフォーム原本 =====
# 日本語ファイル名の原本PDFは、FDFに直接参照させない。
# 保存先へ form.pdf としてコピーし、変換後FDFは form.pdf を開く。
$formPdfPath = Join-Path $baseFolder "2026生活習慣病療養計画書.pdf"

if (!(Test-Path -LiteralPath $formPdfPath)) {
    Write-Host "親フォルダに「2026生活習慣病療養計画書.pdf」が見つかりません。"
    Write-Host "ここではなく、一つ上の階層の親フォルダにこのPDFを置いてください。"
    Write-Host ""
    Write-Host "確認した場所:"
    Write-Host $formPdfPath
    Write-Host ""
    Read-Host "Enterキーで終了"
    exit
}

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# ===== エクスプローラー風フォルダ選択 =====
$folderPickerCode = @"
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace FolderPicker
{
    public class Picker
    {
        public static string Pick(string title, string initialPath)
        {
            IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialog();

            uint options;
            dialog.GetOptions(out options);

            // FOS_PICKFOLDERS      = 0x00000020
            // FOS_FORCEFILESYSTEM  = 0x00000040
            // FOS_PATHMUSTEXIST    = 0x00000800
            dialog.SetOptions(options | 0x00000020 | 0x00000040 | 0x00000800);
            dialog.SetTitle(title);

            if (!String.IsNullOrEmpty(initialPath) && Directory.Exists(initialPath))
            {
                IShellItem folder;
                SHCreateItemFromParsingName(
                    initialPath,
                    IntPtr.Zero,
                    typeof(IShellItem).GUID,
                    out folder
                );
                dialog.SetFolder(folder);
            }

            int hr = dialog.Show(IntPtr.Zero);

            // キャンセル
            if (hr == unchecked((int)0x800704C7))
            {
                return null;
            }

            Marshal.ThrowExceptionForHR(hr);

            IShellItem result;
            dialog.GetResult(out result);

            IntPtr pszString;
            result.GetDisplayName(0x80058000, out pszString); // SIGDN_FILESYSPATH

            string path = Marshal.PtrToStringUni(pszString);
            Marshal.FreeCoTaskMem(pszString);

            return path;
        }

        [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
        private static extern void SHCreateItemFromParsingName(
            [MarshalAs(UnmanagedType.LPWStr)] string pszPath,
            IntPtr pbc,
            [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
            [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv
        );

        [ComImport]
        [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
        private class FileOpenDialog
        {
        }

        [ComImport]
        [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileDialog
        {
            [PreserveSig]
            int Show(IntPtr parent);

            void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
            void SetFileTypeIndex(uint iFileType);
            void GetFileTypeIndex(out uint piFileType);
            void Advise(IntPtr pfde, out uint pdwCookie);
            void Unadvise(uint dwCookie);
            void SetOptions(uint fos);
            void GetOptions(out uint pfos);
            void SetDefaultFolder(IShellItem psi);
            void SetFolder(IShellItem psi);
            void GetFolder(out IShellItem ppsi);
            void GetCurrentSelection(out IShellItem ppsi);
            void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
            void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
            void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
            void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
            void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
            void GetResult(out IShellItem ppsi);
            void AddPlace(IShellItem psi, int fdap);
            void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
            void Close(int hr);
            void SetClientGuid(ref Guid guid);
            void ClearClientData();
            void SetFilter(IntPtr pFilter);
        }

        [ComImport]
        [Guid("d57c7288-d4ad-4768-be02-9d969532d960")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileOpenDialog : IFileDialog
        {
            void GetResults(out IntPtr ppenum);
            void GetSelectedItems(out IntPtr ppsai);
        }

        [ComImport]
        [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellItem
        {
            void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
            void GetParent(out IShellItem ppsi);
            void GetDisplayName(uint sigdnName, out IntPtr ppszName);
            void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
            void Compare(IShellItem psi, uint hint, out int piOrder);
        }
    }
}
"@

if (-not ("FolderPicker.Picker" -as [type])) {
    Add-Type -TypeDefinition $folderPickerCode -Language CSharp
}

function Select-Folder {
    param(
        [string]$Title,
        [string]$DefaultPath
    )

    if (!(Test-Path -LiteralPath $DefaultPath)) {
        $DefaultPath = $baseFolder
    }

    return [FolderPicker.Picker]::Pick($Title, $DefaultPath)
}

# ===== 変換対象FDF/PDFの選択 =====
# 起動直後にFDF/PDF一覧を表示する。
# - チェックしたファイルだけ変換
# - フォルダ内の全FDF/PDFを一括変換
# の両方に対応する。
function Select-ConvertTargetFdfs {
    param(
        [string]$DefaultPath
    )

    if (!(Test-Path -LiteralPath $DefaultPath)) {
        $DefaultPath = $baseFolder
    }

    $script:FDF_TARGET_FOLDER = $DefaultPath
    $script:FDF_ALL_FILES = @()
    $script:FDF_RETURN_FILES = $null

    $form = New-Object System.Windows.Forms.Form
    $form.Text = "変換するFDF/PDFを選択"
    $form.StartPosition = "CenterScreen"
    $form.Size = New-Object System.Drawing.Size(760, 560)
    $form.FormBorderStyle = "FixedDialog"
    $form.MaximizeBox = $false
    $form.MinimizeBox = $false

    $labelFolder = New-Object System.Windows.Forms.Label
    $labelFolder.Text = "対象フォルダ:"
    $labelFolder.AutoSize = $true
    $labelFolder.Location = New-Object System.Drawing.Point(20, 18)
    $form.Controls.Add($labelFolder)

    $textFolder = New-Object System.Windows.Forms.TextBox
    $textFolder.ReadOnly = $true
    $textFolder.Location = New-Object System.Drawing.Point(20, 42)
    $textFolder.Size = New-Object System.Drawing.Size(570, 24)
    $form.Controls.Add($textFolder)

    $btnChangeFolder = New-Object System.Windows.Forms.Button
    $btnChangeFolder.Text = "フォルダ変更"
    $btnChangeFolder.Size = New-Object System.Drawing.Size(120, 28)
    $btnChangeFolder.Location = New-Object System.Drawing.Point(610, 40)
    $form.Controls.Add($btnChangeFolder)

    $labelHelp = New-Object System.Windows.Forms.Label
    $labelHelp.Text = "変換したいFDF/PDFにチェックを入れるか、「このフォルダを一括変換」を押してください。"
    $labelHelp.AutoSize = $true
    $labelHelp.Location = New-Object System.Drawing.Point(20, 78)
    $form.Controls.Add($labelHelp)

    $labelCount = New-Object System.Windows.Forms.Label
    $labelCount.AutoSize = $true
    $labelCount.Location = New-Object System.Drawing.Point(20, 102)
    $form.Controls.Add($labelCount)

    $list = New-Object System.Windows.Forms.ListView
    $list.Location = New-Object System.Drawing.Point(20, 130)
    $list.Size = New-Object System.Drawing.Size(710, 300)
    $list.View = [System.Windows.Forms.View]::Details
    $list.FullRowSelect = $true
    $list.GridLines = $true
    $list.CheckBoxes = $true
    $list.MultiSelect = $true

    [void]$list.Columns.Add("ファイル名", 390)
    [void]$list.Columns.Add("種類", 70)
    [void]$list.Columns.Add("更新日時", 150)
    [void]$list.Columns.Add("サイズ", 80)

    $form.Controls.Add($list)

    $btnCheckAll = New-Object System.Windows.Forms.Button
    $btnCheckAll.Text = "すべてチェック"
    $btnCheckAll.Size = New-Object System.Drawing.Size(120, 32)
    $btnCheckAll.Location = New-Object System.Drawing.Point(20, 445)
    $form.Controls.Add($btnCheckAll)

    $btnUncheckAll = New-Object System.Windows.Forms.Button
    $btnUncheckAll.Text = "チェック解除"
    $btnUncheckAll.Size = New-Object System.Drawing.Size(120, 32)
    $btnUncheckAll.Location = New-Object System.Drawing.Point(150, 445)
    $form.Controls.Add($btnUncheckAll)

    $btnSelected = New-Object System.Windows.Forms.Button
    $btnSelected.Text = "チェックしたファイルを変換"
    $btnSelected.Size = New-Object System.Drawing.Size(190, 40)
    $btnSelected.Location = New-Object System.Drawing.Point(290, 438)
    $form.Controls.Add($btnSelected)

    $btnBulk = New-Object System.Windows.Forms.Button
    $btnBulk.Text = "このフォルダを一括変換"
    $btnBulk.Size = New-Object System.Drawing.Size(180, 40)
    $btnBulk.Location = New-Object System.Drawing.Point(490, 438)
    $form.Controls.Add($btnBulk)

    $btnCancel = New-Object System.Windows.Forms.Button
    $btnCancel.Text = "キャンセル"
    $btnCancel.Size = New-Object System.Drawing.Size(110, 32)
    $btnCancel.Location = New-Object System.Drawing.Point(620, 490)
    $form.Controls.Add($btnCancel)

    function Refresh-FdfList {
        $script:FDF_ALL_FILES = @(Get-ChildItem -LiteralPath $script:FDF_TARGET_FOLDER -File -ErrorAction SilentlyContinue | Where-Object {
            $_.Extension -ieq ".fdf" -or $_.Extension -ieq ".pdf"
        } | Sort-Object Name)

        $textFolder.Text = $script:FDF_TARGET_FOLDER
        $list.Items.Clear()

        foreach ($f in $script:FDF_ALL_FILES) {
            $item = New-Object System.Windows.Forms.ListViewItem($f.Name)
            [void]$item.SubItems.Add($f.Extension.TrimStart(".").ToUpper())
            [void]$item.SubItems.Add($f.LastWriteTime.ToString("yyyy/MM/dd HH:mm"))
            [void]$item.SubItems.Add(("{0:N0} KB" -f [Math]::Ceiling($f.Length / 1024)))
            $item.Tag = $f
            [void]$list.Items.Add($item)
        }

        if ($script:FDF_ALL_FILES.Count -gt 0) {
            $labelCount.Text = "FDF/PDFファイル数: $($script:FDF_ALL_FILES.Count)"
            $btnBulk.Enabled = $true
            $btnSelected.Enabled = $true
            $btnCheckAll.Enabled = $true
            $btnUncheckAll.Enabled = $true
        } else {
            $labelCount.Text = "FDF/PDFファイル数: 0  ※このフォルダにはFDF/PDFがありません"
            $btnBulk.Enabled = $false
            $btnSelected.Enabled = $false
            $btnCheckAll.Enabled = $false
            $btnUncheckAll.Enabled = $false
        }
    }

    $btnChangeFolder.Add_Click({
        $folder = Select-Folder `
            -Title "FDF/PDFフォルダを選択してください" `
            -DefaultPath $script:FDF_TARGET_FOLDER

        if ($null -ne $folder) {
            $script:FDF_TARGET_FOLDER = $folder
            Refresh-FdfList
            $form.Activate()
        }
    })

    $btnCheckAll.Add_Click({
        foreach ($item in $list.Items) {
            $item.Checked = $true
        }
    })

    $btnUncheckAll.Add_Click({
        foreach ($item in $list.Items) {
            $item.Checked = $false
        }
    })

    $btnSelected.Add_Click({
        $checkedFiles = @()

        foreach ($item in $list.CheckedItems) {
            $checkedFiles += $item.Tag
        }

        if ($checkedFiles.Count -eq 0) {
            [System.Windows.Forms.MessageBox]::Show(
                "変換するFDF/PDFファイルにチェックを入れてください。",
                "ファイル未選択",
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            ) | Out-Null
            return
        }

        $script:FDF_RETURN_FILES = $checkedFiles
        $form.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $form.Close()
    })

    $btnBulk.Add_Click({
        if ($script:FDF_ALL_FILES.Count -eq 0) {
            return
        }

        $script:FDF_RETURN_FILES = $script:FDF_ALL_FILES
        $form.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $form.Close()
    })

    $btnCancel.Add_Click({
        $script:FDF_RETURN_FILES = $null
        $form.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
        $form.Close()
    })

    Refresh-FdfList

    $result = $form.ShowDialog()

    if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
        return $script:FDF_RETURN_FILES
    }

    return $null
}

# ===== 初期表示フォルダ =====
$defaultSrcFolder = Join-Path $baseFolder "旧_保存データ"
if (!(Test-Path -LiteralPath $defaultSrcFolder)) {
    $defaultSrcFolder = $baseFolder
}

$defaultOutFolder = Join-Path $baseFolder "保存データ"
if (!(Test-Path -LiteralPath $defaultOutFolder)) {
    $defaultOutFolder = $baseFolder
}

# ===== 変換対象と保存先を選択 =====
$srcFiles = Select-ConvertTargetFdfs -DefaultPath $defaultSrcFolder

if ($null -eq $srcFiles -or @($srcFiles).Count -eq 0) {
    Write-Host "キャンセルしました。"
    Read-Host "Enterキーで終了"
    exit
}

$outFolder = Select-Folder `
    -Title "変換後FDFの保存先フォルダを選択してください" `
    -DefaultPath $defaultOutFolder

if ($null -eq $outFolder) {
    Write-Host "キャンセルしました。"
    Read-Host "Enterキーで終了"
    exit
}

if (!(Test-Path -LiteralPath $outFolder)) {
    New-Item -ItemType Directory -Path $outFolder | Out-Null
}

# 保存先フォルダに form.pdf をコピーする。
Copy-Item -LiteralPath $formPdfPath -Destination (Join-Path $outFolder "form.pdf") -Force

# 変換後FDFから見た新PDFフォームの相対パス。
$newPdfRef = "form.pdf"

# ===== 旧 → 新 フィールド名変換 =====
$renameMap = @{
    "birth_wareki" = "KOYOMI"
    "birth_year"   = "year"
    "birth_month"  = "month"
    "birth_date"   = "day"

    "input_date"   = "input_day"

    # 旧フォームの作成回数。直接 INPUT_TIMES には出さず、後で 0/1 に正規化する。
    "input_times"  = "INPUT_TIMES_OLD"

    # 旧医師名は現在の Dr_name に入れず、退避用 old_Dr_name へ移す。
    "Dr_name"      = "old_Dr_name"
}

# ===== 新フォームに残すフィールド一覧 =====
# ボタン、ラベル、印刷設定、旧PDFにしかない項目は除外する。
$allowedFields = @(
    "BMI","BS","BS_hours","BW",
    "old_Dr_name",

    "HDL_cho","HbA1c","Height",
    "INPUT_TIMES","INPUT_TIMES_OLD","input_times_count","input_times_count_date","KOYOMI",
    "LDL_cho","Result_BW","TG","Tcho",

    "alchol","alchol_times",
    "caution_exercise",

    "chbox_BMI","chbox_BP","chbox_BW","chbox_HbA1c",
    "chbox_diet","chbox_diet0","chbox_diet1","chbox_diet2","chbox_diet3",
    "chbox_diet4","chbox_diet5","chbox_diet6","chbox_diet7","chbox_diet8","chbox_diet9",

    "chbox_others","chbox_others1","chbox_others2","chbox_others3","chbox_others4","chbox_others5","chbox_others6",
    "chbox_smoking","chbox_smoking2","chbox_smoking3",
    "chbox_sports","chbox_sports0","chbox_sports1","chbox_sports2","chbox_sports3",

    "daily_exercise","day","dia_Bp",
    "diet_others","eatting_style",

    "exercise_HR","exercise_intensity","exercise_kind","exercise_min","exercise_other","exercise_time",

    "food","food_times",
    "goal","goal_behavior",

    # 旧日付は直接移植しないが、zenkai_year/month/day 退避のため読み取る。
    "input_day","input_month","input_year",

    "labo1","labo10","labo2","labo6","labo7","labo8","labo9",
    "labo_HbA1c","labo_date","labo_month",

    "main_problem","month",
    "nutrition",
    "other_labo","other_others",
    "others1","others2",

    "patient_id","patient_name",

    "state_goal","state_goal_d",
    "sys_Bp",

    "text_Gaisyoku","text_other",
    "when_BS",

    "year","yearsold"
)

# ===== 変換準備 =====
$enc = [System.Text.Encoding]::GetEncoding(28591)

$allowedSet = [System.Collections.Generic.HashSet[string]]::new()
$allowedFields | ForEach-Object { [void]$allowedSet.Add($_) }

# 旧FDFの日付、年齢、初回/継続は直接出力しない。
# 日付は本日で作り直し、旧日付は zenkai_* に退避する。
# INPUT_TIMES は旧 input_times を 0/1 に正規化して出力する。
$skipDirectFields = @("input_year","input_month","input_day","yearsold","INPUT_TIMES","INPUT_TIMES_OLD","input_times_count","input_times_count_date","old_Dr_name")
$skipDirectSet = [System.Collections.Generic.HashSet[string]]::new()
$skipDirectFields | ForEach-Object { [void]$skipDirectSet.Add($_) }

function Escape-PdfLiteral($s) {
    $s = $s.Replace("\", "\\")
    $s = $s.Replace("(", "\(")
    $s = $s.Replace(")", "\)")
    return $s
}

function Read-PdfLiteral($s, $openIndex) {
    $depth = 0
    for ($i = $openIndex; $i -lt $s.Length; $i++) {
        $c = $s[$i]

        if ($c -eq "\") {
            $i++
            continue
        }

        if ($c -eq "(") {
            $depth++
        }
        elseif ($c -eq ")") {
            $depth--
            if ($depth -eq 0) {
                return @{
                    Text  = $s.Substring($openIndex, $i - $openIndex + 1)
                    Inner = $s.Substring($openIndex + 1, $i - $openIndex - 1)
                    End   = $i
                }
            }
        }
    }
    return $null
}

function Unescape-PdfName($s) {
    $s = $s.Replace("\(", "(")
    $s = $s.Replace("\)", ")")
    $s = $s.Replace("\\", "\")
    return $s
}

function Get-PlainPdfValue($v) {
    if ($null -eq $v) { return "" }

    # /V/Yes, /V/Off, /V/2nd など。
    if ($v.Text.StartsWith("/")) {
        return $v.Inner.TrimStart("/")
    }

    # /V<FEFF...>
    if ($v.Text.StartsWith("<")) {
        return Decode-PdfHexString $v.Text
    }

    # /V(...)
    return (Unescape-PdfName $v.Inner)
}

function Make-PdfLiteralValue($value) {
    if ($null -eq $value) { $value = "" }
    return "(" + (Escape-PdfLiteral ([string]$value)) + ")"
}

function Make-PdfHexStringValue($value) {
    # PDF/FDFの文字列をUTF-16BEのHex stringで作る。
    # 日本語を含むPDFフォーム値をPDFから読み出してFDFに戻すために使う。
    if ($null -eq $value) { $value = "" }
    $bytes = New-Object System.Collections.Generic.List[byte]
    [void]$bytes.Add(0xFE)
    [void]$bytes.Add(0xFF)
    [byte[]]$body = [System.Text.Encoding]::BigEndianUnicode.GetBytes([string]$value)
    foreach ($b in $body) { [void]$bytes.Add($b) }
    $hex = (($bytes.ToArray() | ForEach-Object { $_.ToString("X2") }) -join "")
    return "<$hex>"
}

function Decode-PdfHexString($hexText) {
    # <FEFF....> 形式のFDF値を可能なら文字列に戻す。
    $h = ([string]$hexText).Trim()
    $h = $h.TrimStart("<").TrimEnd(">")
    $h = ($h -replace "\s", "")
    if ($h.Length -lt 2 -or ($h.Length % 2) -ne 0) { return "" }

    $bytes = New-Object byte[] ($h.Length / 2)
    for ($i = 0; $i -lt $bytes.Length; $i++) {
        $bytes[$i] = [Convert]::ToByte($h.Substring($i * 2, 2), 16)
    }

    if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
        return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
    }
    if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
        return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
    }

    return $enc.GetString($bytes)
}

function Escape-PdfNameObject($value) {
    # ラジオボタン/チェックボックスの /V/xxx 用。
    # ASCII以外はUTF-8バイトの #XX エスケープにする。
    if ($null -eq $value) { $value = "Off" }
    $s = [string]$value
    if ($s.StartsWith("/")) { $s = $s.Substring(1) }
    if ($s -eq "") { $s = "Off" }

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($s)
    $sb = New-Object System.Text.StringBuilder
    foreach ($b in $bytes) {
        $ch = [char]$b
        $isRegular = (($b -ge 0x30 -and $b -le 0x39) -or ($b -ge 0x41 -and $b -le 0x5A) -or ($b -ge 0x61 -and $b -le 0x7A) -or $ch -eq "_" -or $ch -eq "-" -or $ch -eq ".")
        if ($isRegular) {
            [void]$sb.Append($ch)
        } else {
            [void]$sb.Append("#" + $b.ToString("X2"))
        }
    }
    return $sb.ToString()
}

function Make-FdfRawValueFromPdfField($fieldType, $value) {
    if ($null -eq $value) { $value = "" }
    $typeText = ([string]$fieldType).ToLowerInvariant()

    if ($typeText -eq "checkbox" -or $typeText -eq "radiobutton") {
        return "/" + (Escape-PdfNameObject $value)
    }

    return Make-PdfHexStringValue $value
}

function ToIntOrNull($x) {
    if ($null -eq $x) { return $null }
    $s = ([string]$x).Trim()
    if ($s -eq "") { return $null }

    $n = 0
    if ([int]::TryParse($s, [ref]$n)) {
        return $n
    }
    return $null
}

function Calc-AgeFromFields($vals, [datetime]$baseDate) {
    $eraBase = ToIntOrNull $vals["KOYOMI"]
    $y = ToIntOrNull $vals["year"]
    $m = ToIntOrNull $vals["month"]
    $d = ToIntOrNull $vals["day"]

    if ($null -eq $y -or $null -eq $m -or $null -eq $d) {
        return $null
    }

    # KOYOMI が 1925/1988/2018 などなら和暦。
    # 例: 昭和50年 = 1925 + 50 = 1975
    if ($null -ne $eraBase -and $eraBase -gt 0 -and $y -lt 1900) {
        $birthYear = $eraBase + $y
    } else {
        $birthYear = $y
    }

    try {
        $birth = [datetime]::new($birthYear, $m, $d)
    } catch {
        return $null
    }

    $age = $baseDate.Year - $birth.Year
    if ($baseDate.Date -lt $birth.AddYears($age).Date) {
        $age--
    }

    return $age
}

function Read-PdfValue($s, $vpos) {
    $i = $vpos + 2

    while ($i -lt $s.Length -and [char]::IsWhiteSpace($s[$i])) {
        $i++
    }

    if ($i -ge $s.Length) { return $null }

    # /V(...)
    if ($s[$i] -eq "(") {
        return Read-PdfLiteral $s $i
    }

    # /V<FEFF...> など。
    if ($s[$i] -eq "<" -and ($i + 1 -lt $s.Length) -and $s[$i + 1] -ne "<") {
        $end = $s.IndexOf(">", $i + 1)
        if ($end -ge 0) {
            return @{
                Text  = $s.Substring($i, $end - $i + 1)
                Inner = $s.Substring($i + 1, $end - $i - 1)
                End   = $end
            }
        }
    }

    # /V/Yes など。
    if ($s[$i] -eq "/") {
        $start = $i
        $i++
        while ($i -lt $s.Length) {
            $c = $s[$i]
            if ([char]::IsWhiteSpace($c) -or $c -eq ">" -or $c -eq "]") {
                break
            }
            $i++
        }
        return @{
            Text  = $s.Substring($start, $i - $start)
            Inner = $s.Substring($start, $i - $start)
            End   = $i - 1
        }
    }

    return $null
}


function Get-FdfFieldRecords($path) {
    $bytes = [System.IO.File]::ReadAllBytes($path)
    $text = $enc.GetString($bytes)

    $records = New-Object System.Collections.Generic.List[object]
    $pos = 0

    while ($true) {
        # /T(field) と /T (field) の両方を拾う。
        $tMatch = $tRegex.Match($text, $pos)
        if (!$tMatch.Success) { break }

        $tOpen = $text.IndexOf("(", $tMatch.Index)
        if ($tOpen -lt 0) { break }

        $tLit = Read-PdfLiteral $text $tOpen
        if ($null -eq $tLit) { break }

        $fieldName = Unescape-PdfName $tLit.Inner

        $nextTMatch = $tRegex.Match($text, $tLit.End + 1)
        if ($nextTMatch.Success) {
            $nextT = $nextTMatch.Index
        } else {
            $nextT = -1
        }

        $vpos = $text.IndexOf("/V", $tLit.End + 1)

        if ($vpos -lt 0) {
            $pos = $tLit.End + 1
            continue
        }

        if ($nextT -ge 0 -and $nextT -lt $vpos) {
            $pos = $tLit.End + 1
            continue
        }

        $v = Read-PdfValue $text $vpos
        if ($null -eq $v) {
            $pos = $tLit.End + 1
            continue
        }

        $records.Add([pscustomobject]@{
            Name       = $fieldName
            RawValue   = $v.Text
            PlainValue = (Get-PlainPdfValue $v)
            Source     = "FDF"
        })

        $pos = $v.End + 1
    }

    return $records
}

function Get-PdfFieldRecords($path) {
    # Acrobat/ReaderのCOMを使ってPDFフォームの値を読み出す。
    # Acrobat Pro DC 2015 のようにAcrobatが入っているPCで動作する想定。
    $records = New-Object System.Collections.Generic.List[object]

    $pdDoc = $null

    try {
        # AVDocではなくPDDocで開く。画面にPDFを表示しにくく、変換処理向き。
        $pdDoc = New-Object -ComObject AcroExch.PDDoc
        $opened = $pdDoc.Open($path)
        if (-not $opened) {
            throw "PDFをAcrobatで開けませんでした。"
        }

        $jso = $pdDoc.GetJSObject()

        $n = [int]$jso.numFields
        for ($i = 0; $i -lt $n; $i++) {
            $name = [string]$jso.getNthFieldName($i)
            if ([string]::IsNullOrWhiteSpace($name)) { continue }

            $field = $jso.getField($name)
            if ($null -eq $field) { continue }

            $type = ""
            try { $type = [string]$field.type } catch { $type = "" }

            # ボタン類は値移行対象外。allowedFieldsでも落ちるが、ここで軽く除外する。
            if ($type -eq "button") { continue }

            $value = ""
            try {
                $value = [string]$field.value
            } catch {
                $value = ""
            }

            $raw = Make-FdfRawValueFromPdfField $type $value

            $records.Add([pscustomobject]@{
                Name       = $name
                RawValue   = $raw
                PlainValue = $value
                Source     = "PDF"
            })
        }
    }
    finally {
        if ($null -ne $pdDoc) {
            try { [void]$pdDoc.Close() } catch { }
        }
    }

    return $records
}

function Get-SourceFieldRecords($path) {
    $ext = [System.IO.Path]::GetExtension($path).ToLowerInvariant()
    if ($ext -eq ".fdf") {
        return Get-FdfFieldRecords $path
    }
    if ($ext -eq ".pdf") {
        return Get-PdfFieldRecords $path
    }
    throw "対応していないファイル形式です: $ext"
}

function Get-DateFromFileName($fileName) {
    # 例: 0000000_名無しさん2026年7月3日.pdf から日付を拾う。
    $m = [regex]::Match($fileName, "(?<y>20\d{2})年(?<m>\d{1,2})月(?<d>\d{1,2})日")
    if (-not $m.Success) { return $null }

    try {
        return [datetime]::new(
            [int]$m.Groups["y"].Value,
            [int]$m.Groups["m"].Value,
            [int]$m.Groups["d"].Value
        )
    } catch {
        return $null
    }
}

function Make-DateKeyFromParts($y, $m, $d) {
    $yy = ToIntOrNull $y
    $mm = ToIntOrNull $m
    $dd = ToIntOrNull $d

    if ($null -eq $yy -or $null -eq $mm -or $null -eq $dd) {
        return ""
    }

    try {
        $dt = [datetime]::new($yy, $mm, $dd)
        return $dt.ToString("yyyy-MM-dd")
    } catch {
        return ""
    }
}

function Normalize-TimesCount($text) {
    # input_times_count は「前回までの回数」として保存する。
    # 旧FDFの input_times が数値ならそのまま使用。
    # 旧PDFの INPUT_TIMES が 1st/2nd のようなラジオ値なら、1st=0、それ以外=1 とする。
    if ($null -eq $text) { return "" }

    $s = ([string]$text).Trim()
    if ($s -eq "") { return "" }

    $n = ToIntOrNull $s
    if ($null -ne $n) { return [string]$n }

    $lower = $s.ToLowerInvariant()
    if ($lower -eq "1st") { return "0" }

    # 2nd などは「継続」として扱う。実際の回数は不明なので 1 とする。
    return "1"
}

# ===== 変換本体 =====
$newF = Escape-PdfLiteral $newPdfRef
$log = New-Object System.Collections.Generic.List[string]

# 検索開始位置つきの Match(input, startAt) を使うため、Regexオブジェクトを作る。
$tRegex = New-Object System.Text.RegularExpressions.Regex -ArgumentList "/T\s*\("

@($srcFiles) | ForEach-Object {
    try {
        $records = @(Get-SourceFieldRecords $_.FullName)

        $fieldsOut = New-Object System.Collections.Generic.List[string]
        $fieldValues = @{}
        $fieldRawValues = @{}
        $today = Get-Date

        foreach ($rec in $records) {
            $originalFieldName = [string]$rec.Name

            # 新PDF由来の old_Dr_name は古い退避値の可能性があるので無視する。
            # Dr_name を old_Dr_name に退避する処理だけを採用する。
            if ($originalFieldName -eq "old_Dr_name") {
                continue
            }

            $fieldName = $originalFieldName
            if ($renameMap.ContainsKey($fieldName)) {
                $fieldName = $renameMap[$fieldName]
            }

            if ($allowedSet.Contains($fieldName)) {
                $fieldValues[$fieldName] = [string]$rec.PlainValue
                $fieldRawValues[$fieldName] = [string]$rec.RawValue

                if (-not $skipDirectSet.Contains($fieldName)) {
                    $safeName = Escape-PdfLiteral $fieldName
                    $fieldsOut.Add("<< /T($safeName) /V$($rec.RawValue) >>")
                }
            }
        }

        # 旧日付フィールドが無いPDFでは、ファイル名から日付を拾える場合だけ前回日付に使う。
        if (-not ($fieldValues.ContainsKey("input_year") -and $fieldValues.ContainsKey("input_month") -and $fieldValues.ContainsKey("input_day"))) {
            $dateFromName = Get-DateFromFileName $_.Name
            if ($null -ne $dateFromName) {
                if (-not $fieldValues.ContainsKey("input_year")) { $fieldValues["input_year"] = [string]$dateFromName.Year }
                if (-not $fieldValues.ContainsKey("input_month")) { $fieldValues["input_month"] = [string]$dateFromName.Month }
                if (-not $fieldValues.ContainsKey("input_day")) { $fieldValues["input_day"] = [string]$dateFromName.Day }
            }
        }

        # 旧FDFの日付を「前回日付」として隠しフィールドへ退避する。
        if ($fieldValues.ContainsKey("input_year")) {
            $zyVal = Make-PdfLiteralValue $fieldValues["input_year"]
            $fieldsOut.Add("<< /T(zenkai_year) /V$zyVal >>")
        }
        if ($fieldValues.ContainsKey("input_month")) {
            $zmVal = Make-PdfLiteralValue $fieldValues["input_month"]
            $fieldsOut.Add("<< /T(zenkai_month) /V$zmVal >>")
        }
        if ($fieldValues.ContainsKey("input_day")) {
            $zdVal = Make-PdfLiteralValue $fieldValues["input_day"]
            $fieldsOut.Add("<< /T(zenkai_day) /V$zdVal >>")
        }

        # 旧医師名を退避する。Dr_name本体はFDFに出さず、form.pdf側の現在値を使う。
        if ($fieldRawValues.ContainsKey("old_Dr_name")) {
            $fieldsOut.Add("<< /T(old_Dr_name) /V$($fieldRawValues["old_Dr_name"]) >>")
        }

        # 表示上の記入日は変換時点の本日にする。
        $fieldsOut.Add("<< /T(input_year) /V($($today.Year)) >>")
        $fieldsOut.Add("<< /T(input_month) /V($($today.Month)) >>")
        $fieldsOut.Add("<< /T(input_day) /V($($today.Day)) >>")

        # 年齢は本日の記入日に合わせて可能なら再計算する。
        $age = Calc-AgeFromFields $fieldValues $today
        if ($null -ne $age) {
            $fieldsOut.Add("<< /T(yearsold) /V($age) >>")
        }

        # 旧 input_times / INPUT_TIMES を新 INPUT_TIMES に変換する。
        # 新PDF側のラジオボタン export value は、初回=0、継続=1 を想定。
        $oldTimesText = $null
        if ($fieldValues.ContainsKey("INPUT_TIMES_OLD")) {
            $oldTimesText = [string]$fieldValues["INPUT_TIMES_OLD"]
        }
        elseif ($fieldValues.ContainsKey("INPUT_TIMES")) {
            $oldTimesText = [string]$fieldValues["INPUT_TIMES"]
        }

        if ($null -ne $oldTimesText -and $oldTimesText.Trim() -ne "") {
            $oldTimesText = $oldTimesText.Trim()
            $oldTimesLower = $oldTimesText.ToLowerInvariant()
            $oldTimesNumber = ToIntOrNull $oldTimesText

            if (($null -ne $oldTimesNumber -and $oldTimesNumber -eq 0) -or $oldTimesLower -eq "1st") {
                $newInputTimesValue = "0"
            } else {
                $newInputTimesValue = "1"
            }

            $fieldsOut.Add("<< /T(INPUT_TIMES) /V/$newInputTimesValue >>")
        }

        # input_times_count は「前回までの回数」として入れる。
        # 保存ボタン側で、記入日が input_times_count_date と違うときだけ +1 する。
        $timesCountText = ""
        if ($fieldValues.ContainsKey("input_times_count") -and ([string]$fieldValues["input_times_count"]).Trim() -ne "") {
            $timesCountText = Normalize-TimesCount $fieldValues["input_times_count"]
        }
        elseif ($null -ne $oldTimesText -and $oldTimesText.Trim() -ne "") {
            $timesCountText = Normalize-TimesCount $oldTimesText
        }

        if ($timesCountText -ne "") {
            $fieldsOut.Add("<< /T(input_times_count) /V$(Make-PdfLiteralValue $timesCountText) >>")
        }

        # input_times_count_date は、必ず「前回書類の記入日」を入れる。
        # これが空だと、保存JSが初回保存か再保存か判定できず、回数が不安定になる。
        $previousDateKey = Make-DateKeyFromParts $fieldValues["input_year"] $fieldValues["input_month"] $fieldValues["input_day"]
        if ($previousDateKey -eq "" -and $fieldValues.ContainsKey("input_times_count_date")) {
            $previousDateKey = ([string]$fieldValues["input_times_count_date"]).Trim()
        }

        if ($previousDateKey -ne "") {
            $fieldsOut.Add("<< /T(input_times_count_date) /V$(Make-PdfLiteralValue $previousDateKey) >>")
        }

        $newFdf =
            "%FDF-1.2`r" +
            "%âãÏÓ`r" +
            "1 0 obj`r" +
            "<< /FDF <<`r" +
            "/F($newF)`r" +
            "/UF($newF)`r" +
            "/Fields [`r" +
            ($fieldsOut -join "`r") + "`r" +
            "]`r" +
            ">> >>`r" +
            "endobj`r" +
            "trailer`r" +
            "<< /Root 1 0 R >>`r" +
            "%%EOF`r"

        $outName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) + ".fdf"
        $outPath = Join-Path $outFolder $outName
        [System.IO.File]::WriteAllBytes($outPath, $enc.GetBytes($newFdf))

        $log.Add("OK  $($_.Name) -> $outName  fields=$($fieldsOut.Count)")
    }
    catch {
        $log.Add("NG  $($_.Name)  $($_.Exception.Message)")
    }
}

# ===== ログ出力 =====
$logPath = Join-Path $outFolder "_convert_log.txt"
[System.IO.File]::WriteAllLines($logPath, $log, [System.Text.Encoding]::UTF8)

Write-Host "完了"
Write-Host "変換したファイル数: $(@($srcFiles).Count)"
Write-Host "保存先: $outFolder"
Write-Host "ログ: $logPath"

Read-Host "Enterキーで終了"
