use crate::board::movegen::{LegalAll, MoveListGen};
use crate::board::{PackedSfen, PackedSfenError, Position, SfenError};
use crate::records::record::{GameRecord, MoveRecord, NodeId};
use crate::types::square::flip;
use crate::types::{Color, GameResult, Move, Move32, Piece, PieceType, Square};
use std::convert::TryFrom;

/// sbinpack の合法手オーダリングに使用するソートキー。
///
/// `(is_drop, is_capture, is_promotion, piece_priority, to_rank, to_file, from_rank, from_file, raw_move)`
pub type MoveOrderKey = (u8, u8, u8, u8, u8, u8, u8, u8, u16);

const FILE_PRIORITY: [u8; 9] = [7, 5, 3, 1, 0, 2, 4, 6, 8];

/// sbinpack v1.0.0 のバージョン番号。
pub const SBINPACK_VERSION: u8 = 1;
/// sbinpack チャンクヘッダーのマジックバイト (`b"SBIN"`)。
pub const SBINPACK_MAGIC: [u8; 4] = *b"SBIN";

/// ZigZagエンコード（i32 -> u32）。
#[must_use]
pub const fn zigzag_i32(value: i32) -> u32 {
    ((value << 1) ^ (value >> 31)) as u32
}

/// ZigZagデコード（u32 -> i32）。
#[must_use]
pub const fn unzigzag_u32(value: u32) -> i32 {
    ((value >> 1) as i32) ^ (-((value & 1) as i32))
}

/// ULEB128でu32をエンコードする。
pub fn encode_uleb128_u32(mut value: u32, out: &mut Vec<u8>) {
    while value >= 0x80 {
        out.push((value as u8) | 0x80);
        value >>= 7;
    }
    out.push(value as u8);
}

/// ULEB128からu32をデコードする。
pub fn decode_uleb128_u32(input: &[u8], offset: &mut usize) -> Option<u32> {
    let mut value = 0u32;
    let mut shift = 0u32;
    while *offset < input.len() && shift < 32 {
        let byte = input[*offset];
        *offset += 1;
        value |= u32::from(byte & 0x7f) << shift;
        if (byte & 0x80) == 0 {
            return Some(value);
        }
        shift += 7;
    }
    None
}

/// sbinpack v1.0.0 の評価値差分をエンコードする。
pub fn encode_score_delta(prev_eval: i32, current_eval: i32, out: &mut Vec<u8>) {
    let delta = current_eval - prev_eval;
    encode_uleb128_u32(zigzag_i32(delta), out);
}

/// sbinpack v1.0.0 の評価値差分をデコードする。
pub fn decode_score_delta(input: &[u8], offset: &mut usize) -> Option<i32> {
    decode_uleb128_u32(input, offset).map(unzigzag_u32)
}

/// sbinpack の読み書き時に発生するエラー。
#[derive(Debug)]
pub enum SbinpackError {
    /// マジックバイトが不正。
    InvalidMagic,
    /// データが途中で途切れている。
    Truncated,
    /// SFEN の解析に失敗。
    Sfen(SfenError),
    /// PackedSfen の解析に失敗。
    PackedSfen(PackedSfenError),
    /// 合法手リスト内のインデックスが範囲外。
    InvalidMoveIndex(u32),
    /// 指し手をオーダリングでエンコードできない。
    UnencodableMove(Move32),
    /// 指定手数目の評価値が存在しない。
    MissingEval(usize),
}

/// sbinpack チェーンの先頭局面データ（PackedSfen + 評価値 + 最善手 + 手数/結果）。
#[derive(Debug, Clone, Copy)]
pub struct SbinpackStem {
    pub packed_sfen: PackedSfen,
    pub score: i16,
    pub best_move: Move,
    pub ply_result: u16,
}

/// sbinpack チェーン内の 1 手分のデータ（指し手 + 評価値）。
#[derive(Debug, Clone, Copy)]
pub struct SbinpackMove {
    pub mv: Move32,
    pub eval: i32,
}

/// sbinpack の 1 チェーン（先頭局面 + 後続手順）。
///
/// 1 つの初期局面から始まる連続した手順を表す。
#[derive(Debug, Clone)]
pub struct SbinpackChain {
    pub stem: SbinpackStem,
    pub moves: Vec<SbinpackMove>,
}

/// sbinpack ファイル全体（複数チャンク・チェーンの集合）。
#[derive(Debug, Clone)]
pub struct SbinpackFile {
    pub version: u8,
    pub flags: u8,
    pub chains: Vec<SbinpackChain>,
}

