//! Frame-stepping engine for movement timing, gravity, and lock delay.

use crate::engine::board::Board;

use super::config::EngineConfig;
use super::input::{Action, InputKind, InputSource};

const BOARD_WIDTH: i32 = 10;
const LOCK_TIME_FRAMES: f64 = 30.0;
const DROP_FRACTION_THRESHOLD: f64 = 0.04;
const SPAWN_SOFT_DROP_FRACTION_THRESHOLD: f64 = 1.0 / 30.0;

#[derive(Clone, Copy)]
struct ShiftState {
    held: bool,
    arr: f64,
    das: f64,
    direction: i32,
}

pub struct Engine {
    pub board: Board,
    config: EngineConfig,
    frame: i32,
    gravity_accumulator: f64,
    gravity_y_frac: f64,
    subframe: f64,
    input_soft_drop: bool,
    input_hold: bool,
    input_rotate_ccw: bool,
    input_rotate_cw: bool,
    input_rotate_180: bool,
    left_shift: ShiftState,
    right_shift: ShiftState,
    last_shift: i32,
    lock_active: bool,
    lock_frames: f64,
    topped_out: bool,
}

impl Engine {
    pub fn new(board: Board, config: EngineConfig) -> Engine {
        let arr = config.handling.arr;
        Engine {
            board,
            config,
            frame: -1,
            gravity_accumulator: 0.0,
            gravity_y_frac: 0.0,
            subframe: 0.0,
            input_soft_drop: false,
            input_hold: false,
            input_rotate_ccw: false,
            input_rotate_cw: false,
            input_rotate_180: false,
            left_shift: ShiftState {
                held: false,
                arr,
                das: 0.0,
                direction: -1,
            },
            right_shift: ShiftState {
                held: false,
                arr,
                das: 0.0,
                direction: 1,
            },
            last_shift: -1,
            lock_active: false,
            lock_frames: 0.0,
            topped_out: false,
        }
    }

    /// Returns whether a newly spawned piece overlapped existing blocks.
    pub fn topped_out(&self) -> bool {
        self.topped_out
    }

    pub fn step(&mut self, input: &mut dyn InputSource) -> Option<i32> {
        let next_frame = self.frame + 1;
        if !input.has_frame(next_frame) {
            return None;
        }
        self.frame = next_frame;
        self.subframe = 0.0;
        let mut inputs = input.frame_inputs(self.frame);
        inputs.sort_by(|a, b| a.subframe.total_cmp(&b.subframe));
        for fi in &inputs {
            self.process_subframe(fi.subframe);
            match fi.kind {
                InputKind::Down => self.action_down(fi.action, fi.hoisted),
                InputKind::Up => self.action_up(fi.action),
            }
        }
        self.process_subframe(1.0);
        Some(self.frame)
    }

    fn reset_lock_delay(&mut self) {
        self.lock_frames = 0.0;
    }

    fn after_spawn(&mut self) {
        let spawn_soft_drop_adjust = if self.gravity_y_frac >= SPAWN_SOFT_DROP_FRACTION_THRESHOLD {
            1
        } else {
            0
        };
        self.lock_active = false;
        self.reset_lock_delay();
        self.gravity_accumulator = 0.0;
        self.apply_spawn_rotation_and_hold();
        if self.board.current.is_some() && self.is_20g() {
            if self.input_soft_drop {
                self.board.sonic_drop(spawn_soft_drop_adjust);
            } else {
                self.board.gravity_drop(None);
            }
        }
        self.gravity_y_frac = 0.0;
    }

    fn apply_spawn_rotation_and_hold(&mut self) {
        if self.board.current.is_none() {
            return;
        }
        if self.config.handling.irs == "hold" {
            let mut irs: i8 = 0;
            if self.input_rotate_ccw {
                irs -= 1;
            }
            if self.input_rotate_cw {
                irs += 1;
            }
            if self.input_rotate_180 {
                irs += 2;
            }
            if irs != 0 {
                self.board.rotate(irs);
            }
        }
        if self.config.handling.ihs == "hold" && self.input_hold {
            if self.board.do_hold() {
                self.after_spawn();
            }
        }
    }

