#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
mhp3qst — MHP3 改造クエスト CLI ツール
MHP3TaskEdit(.NET) を逆コンパイルして得た暗号・フォーマット仕様を再実装したもの。
GUIを使わずに、クエストファイル(.dat / .mis)の解析・変換・フィールド編集を行う。

依存: mhcrypt.py（同フォルダ）

サブコマンド:
  info     <file>                      クエストのヘッダ情報を表示（.dat/.mis 自動判定）
  dat2mis  <in.dat> <out.mis>          平文DAT → 暗号化MIS
  mis2dat  <in.mis> <out.dat>          暗号化MIS → 平文DAT
  rew      <file.rew>                  報酬ファイルを復号して表示
  set      <in> <out> [--field v ...]  固定フィールドを編集して保存（拡張子で形式自動: .dat/.mis）
  selftest                             テンプレDATで往復検証

set で編集できる主なフィールド（固定オフセット = 安全な上書き）:
  --number N        クエスト番号 (例 60006)
  --money  N        報酬金
  --points N        報酬ポイント(ギルドP)
  --continue N      継続回数 (0-9)
  --time   N        制限時間(分)
  --star   N        難度(星) 1-
  --boss-volume i=V   第iボス(1始まり)の体積(大きさ) 0-255
  --boss-hp     i=V   第iボスのHP等級
  --boss-atk    i=V   第iボスの攻撃力
  --boss-def    i=V   第iボスの防御力
  --boss-sta    i=V   第iボスの耐力(スタミナ)