fn chain_from_position(
    pos: &Position,
    moves: &[MoveRecord],
    stem_score: i16,
    result: GameResult,
) -> Result<SbinpackChain, SbinpackError> {
    let packed_sfen = pos.to_packed_sfen();
    let mut current = pos.clone();

    let mut sbin_moves = Vec::with_capacity(moves.len());
    for (index, mv_record) in moves.iter().enumerate() {
        let eval = mv_record.eval().ok_or(SbinpackError::MissingEval(index))?;
        let mv16 = mv_record.mv();
        let mv = current.move32_from_move(mv16);
        sbin_moves.push(SbinpackMove { mv, eval: eval.to_i32() });
        current.apply_move(mv16);
    }

    let best_move = moves.first().map(MoveRecord::mv).unwrap_or(Move::MOVE_NONE);
    let ply = pos.game_ply();
    let ply_result = pack_ply_result(result, ply);

    Ok(SbinpackChain {
        stem: SbinpackStem { packed_sfen, score: stem_score, best_move, ply_result },
        moves: sbin_moves,
    })
}

fn moves_to_node(record: &GameRecord, node_id: NodeId) -> Vec<MoveRecord> {
    let mut moves = Vec::new();
    let mut current = Some(node_id);
    while let Some(id) = current {
        let node = record.node(id);
        if let Some(mv) = node.mv() {
            moves.push(mv.clone());
        }
        current = node.parent();
    }
    moves.reverse();
    moves
}

fn position_at_node(record: &GameRecord, node_id: NodeId) -> Result<Position, SbinpackError> {
    let mut pos = Position::empty();
    pos.set_sfen(record.init_position_sfen()).map_err(SbinpackError::Sfen)?;
    for mv in moves_to_node(record, node_id) {
        pos.apply_move32(pos.move32_from_move(mv.mv()));
    }
    Ok(pos)
}

fn line_moves_from(record: &GameRecord, start: NodeId) -> Vec<MoveRecord> {
    let mut moves = Vec::new();
    let mut current = Some(start);
    while let Some(id) = current {
        let node = record.node(id);
        if let Some(mv) = node.mv() {
            moves.push(mv.clone());
        }
        current = record.children(id).first().copied();
    }
    moves
}

/// `GameRecord` から `SbinpackChain` を作成します。
pub fn chain_from_record(
    record: &GameRecord,
    stem_score: i16,
) -> Result<SbinpackChain, SbinpackError> {
    let mut pos = Position::empty();
    pos.set_sfen(record.init_position_sfen()).map_err(SbinpackError::Sfen)?;
    let moves: Vec<MoveRecord> = record.main_moves().cloned().collect();
    chain_from_position(&pos, &moves, stem_score, record.result())
}

/// `GameRecord` から `SbinpackChain` を複数作成します。
pub fn chains_from_record(
    record: &GameRecord,
    stem_score: i16,
    include_main: bool,
    include_variations: bool,
) -> Result<Vec<SbinpackChain>, SbinpackError> {
    let mut chains = Vec::new();

    if include_main {
        let mut pos = Position::empty();
        pos.set_sfen(record.init_position_sfen()).map_err(SbinpackError::Sfen)?;
        let moves: Vec<MoveRecord> = record.main_moves().cloned().collect();
        chains.push(chain_from_position(&pos, &moves, stem_score, record.result())?);
    }

    if include_variations {
        let mut stack = vec![record.root_id()];
        while let Some(node_id) = stack.pop() {
            let children = record.children(node_id);
            if children.len() > 1 {
                for &child in &children[1..] {
                    let parent_pos = position_at_node(record, node_id)?;
                    let moves = line_moves_from(record, child);
                    chains.push(chain_from_position(
                        &parent_pos,
                        &moves,
                        stem_score,
                        record.result(),
                    )?);
                }
            }
            for &child in children {
                stack.push(child);
            }
        }
    }

    Ok(chains)
}

/// sbinpack v1.0.0 の `ChunkHeader + Chain*` を構築します（単一チャンク）。
pub fn serialize_file(chains: &[SbinpackChain]) -> Result<Vec<u8>, SbinpackError> {
    let mut payload = Vec::new();
    for chain in chains {
        serialize_chain(chain, &mut payload)?;
    }

    let mut out = Vec::new();
    out.extend_from_slice(&SBINPACK_MAGIC);
    out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
    out.extend_from_slice(&payload);
    Ok(out)
}

