#{.link: "C:/NimDev/FD_Linker_Listen.res".}
{.passL: "-mconsole".}
import os, times, strutils, sequtils, re, winim, net, endians, nativesockets, encodings
import tables, osproc, asyncdispatch, asyncnet
import std/parsecfg # os と tables は上で読み込んでいるので削ってOK


# --- 1. パスと器の宣言（ここをこのまま貼り付けてください） ---
let 
  appDir = getAppDir()
  learnPath = appDir / "learn.txt"
  configPath = appDir / "config.txt"

var learnData = initTable[string, string]()

# --- 2. 起動時の自動読み込み処理（順次解決版） ---
if fileExists(learnPath):
  try:
    # ライブラリの関数を信じず、1行ずつ自力でバラして読み込む
    for line in lines(learnPath):
      let pair = line.split('=')
      if pair.len == 2:
        # learn.txt の中身（Size150_CallOffset=16 等）を直接メモリに入れる
        learnData[pair[0].strip()] = pair[1].strip()
  except: discard

proc updateLearnTxt(packetSize: int, key: string, pos: int) =
  let section = "Size" & $packetSize
  learnData[section & "_" & key] = $pos
  try:
    var cfg = if fileExists(learnPath): loadConfig(learnPath) else: newConfig()
    cfg.setSectionKey(section, key, $pos)
    cfg.writeConfig(learnPath)
  except: discard

# --- ここから下は VkMapping の定義へ続く ---

let VkMapping = {
  # --- 数字 ---
  '0': 0x30.Byte, '1': 0x31.Byte, '2': 0x32.Byte, '3': 0x33.Byte, '4': 0x34.Byte,
  '5': 0x35.Byte, '6': 0x36.Byte, '7': 0x37.Byte, '8': 0x38.Byte, '9': 0x39.Byte,
  
  # --- アルファベット (コールサイン・GL用) ---
  'A': 0x41.Byte, 'B': 0x42.Byte, 'C': 0x43.Byte, 'D': 0x44.Byte, 'E': 0x45.Byte,
  'F': 0x46.Byte, 'G': 0x47.Byte, 'H': 0x48.Byte, 'I': 0x49.Byte, 'J': 0x4A.Byte,
  'K': 0x4B.Byte, 'L': 0x4C.Byte, 'M': 0x4D.Byte, 'N': 0x4E.Byte, 'O': 0x4F.Byte,
  'P': 0x50.Byte, 'Q': 0x51.Byte, 'R': 0x52.Byte, 'S': 0x53.Byte, 'T': 0x54.Byte,
  'U': 0x55.Byte, 'V': 0x56.Byte, 'W': 0x57.Byte, 'X': 0x58.Byte, 'Y': 0x59.Byte,
  'Z': 0x5A.Byte,
  
  # --- 記号 (SNR・書式用) ---
  '+': 0xBB.Byte, '-': 0xBD.Byte, ':': 0xBA.Byte, 
  ' ': 0x20.Byte, '.': 0xBE.Byte, '/': 0xBF.Byte
}.toTable

discard SetConsoleOutputCP(65001)

const
  COLOR_SKYBLUE = FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_INTENSITY
  COLOR_WHITE = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
  COLOR_GREEN = FOREGROUND_GREEN or FOREGROUND_INTENSITY

proc setColor(color: WORD) =
  let hConsole = GetStdHandle(STD_OUTPUT_HANDLE)
  SetConsoleTextAttribute(hConsole, color)

var 
  # --- 1. 設定ファイル(config.txt)から読み込む固定値 ---
  confPort: int = 2237
  confWait: int = 3
  confMyCall: string = "JH0PCF"
  confMyGL: string = "PM96DP"
  confEnableSNR: int = 0      # 0: SNR出力を眠らせる, 1: 起こす
  confSNRTarget: int = 0      # 0:RS欄, 1:Remarks欄
  confRemarksTarget: int = 1  # 1:Remarks1, 2:Remarks2
  confAutoSave: int = 0       # 0:転送のみ, 1:Alt+I, 2:Alt+S
  confLaunchHamlog: int = 0   # 自動起動スイッチ
  confHamlogPath: string = "C:\\Hamlog\\Hamlogw.exe"
  socket: AsyncSocket  

  # --- 2. 解析・転送プロセスで使う「動的な器（プール）」 ---
  savedCall: string = ""      # 追加
  savedFreq: string = ""      # 追加
  savedGL: string = ""        # 追加
  savedRS: string = ""
  savedSNR: string = ""       # 相手へのSNRレポート(S)
  savedMyReport: string = ""  # 自分へのSNRレポート(R)