    fn action_down(&mut self, action: Action, hoisted: bool) {
        match action {
            Action::MoveLeft => {
                self.activate_shift(-1, hoisted);
                self.internal_shift();
            }
            Action::MoveRight => {
                self.activate_shift(1, hoisted);
                self.internal_shift();
            }
            Action::SoftDrop => self.input_soft_drop = true,
            Action::RotateCCW => {
                self.input_rotate_ccw = true;
                self.rotate(-1);
            }
            Action::RotateCW => {
                self.input_rotate_cw = true;
                self.rotate(1);
            }
            Action::Rotate180 => {
                self.input_rotate_180 = true;
                if self.config.allow_180 {
                    self.rotate(2);
                }
            }
            Action::HardDrop => {
                if self.board.current.is_some() {
                    let adj = self.spawn_offset_adjust();
                    if self.board.hard_drop(true, adj) == -1 {
                        self.topped_out = true;
                    }
                    self.after_spawn();
                }
            }
            Action::Hold => {
                self.input_hold = true;
                if self.board.current.is_some() && self.board.do_hold() {
                    self.after_spawn();
                }
            }
        }
    }

    fn action_up(&mut self, action: Action) {
        let arr = self.config.handling.arr;
        let cancel = self.config.handling.cancel;
        match action {
            Action::MoveLeft => {
                self.left_shift.held = false;
                self.left_shift.das = 0.0;
                if self.right_shift.held {
                    self.last_shift = self.right_shift.direction;
                }
                if cancel {
                    self.right_shift.arr = arr;
                    self.right_shift.das = 0.0;
                }
            }
            Action::MoveRight => {
                self.right_shift.held = false;
                self.right_shift.das = 0.0;
                if self.left_shift.held {
                    self.last_shift = self.left_shift.direction;
                }
                if cancel {
                    self.left_shift.arr = arr;
                    self.left_shift.das = 0.0;
                }
            }
            Action::SoftDrop => self.input_soft_drop = false,
            Action::RotateCCW => self.input_rotate_ccw = false,
            Action::RotateCW => self.input_rotate_cw = false,
            Action::Rotate180 => self.input_rotate_180 = false,
            Action::Hold => self.input_hold = false,
            Action::HardDrop => {}
        }
    }

    fn activate_shift(&mut self, direction: i32, hoisted: bool) {
        let das = self.config.handling.das;
        let dcd = self.config.handling.dcd as f64;
        let arr = self.config.handling.arr;
        let new_das = if hoisted { (das - dcd).max(0.0) } else { 0.0 };
        let shift = if direction < 0 {
            &mut self.left_shift
        } else {
            &mut self.right_shift
        };
        shift.held = true;
        shift.das = new_das;
        shift.arr = arr;
        self.last_shift = direction;
    }

    fn process_subframe(&mut self, target: f64) {
        let target = target.clamp(0.0, 1.0);
        if target <= self.subframe {
            return;
        }
        let delta = target - self.subframe;
        self.process_all_shift(delta);
        self.process_fall(delta);
        self.subframe = target;
    }

    fn process_all_shift(&mut self, delta: f64) {
        self.left_shift = self.process_shift(self.left_shift, delta);
        self.right_shift = self.process_shift(self.right_shift, delta);
    }

    fn process_shift(&mut self, mut shift: ShiftState, delta: f64) -> ShiftState {
        if !shift.held || self.last_shift != shift.direction {
            return shift;
        }
        let das = self.config.handling.das;
        let arr = self.config.handling.arr;
        let charge_delta = (delta - (das - shift.das).max(0.0)).max(0.0);
        shift.das = (shift.das + delta).min(das);
        if shift.das < das {
            return shift;
        }
        if self.board.current.is_none() {
            return shift;
        }
        shift.arr += charge_delta;
        if shift.arr < arr {
            return shift;
        }
        let repeats: i32 = if arr == 0.0 {
            BOARD_WIDTH
        } else {
            (shift.arr / arr).floor() as i32
        };
        if arr != 0.0 {
            shift.arr -= arr * repeats as f64;
        }
        for _ in 0..repeats {
            self.internal_shift();
        }
        shift
    }

    fn spawn_offset_adjust(&self) -> i32 {
        if self.gravity_y_frac >= DROP_FRACTION_THRESHOLD {
            return 1;
        }
        if self.input_soft_drop && self.gravity_accumulator >= DROP_FRACTION_THRESHOLD {
            return 1;
        }
        0
    }

    fn internal_shift(&mut self) -> bool {
        if self.board.current.is_none() {
            return false;
        }
        let moved = if self.last_shift < 0 {
            self.board.move_left()
        } else {
            self.board.move_right()
        };
        if moved {
            self.reset_lock_delay();
            if self.board.current.is_some() && self.is_20g() {
                if self.input_soft_drop {
                    let adj = self.spawn_offset_adjust();
                    self.board.sonic_drop(adj);
                } else {
                    self.board.gravity_drop(None);
                }
            }
            if self.lock_active && self.board.current.is_some() && !self.board.grounded() {
                self.lock_active = false;
            }
        }
        moved
    }