/// sbinpack v1.0.0 の `ChunkHeader + Chain*` を読み込みます。
pub fn deserialize_file(input: &[u8]) -> Result<SbinpackFile, SbinpackError> {
    let mut offset = 0;
    let mut chains = Vec::new();
    while offset < input.len() {
        if offset + 8 > input.len() {
            return Err(SbinpackError::Truncated);
        }
        if input[offset..offset + 4] != SBINPACK_MAGIC {
            return Err(SbinpackError::InvalidMagic);
        }
        let chunk_size = u32::from_le_bytes([
            input[offset + 4],
            input[offset + 5],
            input[offset + 6],
            input[offset + 7],
        ]) as usize;
        offset += 8;
        if offset + chunk_size > input.len() {
            return Err(SbinpackError::Truncated);
        }
        let chunk_end = offset + chunk_size;
        while offset < chunk_end {
            let (chain, next) = deserialize_chain(input, offset, chunk_end)?;
            chains.push(chain);
            offset = next;
        }
    }

    Ok(SbinpackFile { version: SBINPACK_VERSION, flags: 0, chains })
}

/// result (6bit) + ply (10bit) をパックする。
#[must_use]
pub const fn pack_ply_result(result: GameResult, ply: u16) -> u16 {
    let result_bits = (result as u16) & 0x3f;
    let ply_bits = if ply > 1023 { 1023 } else { ply };
    (result_bits << 10) | ply_bits
}

/// result (6bit) + ply (10bit) をアンパックする。
#[must_use]
pub fn unpack_ply_result(value: u16) -> (Option<GameResult>, u16) {
    let result = game_result_from_u8(((value >> 10) & 0x3f) as u8);
    let ply = value & 0x03ff;
    (result, ply)
}

#[must_use]
pub fn game_result_from_u8(value: u8) -> Option<GameResult> {
    match value {
        0 => Some(GameResult::BlackWin),
        1 => Some(GameResult::WhiteWin),
        2 => Some(GameResult::DrawByRepetition),
        3 => Some(GameResult::Error),
        4 => Some(GameResult::BlackWinByDeclaration),
        5 => Some(GameResult::WhiteWinByDeclaration),
        6 => Some(GameResult::DrawByMaxPlies),
        7 => Some(GameResult::Invalid),
        8 => Some(GameResult::BlackWinByForfeit),
        9 => Some(GameResult::WhiteWinByForfeit),
        10 => Some(GameResult::DrawByImpasse),
        11 => Some(GameResult::Paused),
        12 => Some(GameResult::BlackWinByIllegalMove),
        13 => Some(GameResult::WhiteWinByIllegalMove),
        16 => Some(GameResult::BlackWinByTimeout),
        17 => Some(GameResult::WhiteWinByTimeout),
        _ => None,
    }
}

fn serialize_chain(chain: &SbinpackChain, out: &mut Vec<u8>) -> Result<(), SbinpackError> {
    out.extend_from_slice(&chain.stem.packed_sfen.data);
    out.extend_from_slice(&chain.stem.score.to_le_bytes());
    out.extend_from_slice(&chain.stem.best_move.raw().to_le_bytes());
    out.extend_from_slice(&chain.stem.ply_result.to_le_bytes());

    let count = u16::try_from(chain.moves.len()).unwrap_or(u16::MAX);
    out.extend_from_slice(&count.to_le_bytes());

    let mut pos = Position::empty();
    let _ = pos.set_packed_sfen(&chain.stem.packed_sfen, false, 0);
    let mut prev_eval = i32::from(chain.stem.score);
    for entry in chain.moves.iter().take(count as usize) {
        let index =
            encoded_move_index(&pos, entry.mv).ok_or(SbinpackError::UnencodableMove(entry.mv))?;
        encode_uleb128_u32(u32::from(index), out);
        encode_score_delta(prev_eval, entry.eval, out);
        prev_eval = entry.eval;
        pos.apply_move32(entry.mv);
    }
    Ok(())
}

fn deserialize_chain(
    input: &[u8],
    offset: usize,
    chunk_end: usize,
) -> Result<(SbinpackChain, usize), SbinpackError> {
    let mut cursor = offset;
    if cursor + 38 + 2 > chunk_end {
        return Err(SbinpackError::Truncated);
    }
    let mut packed = PackedSfen::default();
    packed.data.copy_from_slice(&input[cursor..cursor + 32]);
    cursor += 32;
    let score = i16::from_le_bytes([input[cursor], input[cursor + 1]]);
    cursor += 2;
    let best_move = Move::from_raw(u16::from_le_bytes([input[cursor], input[cursor + 1]]));
    cursor += 2;
    let ply_result = u16::from_le_bytes([input[cursor], input[cursor + 1]]);
    cursor += 2;

    let count = u16::from_le_bytes([input[cursor], input[cursor + 1]]) as usize;
    cursor += 2;

    let mut pos = Position::empty();
    pos.set_packed_sfen(&packed, false, 0).map_err(SbinpackError::PackedSfen)?;

    let mut moves = Vec::with_capacity(count);
    let mut prev_eval = i32::from(score);
    for _ in 0..count {
        let Some(index) = decode_uleb128_u32(input, &mut cursor) else {
            return Err(SbinpackError::Truncated);
        };
        let Some(mv) = move_from_index(
            &pos,
            u16::try_from(index).map_err(|_| SbinpackError::InvalidMoveIndex(index))?,
        ) else {
            return Err(SbinpackError::InvalidMoveIndex(index));
        };
        let Some(delta) = decode_score_delta(input, &mut cursor) else {
            return Err(SbinpackError::Truncated);
        };
        let eval = prev_eval + delta;
        moves.push(SbinpackMove { mv, eval });
        prev_eval = eval;
        pos.apply_move32(mv);
    }

    Ok((
        SbinpackChain {
            stem: SbinpackStem { packed_sfen: packed, score, best_move, ply_result },
            moves,
        },
        cursor,
    ))
}