# --- 【新設：自己学習・スキャン用変数】 ---
  snrOffset: int = 29           # デフォルトは29、学習で書き換わる
  anchorText: string = "FreeDV" # 灯台の正体

  # --- 3. パケットから直接抜く一時的な作業用 ---
  exSent: string = ""
  exRecv: string = ""
  myReport: string = ""

  # --- 4. 解析の「歩き方」と「二重解析防止」の記録 ---
  cursor: int = 0             # ★ これがないとパケットをスキャンできません
  lastCall: string = ""       # ★ これがないと二重転送を防げません
  lastTime: float = 0.0       # ★ これがないと時間判定でエラーになります

type
  COPYDATASTRUCT = object
    dwData: ULONG_PTR
    cbData: DWORD
    lpData: PVOID

proc loadConfig() =
  let path = "config.txt"
  if fileExists(path):
    for line in lines(path):
      if line.startsWith("#") or line.strip() == "": continue
      let parts = line.split('=')
      if parts.len == 2:
        let key = parts[0].strip().toLowerAscii()
        let val = parts[1].strip()
        case key
        of "port": confPort = parseInt(val)
        of "mycall": confMyCall = val.toUpperAscii()
        of "mygl": 
          confMyGL = val.toUpperAscii()
        of "waittime": confWait = parseInt(val)
        of "auto_save": confAutoSave = parseInt(val)
        of "enablesnr": confEnableSNR = parseInt(val)
        of "snrtarget": confSNRTarget = parseInt(val)
        of "remarkstarget": confRemarksTarget = parseInt(val)
        of "launch_hamlog": confLaunchHamlog = parseInt(val)
        of "hamlog_path": confHamlogPath = val.replace("\\", "\\\\") # パスの \ を調整

proc checkAndLaunchHamlog() =
  # --- 1. config.txt で「0 (起動しない)」に設定されていたらすぐ終わる ---
  if confLaunchHamlog == 0: return

  # ハムログの入力ウインドウを探す
  let hwnd = FindWindow("THamlogInput", nil)
  
  # いないときだけ「起動処理」に入る  
  if hwnd == 0:
    setColor(COLOR_GREEN)
    # --- 2. 固定パスではなく、設定ファイルのパス（confHamlogPath）を表示 ---
    echo "  -> ハムログが起動していないようです。起動を試みます..."
    echo "      PATH: ", confHamlogPath
    
    # --- 3. 設定ファイルのパスを使って起動チェック ---
    if fileExists(confHamlogPath):
      try:
        discard startProcess(confHamlogPath)
        echo "  -> ハムログを起動しました。1秒待機します..."
        sleep(1000)
      except:
        echo "  [エラー] 起動に失敗しました。パスを確認してください。"
    else:
      echo "  [エラー] 指定された場所に Hamlogw.exe がありません。"
      echo "           config.txt のパスを確認してください。"
    setColor(COLOR_GREEN)

proc readInt32(data: string): int32 =
  if cursor + 4 > data.len: return 0
  var val, raw: int32
  copyMem(addr raw, addr data[cursor], 4);
  swapEndian32(addr val, addr raw); cursor += 4; return val

proc readInt64(data: string): int64 =
  if cursor + 8 > data.len: return 0
  var val, raw: int64
  copyMem(addr raw, addr data[cursor], 8);
  swapEndian64(addr val, addr raw); cursor += 8; return val

proc readString(data: string): string =
  let length = readInt32(data)
  if length <= 0 or cursor + length > data.len: return ""
  result = data[cursor ..< cursor + length];
  cursor += length