    fn rotate(&mut self, delta: i8) {
        if self.board.current.is_none() {
            return;
        }
        if self.board.rotate(delta) {
            self.reset_lock_delay();
            if self.lock_active && self.board.current.is_some() && !self.board.grounded() {
                self.lock_active = false;
            }
            if self.board.current.is_some() && self.is_20g() {
                if self.input_soft_drop {
                    let adj = self.spawn_offset_adjust();
                    self.board.sonic_drop(adj);
                } else {
                    self.board.gravity_drop(None);
                }
            }
            self.internal_dcd();
        }
    }

    fn internal_dcd(&mut self) {
        if self.config.handling.dcd <= 0 {
            return;
        }
        let das = self.config.handling.das;
        let dcd = self.config.handling.dcd as f64;
        let arr = self.config.handling.arr;
        let floor = (das - dcd).max(0.0);
        self.left_shift.das = self.left_shift.das.min(floor);
        self.left_shift.arr = arr;
        self.right_shift.das = self.right_shift.das.min(floor);
        self.right_shift.arr = arr;
    }

    fn leveled_level(&self) -> u32 {
        let thresholds = match &self.config.gravity {
            None => return 1,
            Some(g) => &g.thresholds,
        };
        let mut level = 1u32;
        for (index, &threshold) in thresholds.iter().enumerate().skip(1) {
            let level_number = (index + 1) as u32;
            if self.board.lines_cleared < threshold {
                return level;
            }
            level = level_number;
        }
        level
    }

    fn gravity_g(&self) -> f64 {
        let g = match &self.config.gravity {
            None => return 0.0,
            Some(g) => g,
        };
        let level = self.leveled_level();
        let denom = (g.base - (level as f64 - 1.0) * g.speed)
            .max(1e-9)
            .powi(level as i32 - 1);
        (1.0 / 60.0) / denom
    }

    fn is_20g(&self) -> bool {
        if self.board.current.is_none() {
            return false;
        }
        if self.input_soft_drop {
            return self.config.handling.sdf == 41;
        }
        self.gravity_g() >= 20.0 && self.config.gravity_may20g
    }

    fn process_fall(&mut self, delta: f64) {
        if self.board.current.is_none() {
            return;
        }

        let (cells, use_soft_drop) = if self.input_soft_drop {
            let sdf = self.config.handling.sdf;
            let c = if sdf == 41 {
                400.0 * delta
            } else {
                (self.gravity_g() * sdf as f64 * delta).max(0.05 * sdf as f64 * delta)
            };
            (c, true)
        } else {
            (self.gravity_g() * delta, false)
        };

        let use_instant_sd = use_soft_drop && self.config.handling.sdf == 41;

        if cells <= 0.0 {
            self.process_lock_delay(delta);
            return;
        }

        let old_row = if use_instant_sd {
            None
        } else {
            self.board.current.map(|c| c.row)
        };

        self.gravity_accumulator += cells;
        let steps = self.gravity_accumulator as i64;
        self.gravity_accumulator -= steps as f64;
        let mut broke = false;
        for _ in 0..steps {
            let dropped = if use_soft_drop {
                self.board.soft_drop()
            } else {
                self.board.gravity_drop(Some(1)) as u32
            };
            if dropped == 0 {
                self.process_lock_delay(delta);
                broke = true;
                break;
            }
            if self.board.current.is_none() {
                broke = true;
                break;
            }
        }
        if !broke {
            self.process_lock_delay(delta);
        }

        if !use_instant_sd {
            if let (Some(cur), Some(old)) = (self.board.current, old_row) {
                if cur.row != old || !self.board.grounded() {
                    self.gravity_y_frac = self.gravity_accumulator;
                }
            }
        }
    }

    fn process_lock_delay(&mut self, delta: f64) {
        if self.board.current.is_none() {
            return;
        }
        if !self.board.grounded() {
            self.lock_active = false;
            self.reset_lock_delay();
            return;
        }
        self.lock_active = true;
        self.lock_frames += delta;
        if self.lock_frames > LOCK_TIME_FRAMES {
            if self.board.lock_current(true) == -1 {
                self.topped_out = true;
            }
            self.after_spawn();
        }
    }
}