"""
import sys, os, hashlib, struct, argparse
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mhcrypt as mc

# ---- ヘッダ固定オフセット（DataLoadToControl より） ----
OFF = {
    'contract':   (152, 3),   # 契約金
    'money':      (156, 3),   # 報酬金
    'cont_div':   (160, 3),   # 継続あたり金額 → 継続回数 = money / cont_div
    'union':      (124, 2),   # 報酬ポイント(ギルドP)
    'time':       (164, 3),   # /1800 = 分
    'number':     (176, 2),   # クエスト番号
    'star':       (178, 1),   # 難度(星) = 値（表示は -1）
    'tasktype':   (148, 1),   # 種別ビット
    'env':        (149, 1),
    'catcaiye':   (150, 1),   # 32=両方なし 64=両方 / 0,?=片方
    'map':        (180, 1),   # マップID
    'mon_ptr':    (44, 2),    # モンスター表ポインタ
    'intro_ptr':  (172, 2),   # 紹介テキストのポインタ表へのポインタ
}
# 第nボス(0始まり)の体積/HP/攻/防/耐は固定領域 [64 + n*8 ...]
def boss_stat_off(n, which):
    base = 64 + n*8
    return {'volume':base+0, 'volumeChange':base+2, 'hp':base+3,
            'atk':base+4, 'def':base+5, 'sta':base+6}[which]

MAPS = {1:'孤島',2:'砂原',3:'水没林',4:'凍土',5:'火山',6:'大砂漠',7:'溶岩峡谷',
        8:'闘技場',9:'小闘技場',10:'聖地(神域)',11:'極圏',12:'渓流',13:'天空山(霊峰)',
        14:'孤島(夜)',15:'砂原(夜)',16:'水没林(夜)',17:'凍土(夜)',18:'火山(夜)',19:'渓流(夜)'}
TASK_BITS = {1:'狩猟(小型)',2:'採集/納品',4:'狩猟(大型)',8:'大連続狩猟',16:'連続狩猟'}

# ---- 読み込み: dat(平文) / mis(暗号) を自動判定して平文 bytes を返す ----
DAT_MAGIC = bytes.fromhex('94000000763030')  # "....v00"

def load_plain(path):
    raw = open(path,'rb').read()
    ext = os.path.splitext(path)[1].lower()
    if ext == '.mis' or (raw[:7] != DAT_MAGIC and len(raw) % 4 == 0 and ext not in ('.dat','.txt')):
        dat = mis_to_dat_bytes(raw)
        return dat, 'mis'
    return raw, 'dat'

# ---- MIS <-> DAT ----
def dat_to_mis_bytes(dat):
    data = bytearray(dat)
    if len(data) % 4 != 0:
        data += b'\x00' * (4 - len(data) % 4)
    buf = bytearray(len(data) + 20)
    buf[:20] = mc.MIS_SALT
    buf[20:] = data
    h = hashlib.sha1(bytes(buf)).digest()      # SHA1(salt + data)
    buf[:20] = h
    for i in range(len(buf)):
        buf[i] = mc.aaaatobbbb[buf[i]]
    return mc.ByteXXXXtoYYYY(bytes(buf), mc.KEY_MIS)

def mis_to_dat_bytes(mis):
    a = bytearray(mc.ByteYYYYtoXXXX(mis, mc.KEY_MIS))
    for i in range(len(a) - len(a)%4):
        a[i] = mc.invAAAA[a[i]]
    stored = bytes(a[:20])
    chk = bytearray(a)
    chk[:20] = mc.MIS_SALT
    calc = hashlib.sha1(bytes(chk)).digest()
    if stored != calc:
        sys.stderr.write('[警告] MIShヘッダSHA1不一致（壊れている/別形式の可能性）\n')
    return bytes(a[20:])

# ---- ヘッダ解析 ----
def rd(b, off, ln):
    v = 0
    for i in range(ln):
        v |= b[off+i] << (8*i)
    return v

def wr(b, off, ln, val):
    for i in range(ln):
        b[off+i] = (val >> (8*i)) & 0xFF

def parse_header(b):
    h = {}
    h['number'] = rd(b,176,2)
    h['star']   = b[178]
    h['contract']= rd(b,152,3)
    h['money']  = rd(b,156,3)
    div = rd(b,160,3)
    h['cont_div'] = div
    h['continue'] = (h['money']//div) if div else 0
    h['union']  = rd(b,124,2)
    h['time_min'] = rd(b,164,3)//1800
    h['map'] = b[180]
    h['map_name'] = MAPS.get(b[180], '不明(%d)'%b[180])
    tt = b[148]
    if tt == 132:
        h['type'] = '訓練所'
    else:
        h['type'] = TASK_BITS.get(tt & 0x1F, '不明(0x%02X)'%tt)
    h['catcaiye'] = {32:'山菜爺/ネコ なし',64:'山菜爺+ネコ あり',0:'ネコのみ'}.get(b[150], '値%d'%b[150])
    # ボス基礎ステータス（固定領域）
    bosses = []
    for n in range(3):
        base = 64 + n*8
        vol = b[base]; hp=b[base+3]; atk=b[base+4]; dfn=b[base+5]; sta=b[base+6]
        if vol or hp or atk or dfn:
            bosses.append({'idx':n+1,'volume':vol,'hp':hp,'atk':atk,'def':dfn,'sta':sta})
    h['bosses'] = bosses
    return h

# ---- コマンド ----
def cmd_info(args):
    b, fmt = load_plain(args.file)
    print('ファイル: %s  (形式=%s, 平文長=%d)' % (args.file, fmt, len(b)))
    if b[:7] != DAT_MAGIC:
        print('  [注意] v005ヘッダが見つかりません。クエストDATでない可能性。')
    h = parse_header(b)
    print('  クエスト番号 : %d' % h['number'])
    print('  種別         : %s' % h['type'])
    print('  マップ       : %s (id=%d)' % (h['map_name'], h['map']))
    print('  難度(星)     : %d' % h['star'])
    print('  制限時間     : %d 分' % h['time_min'])
    print('  契約金       : %d z' % h['contract'])
    print('  報酬金       : %d z' % h['money'])
    print('  報酬ポイント : %d' % h['union'])
    print('  継続回数     : %d 回' % h['continue'])
    print('  山菜爺/ネコ  : %s' % h['catcaiye'])
    if h['bosses']:
        print('  大型モンスター基礎ステータス:')
        for bs in h['bosses']:
            print('    第%dボス: 体積=%d HP等級=%d 攻撃=0x%02X(%d) 防御=0x%02X(%d) 耐力=%d'
                  % (bs['idx'],bs['volume'],bs['hp'],bs['atk'],bs['atk'],bs['def'],bs['def'],bs['sta']))

def cmd_dat2mis(args):
    dat = open(args.infile,'rb').read()
    open(args.outfile,'wb').write(dat_to_mis_bytes(dat))
    print('OK: %s -> %s (%d bytes)' % (args.infile,args.outfile,os.path.getsize(args.outfile)))

def cmd_mis2dat(args):
    mis = open(args.infile,'rb').read()
    open(args.outfile,'wb').write(mis_to_dat_bytes(mis))
    print('OK: %s -> %s (%d bytes)' % (args.infile,args.outfile,os.path.getsize(args.outfile)))

def cmd_rew(args):
    d = open(args.file,'rb').read()
    a = mc.rew_decrypt(d); tot=0
    print('報酬(%s):' % args.file)
    for i in range(20):
        nv=a[i*4]|(a[i*4+1]<<8); num=a[i*4+2]; rate=a[i*4+3]
        if nv or num or rate:
            print('  slot%02d itemID=0x%04X 個数=%d 確率=%d%%' % (i,nv,num,rate)); tot+=rate
    print('  合計確率=%d%%' % tot)

def _parse_iv(s):
    i,v = s.split('=')
    return int(i)-1, int(v,0)

def cmd_set(args):
    b, fmt = load_plain(args.infile)
    b = bytearray(b)
    if args.number is not None: wr(b,176,2,args.number)
    if args.money  is not None: wr(b,156,3,args.money)
    if args.points is not None: wr(b,124,2,args.points)
    if args.star   is not None: b[178]=args.star
    if args.time   is not None: wr(b,164,3,args.time*1800)
    if getattr(args,'continue') is not None:
        c = getattr(args,'continue')
        money = rd(b,156,3)
        wr(b,160,3,(money//c) if c>0 else 0)
    for spec in (args.boss_volume or []):
        n,v=_parse_iv(spec); b[64+n*8]=v & 0xFF
    for spec in (args.boss_hp or []):
        n,v=_parse_iv(spec); b[64+n*8+3]=v & 0xFF
    for spec in (args.boss_atk or []):
        n,v=_parse_iv(spec); b[64+n*8+4]=v & 0xFF
    for spec in (args.boss_def or []):
        n,v=_parse_iv(spec); b[64+n*8+5]=v & 0xFF
    for spec in (args.boss_sta or []):
        n,v=_parse_iv(spec); b[64+n*8+6]=v & 0xFF
    out = args.outfile
    if out.lower().endswith('.mis'):
        open(out,'wb').write(dat_to_mis_bytes(bytes(b)))
    else:
        open(out,'wb').write(bytes(b))
    print('保存: %s' % out)
    cmd_info(argparse.Namespace(file=out))

def cmd_selftest(args):
    tdir = args.templatedir
    files = [f for f in os.listdir(tdir) if f.lower().endswith('.dat')]
    ok=0; ng=0
    for f in files:
        p=os.path.join(tdir,f)
        dat=open(p,'rb').read()
        mis=dat_to_mis_bytes(dat)
        back=mis_to_dat_bytes(mis)
        # MISはpaddingで長さがmult4化されるため、元長で比較
        if back[:len(dat)]==dat and back[:len(dat)].rstrip(b'\x00')==dat.rstrip(b'\x00'):
            ok+=1
        else:
            ng+=1; print('  NG round-trip:',f)
    print('往復検証(dat->mis->dat): OK=%d NG=%d / %d files' % (ok,ng,len(files)))

def main():
    ap = argparse.ArgumentParser(prog='mhp3qst', description='MHP3 改造クエスト CLI')
    sub = ap.add_subparsers(dest='cmd', required=True)
    p=sub.add_parser('info'); p.add_argument('file'); p.set_defaults(fn=cmd_info)
    p=sub.add_parser('dat2mis'); p.add_argument('infile'); p.add_argument('outfile'); p.set_defaults(fn=cmd_dat2mis)
    p=sub.add_parser('mis2dat'); p.add_argument('infile'); p.add_argument('outfile'); p.set_defaults(fn=cmd_mis2dat)
    p=sub.add_parser('rew'); p.add_argument('file'); p.set_defaults(fn=cmd_rew)
    p=sub.add_parser('selftest'); p.add_argument('templatedir'); p.set_defaults(fn=cmd_selftest)
    p=sub.add_parser('set')
    p.add_argument('infile'); p.add_argument('outfile')
    p.add_argument('--number',type=int); p.add_argument('--money',type=int)
    p.add_argument('--points',type=int); p.add_argument('--continue',type=int,dest='continue')
    p.add_argument('--time',type=int); p.add_argument('--star',type=int)
    p.add_argument('--boss-volume',action='append'); p.add_argument('--boss-hp',action='append')
    p.add_argument('--boss-atk',action='append'); p.add_argument('--boss-def',action='append')
    p.add_argument('--boss-sta',action='append')
    p.set_defaults(fn=cmd_set)
    args=ap.parse_args()
    args.fn(args)

if __name__=='__main__':
    main()