proc parsePacket(data: string): (string, string, string, string) =
  let sizeKey = "Size" & $data.len
  var call, freq, gl, snr: string
  
  # --- 1. 学習済みの位置（Offset）から高速抽出 ---
  let savedCallPos = learnData.getOrDefault(sizeKey & "_CallOffset", "-1").parseInt()
  let savedRSOffset = learnData.getOrDefault(sizeKey & "_RS_Offset", "-1").parseInt()
  
  if savedCallPos != -1 and savedCallPos + 5 < data.len:
    cursor = savedCallPos
    call = readString(data)
  
  if savedRSOffset != -1 and savedRSOffset + 3 <= data.len:
    snr = data[savedRSOffset .. savedRSOffset+2].strip()
  if savedSNR == "" or savedSNR == "0.0":
    savedSNR = snr

  # --- 2. 未学習、または失敗時の「自力解析 ＆ 学習」 ---
  if call == "" or call == confMyCall:
    cursor = 12
    discard readString(data) 
    
    if data.len >= 150:
      discard readInt64(data)
      let currentPos = cursor # ここがコールサインの開始位置
      call = readString(data)
      gl = readString(data)
      let hz = readInt64(data)
      freq = formatFloat(hz.float / 1_000_000.0, ffDecimal, 3)
      
      # 【コールサイン学習】
      if call != "" and call != confMyCall:
        updateLearnTxt(data.len, "CallOffset", currentPos)
    else:
      let hz = readInt64(data)
      freq = formatFloat(hz.float / 1_000_000.0, ffDecimal, 3)
      discard readString(data)
      let currentPos = cursor
      call = readString(data)
      
      # 【コールサイン学習】
      if call != "" and call != confMyCall:
        updateLearnTxt(data.len, "CallOffset", currentPos)

  # --- 3. SNR（レポート）の「自力検索 ＆ 学習」 ---
  if snr == "":
    let fullTxt = data.mapIt(if it.ord in 32..126: it else: ' ').join("")
    let plusPos = fullTxt.find("+")
    let minusPos = fullTxt.find("-")
    let foundPos = if plusPos != -1: plusPos elif minusPos != -1: minusPos else: -1
    
    if foundPos != -1 and foundPos + 3 <= fullTxt.len:
      snr = fullTxt[foundPos .. foundPos+2].strip()
    # 【JH0PCF流・29番地優先ガード】
    # すでに高精度な数値（14.0など）が入っているなら、この「推測値」で上書きしない！
    if savedSNR == "" or savedSNR == "0.0":
      savedSNR = snr
      # 【SNR学習】
      updateLearnTxt(data.len, "RS_Offset", foundPos)

  # --- 4. 結果を返却 ---
  let fullTxtFinal = data.mapIt(if it.ord in 32..126: it else: ' ').join("")
  
# --- GL抽出（V220 最終安定版：空欄防止ロジック） ---

  let allGLs = fullTxtFinal.findAll(re"[A-Za-z]{2}\d{2}([A-Za-z]{2})?")
  var targetGL: string = ""

# 1. パケット全体から「自分じゃないGL」を探す
  for found in allGLs:
    if found.toUpperAscii() != confMyGL.toUpperAscii():
     targetGL = found
     break # 見つかったら即座に確定！

# 2. もし見つからなかったら、直接取得(gl)した値をチェック
  if targetGL == "" and gl != "" and gl.toUpperAscii() != confMyGL.toUpperAscii():
    if (gl.len == 4 or gl.len == 6) and not gl[0].isDigit:
      targetGL = gl



  # 自局以外のコールサインを抽出
  if call == confMyCall or call == "":
    let calls = fullTxtFinal.findAll(re"([A-Za-z0-9]{1,2})\d[A-Za-z]{1,3}(/[A-Za-z0-9]+)?")
    for c in calls:
      if c != confMyCall and c != "FREEDV" and c != "DIGITAL":
        call = c
        break

  return (call, freq, targetGL, snr)  

proc getHamlogHwnd(): HWND =
  var targetHwnd: HWND = 0
  proc cb(hwnd: HWND, lp: LPARAM): BOOL {.stdcall.} =
    var b: array[512, WCHAR]
    if GetWindowTextW(hwnd, addr b[0], 512) > 0:
      if "Turbo HAMLOG" in $cast[ptr WCHAR](addr b[0]):
        cast[ptr HWND](lp)[] = hwnd
        return FALSE
    return TRUE
  EnumWindows(cast[WNDENUMPROC](cb), cast[LPARAM](addr targetHwnd))
  if targetHwnd != 0:
    let editHwnd = FindWindowEx(targetHwnd, 0, "TEditForm", nil)
    if editHwnd != 0: return editHwnd
  return targetHwnd

# --- ここから書き換え UTF-8 から Shift-Jisへの変換---
proc toSjis(s: string): string =
  if s == "": return ""
  # UTF-8からUTF-16(Wide)へ変換
  let wide = +$s 
  # UTF-16からShift-JIS(コードページ 932)へ変換
  let size = WideCharToMultiByte(932, 0, wide, -1, nil, 0, nil, nil)
  if size <= 0: return ""
  result = newString(size - 1)
  WideCharToMultiByte(932, 0, wide, -1, addr result[0], size, nil, nil)