/// sbinpack v1.0.0 の合法手オーダリングで並べた手一覧を返します。
#[must_use]
pub fn ordered_legal_moves(pos: &Position) -> Vec<Move32> {
    let legal = MoveListGen::<LegalAll>::new(pos);
    let mut moves: Vec<Move32> = legal
        .iter()
        .copied()
        .filter_map(|mv| {
            let full = pos.move32_from_move(mv);
            full.is_normal().then_some(full)
        })
        .collect();
    moves.sort_by_key(|candidate| move_order_key(pos, *candidate));
    moves
}

/// sbinpack v1.0.0 の合法手オーダリングに従ったソートキーを返します。
#[must_use]
pub fn move_order_key(pos: &Position, mv: Move32) -> MoveOrderKey {
    let side = pos.turn();
    let is_drop = mv.is_drop();
    let is_capture = if is_drop {
        false
    } else {
        let to_piece = pos.piece_on(mv.to_sq());
        to_piece != Piece::NONE && to_piece.color() != side
    };
    let is_promotion = mv.is_promotion();

    let piece_type = if is_drop {
        mv.dropped_piece().unwrap_or(PieceType::NONE)
    } else {
        pos.moved_piece_before(mv).piece_type()
    };

    let to_sq = perspective_square(mv.to_sq(), side);
    let (to_file, to_rank) = square_to_priority(to_sq);
    let (from_file, from_rank) = if is_drop {
        (9, 9)
    } else {
        let from_sq = perspective_square(mv.from_sq(), side);
        square_to_priority(from_sq)
    };

    (
        u8::from(is_drop),
        u8::from(is_capture),
        u8::from(is_promotion),
        piece_priority(piece_type),
        to_rank,
        to_file,
        from_rank,
        from_file,
        mv.to_move().raw(),
    )
}

/// sbinpack v1.0.0 のオーダリング上のインデックスを返します。
#[must_use]
pub fn encoded_move_index(pos: &Position, mv: Move32) -> Option<u16> {
    let moves = ordered_legal_moves(pos);
    moves.iter().position(|candidate| *candidate == mv).and_then(|idx| u16::try_from(idx).ok())
}

/// sbinpack v1.0.0 のオーダリング上のインデックスから指し手を復元します。
#[must_use]
pub fn move_from_index(pos: &Position, index: u16) -> Option<Move32> {
    let moves = ordered_legal_moves(pos);
    moves.get(index as usize).copied()
}

fn perspective_square(sq: Square, side: Color) -> Square {
    if side == Color::BLACK {
        sq
    } else {
        flip(sq)
    }
}

fn square_to_priority(sq: Square) -> (u8, u8) {
    let file = sq.file().raw();
    let rank = sq.rank().raw();
    let file_priority = if (0..=8).contains(&file) { FILE_PRIORITY[file as usize] } else { 9 };
    let rank_priority = if (0..=8).contains(&rank) { rank as u8 } else { 9 };
    (file_priority, rank_priority)
}

fn piece_priority(piece_type: PieceType) -> u8 {
    match piece_type {
        PieceType::PAWN => 0,
        PieceType::LANCE => 1,
        PieceType::KNIGHT => 2,
        PieceType::SILVER => 3,
        PieceType::GOLD => 4,
        PieceType::BISHOP => 5,
        PieceType::ROOK => 6,
        PieceType::KING => 7,
        PieceType::PRO_PAWN => 8,
        PieceType::PRO_LANCE => 9,
        PieceType::PRO_KNIGHT => 10,
        PieceType::PRO_SILVER => 11,
        PieceType::HORSE => 12,
        PieceType::DRAGON => 13,
        _ => 14,
    }
}
