//! Score-attack point calculation from lock metadata.

use crate::engine::board::{Board, LockMetadata, SpinKind};

const ALL_CLEAR_BASE_POINTS: i64 = 3500;

const LEVEL_LINE_TOTALS: [i64; 16] = [
    0, 3, 8, 15, 24, 35, 48, 63, 80, 99, 120, 144, 170, 198, 228, 260,
];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScoreAttackState {
    pub score: i64,
    pub level: i64,
    pub total_lines: i64,
    pub combo: i64,
    pub b2b: bool,
}

impl Default for ScoreAttackState {
    fn default() -> Self {
        ScoreAttackState {
            score: 0,
            level: 1,
            total_lines: 0,
            combo: -1,
            b2b: false,
        }
    }
}

/// Returns the score multiplier level for a cumulative line count.
pub fn level_for_total_lines(total_lines: i64) -> i64 {
    let mut level = 1i64;
    for (index, &threshold) in LEVEL_LINE_TOTALS.iter().enumerate().skip(1) {
        let level_number = (index + 1) as i64;
        if total_lines < threshold {
            return level;
        }
        level = level_number;
    }
    level
}

fn base_action_points(lines_cleared: u32, spin_kind: SpinKind) -> i64 {
    // Tables are indexed by the number of lines cleared in one lock.
    let line_clear = [0, 100, 300, 500, 800];
    let full_spin = [400, 800, 1200, 1600, 2600];
    let mini_spin = [100, 200, 400, 800, 1600];
    let idx = lines_cleared as usize;
    if idx > 4 {
        panic!("unsupported line clear count {lines_cleared}");
    }
    match spin_kind {
        SpinKind::Full => full_spin[idx],
        SpinKind::Mini => mini_spin[idx],
        SpinKind::None => line_clear[idx],
    }
}

fn is_difficult_clear(lines_cleared: u32, spin_kind: SpinKind) -> bool {
    if lines_cleared == 0 {
        return false;
    }
    lines_cleared == 4 || matches!(spin_kind, SpinKind::Full | SpinKind::Mini)
}

struct LockScore {
    _total: i64,
    drop_points: i64,
}

fn score_lock(state: &mut ScoreAttackState, m: &LockMetadata) -> LockScore {
    let lines = m.lines_cleared;
    let next_lines = state.total_lines + lines as i64;
    let level = level_for_total_lines(next_lines);
    let base = base_action_points(lines, m.spin_kind) * level;

    let difficult = is_difficult_clear(lines, m.spin_kind);
    let b2b_bonus = if difficult && state.b2b { base / 2 } else { 0 };

    let next_combo = if lines > 0 { state.combo + 1 } else { -1 };
    let combo_bonus = next_combo.max(0) * 50 * level;

    let all_clear_bonus = if m.all_clear {
        ALL_CLEAR_BASE_POINTS * level
    } else {
        0
    };
    let hard_drop_points = m.hard_drop_cells as i64 * 2;
    let soft_drop_points = m.soft_drop_cells as i64;

    let total =
        base + b2b_bonus + combo_bonus + all_clear_bonus + hard_drop_points + soft_drop_points;

    let next_b2b = if difficult {
        true
    } else if lines > 0 {
        false
    } else {
        state.b2b
    };

    state.score += total;
    state.level = level;
    state.total_lines = next_lines;
    state.combo = next_combo;
    state.b2b = next_b2b;

    LockScore {
        _total: total,
        drop_points: hard_drop_points + soft_drop_points,
    }
}

pub fn score_board_locks(board: &Board) -> ScoreAttackState {
    let mut state = ScoreAttackState::default();
    let mut metadata_drop_points = 0i64;
    for m in &board.lock_metadata {
        let lock = score_lock(&mut state, m);
        metadata_drop_points += lock.drop_points;
    }
    state.score = state.score - metadata_drop_points + board.drop_score_points;
    state
}