proc sendToHamlog(hwnd: HWND, cmd: int, text: string) =
  if hwnd == 0 or text == "": return
  var d = text & "\0"
  var cds = COPYDATASTRUCT(dwData: cast[ULONG_PTR](cmd), cbData: cast[DWORD](d.len), lpData: cast[PVOID](addr d[0]))
  SendMessage(hwnd, WM_COPYDATA, 0, cast[LPARAM](addr cds))

proc main() =
  # --- ★ここに追加！ 自分の実行ファイルがある場所にカレントディレクトリを移動する ---
  try: os.setCurrentDir(os.getAppDir()) except: discard
  #--- 共通の鍵にする（Listen版でもTimes版でも喧嘩させる） ---
  let hMutex = CreateMutex(nil, FALSE, "FD_Linker_Common_Key")
  if GetLastError() == ERROR_ALREADY_EXISTS:
    # 喧嘩した時のメッセージ
    MessageBox(0, "FD_Linker（Listen/Times）が既に起動しています。\n二重起動はできません。", "二重起動エラー", MB_ICONERROR)
    quit(0)

  loadConfig() # ここで config.txt から新しいポート番号を読み込む

  # ★ 設定を読み込んだ「後」でソケットを作る！
  socket = newAsyncSocket(domain = AF_INET, sockType = SOCK_DGRAM, protocol = IPPROTO_UDP)
  socket.setSockOpt(OptReuseAddr, true)
  
  try:
    socket.bindAddr(Port(confPort)) # これで config.txt の値が反映されます！
  except:
    echo "[エラー] ポート ", confPort, " を開けませんでした。他で使われていませんか？"
    quit(1)

  # 2. ★ここでハムログを自動チェック＆起動！
  checkAndLaunchHamlog()
  os.sleep(1000) # 1.5秒だけ、ハムログの目覚めを待つ
  setColor(COLOR_GREEN)
  echo "======================================================"
  echo "    FD_Linker Listen[Listen Edition] Ver2.2.0"
  echo "    Original Concept by JH0PCF"
  echo "    Special Thanks to JA4JOE"
  echo "    Coding Assisted by Gemini & Copilot AI"
  echo "======================================================"
  setColor(COLOR_GREEN)
  echo "    MY CALL : ", confMyCall
  echo "    MY GL   : ", confMyGL
  echo "    PORT    : ", confPort
  echo "------------------------------------------------------"
  echo ">>> FreeDV ログ送信を待っています (Listenモード)..."
  echo "------------------------------------------------------"

## --- 2. Times版の状態管理変数 ---
var isCaptured = false
var capturedTime: string = ""  # ★Listen版でも「捕まえた時刻」を置く器が必要です

# --- 2. メインループ (非同期) ---

proc mainLoop() {.async.} =
  while true:
    # パケットが来るまで待機（CPU負荷ゼロ）
    let (data, address, port) = await socket.recvFrom(2048)

    setColor(COLOR_GREEN)

    # パケットが有効な長さ（Type 5など）かチェック
    if data.len >= 130:
      # 【核心】parsePacketの中で正規表現を使い、Call/GL/Freqを一気に抜き出す
      let (call, freq, finalGL, _) = parsePacket(data)
      
      # 周波数のバリデーション（V212の知恵）
      let fVal = try: parseFloat(freq) except: 0.0
      if fVal > 500.0 or fVal < 0.1: continue

      # 3. 自局以外の有効なコールサインを見つけたら自動キャプチャ
      if call != "" and call != confMyCall:
        if not isCaptured:
          isCaptured = true
          savedCall = call
          savedFreq = freq
          savedGL = finalGL
          # --- ここが「反省会（学習）」の追加ポイント ---
          # 成功した時の「位置」を抜き出し、learn.txtへ反映
          let actualPos = data.find(call)
          if actualPos != -1:
            updateLearnTxt(data.len, "CallOffset", actualPos)

# --- 【Type 2: あのスキャンと同じ計算式でSNRを抽出】 ---
    if data.len == 65 and uint8(data[11]) == 2:
      # あのスキャンで 13 dB と出た「Offset 29」の2バイト読みを再現
      var s16: int16
      copyMem(addr s16, addr data[29], 2)      
      
      # スキャン時と同じ「2バイト・スワップ」を実行
      var sw16: int16
      swapEndian16(addr sw16, addr s16)      
      
      # スキャンで画面に出たあの数字（13 や 12）がここに入ります
      let val = sw16.float      
      # 13.0 として保存
      savedSNR = formatFloat(val, ffDecimal, 1)
      
      setColor(COLOR_GREEN)
      echo ">>> [SNR解析結果] ", savedSNR, " dB "

      # コンソールに進捗を表示
      setColor(COLOR_GREEN)
      echo "  CALL : ", savedCall
      echo "  Freq : ", savedFreq, " MHz"
      echo "  GL   : ", savedGL
      let hwnd = getHamlogHwnd()
      if hwnd != 0:
        for i in countdown(confWait, 1):
          stdout.write("\r  Hamlog転送まで残り: " & $i & " 秒... ")
          stdout.flushFile()
          for wait in 1..50:
            if (GetAsyncKeyState(VK_RETURN).int32 and 0x8000.int32) != 0: break
            sleep(20)
        echo ""

        SetForegroundWindow(hwnd)
        # 1. 各種情報を送信
        sendToHamlog(hwnd, 6, $ savedFreq)
        sendToHamlog(hwnd, 7, "FREEDV")
        if savedGL != "":
          sendToHamlog(hwnd, 9, savedGL)

# --- SNR出力の仕分けと送信 ---
        if confEnableSNR == 1:
          let remarksMsg = "His SNR=" & savedSNR & "dB"

          if confSNRTarget == 0:
            # --- パターン0: RS欄(His)へ直接送る ---
            sendToHamlog(hwnd, 4, toSjis(savedSNR)) 
          else:
            # --- パターン1: Remarks欄へ送る ---
            let cmd = if confRemarksTarget == 1: 13 else: 14 
            sendToHamlog(hwnd, cmd, toSjis(remarksMsg))

          echo ">>> Hamlog Transfer Done: ", remarksMsg

        # 2. コールサイン送信 ＋ 確定(Enter)
        sendToHamlog(hwnd, 1 or 0x10000 or 0x20000, toSjis(savedCall))
            
        try:
          # 4. 終了処理（鳩時計の「ポッポー！」）
          discard Beep(523, 500) # ポッ
          sleep(5)
          discard Beep(400, 800) # ポー

          echo "  -> 完了しました"
          echo "[完了] 転送終了。"
          lastCall = savedCall

          # --- 1.0秒待機してからセーブ判断 ---
          sleep(1000) 
          
          case confAutoSave:
            of 1:
              # 【確認画面まで】 Alt + S を送信
              keybd_event(0x12, 0, 0, 0)      # Alt 押し
              keybd_event(ord('S'), 0, 0, 0)  # S 押し
              keybd_event(ord('S'), 0, 2, 0)  # S 離す
              keybd_event(0x12, 0, 2, 0)      # Alt 離す
              discard Beep(800, 100)
              echo "  -> ハムログの登録確認画面(Alt+S)を表示しました"
              # --- ★ここに追加：確認画面を一番前に引きずり出す ---
              sleep(200) # ダイアログが出るのをコンマ数秒待つ
              let hDlg = FindWindow("#32770", "確認") # ハムログの確認ダイアログ
              if hDlg != 0:
                SetForegroundWindow(hDlg)
                SetWindowPos(hDlg, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE)
                discard Beep(800, 100)
                  
            of 2:
              # 【自動登録まで】 Alt + S を送信
              keybd_event(0x12, 0, 0, 0)      # Alt 押し
              keybd_event(ord('S'), 0, 0, 0)  # S 押し
              keybd_event(ord('S'), 0, 2, 0)  # S 離す
              keybd_event(0x12, 0, 2, 0)      # Alt 離す
              # --- 確認ダイアログを Enter で突破する ---
              sleep(500)                      # ダイアログが出るのを少し待つ
              keybd_event(0x0D, 0, 0, 0)      # Enter 押し
              keybd_event(0x0D, 0, 2, 0)      # Enter 離す
              discard Beep(1000, 100) 
              echo "  -> ハムログへ保存命令(Alt+S)を送信しました"
                  
            else:
              echo "  -> 手動登録モード（転送のみ完了）"

          # --- 5. 変数の完全リセット ---
          isCaptured    = false
          savedCall     = ""
          savedFreq     = ""
          savedGL       = ""
          savedSNR      = ""  
          capturedTime  = ""

          # --- 6. 設定の再読み込み ---
          sleep(1000) 
          loadConfig()
        except: 
          discard

    # C. CPU負荷を抑えるための非同期待機
    await sleepAsync(10)

# ==========================================================
# 起動ブロック
# ==========================================================
main()               # 設定読み込みとハムログ起動
waitFor mainLoop()   # ★Listenモードとして、ここでずっとパケットを待ち続けます
