use notan::draw::*;
use notan::prelude::*;

const WIDTH: u32 = 800;
const HEIGHT: u32 = 580;
const WALL_SIZE: f32 = 20.0;
const PADDLE_WIDTH: f32 = 30.0;
const PADDLE_HEIGHT: f32 = PADDLE_WIDTH * 4.0;
const PADDLE_SPEED: f32 = 200.0;
const BALL_SIZE: f32 = WALL_SIZE * 0.8;
const BALL_SPEED: f32 = 240.0;
const BALL_SPEED_THRESHOLD: f32 = 60.0;
const FIRE_ANGLE_MAX: f32 = 120.0;
const PI: f32 = std::f32::consts::PI;

#[notan_main]
fn main() -> Result<(), String> {
    let win_config = WindowConfig::new().set_size(WIDTH, HEIGHT).set_vsync(true);

    notan::init_with(State::new)
        .add_config(win_config)
        .add_config(DrawConfig)
        .update(update)
        .draw(draw)
        .build()
}

fn update(app: &mut App, state: &mut State) {
    //Init game with space
    if !state.already_started && app.keyboard.was_pressed(KeyCode::Space) {
        state.already_started = true;
        state.ball.fire(&mut state.rng, true);
    }

    //Move paddle1 with W S
    if app.keyboard.is_down(KeyCode::KeyW) {
        state.paddle_1.y = (state.paddle_1.y - PADDLE_SPEED * app.timer.delta_f32()).max(WALL_SIZE);
    } else if app.keyboard.is_down(KeyCode::KeyS) {
        state.paddle_1.y = (state.paddle_1.y + PADDLE_SPEED * app.timer.delta_f32())
            .min(HEIGHT as f32 - WALL_SIZE - PADDLE_HEIGHT);
    }

    //Move paddle2 with arrows UP DOWN
    if app.keyboard.is_down(KeyCode::ArrowUp) {
        state.paddle_2.y = (state.paddle_2.y - PADDLE_SPEED * app.timer.delta_f32()).max(WALL_SIZE);
    } else if app.keyboard.is_down(KeyCode::ArrowDown) {
        state.paddle_2.y = (state.paddle_2.y + PADDLE_SPEED * app.timer.delta_f32())
            .min(HEIGHT as f32 - WALL_SIZE - PADDLE_HEIGHT);
    }

    //Move ball
    state.ball.x += state.ball.speed_x * app.timer.delta_f32();
    state.ball.y += state.ball.speed_y * app.timer.delta_f32();

    //Manage collision against walls
    let ball_bounds = state.ball.bounds();
    if ball_bounds.min_y <= WALL_SIZE {
        state.ball.y = WALL_SIZE + 1.0;
        state.ball.speed_y *= -1.0
    } else if ball_bounds.max_y >= HEIGHT as f32 - WALL_SIZE {
        state.ball.y = HEIGHT as f32 - WALL_SIZE - 1.0 - BALL_SIZE;
        state.ball.speed_y *= -1.0;
    }

    //Manage collisions against paddles
    if check_intersection(&ball_bounds, state) {
        if state.ball.x < WIDTH as f32 * 0.5 {
            //Left paddle
            let paddle_bounds = state.paddle_1.bounds();
            state.ball.x = state.paddle_1.x + PADDLE_WIDTH + 1.0;
            let angle = ((ball_bounds.center_y - paddle_bounds.center_y) / PADDLE_HEIGHT + 0.5)
                * FIRE_ANGLE_MAX;
            state
                .ball
                .speed_from_angle(random_speed(&mut state.rng), angle, true);
        } else {
            //Right paddle
            let paddle_bounds = state.paddle_2.bounds();
            state.ball.x = state.paddle_2.x - 1.0 - BALL_SIZE;
            let angle = ((ball_bounds.center_y - paddle_bounds.center_y) / PADDLE_HEIGHT + 0.5)
                * FIRE_ANGLE_MAX;
            state
                .ball
                .speed_from_angle(random_speed(&mut state.rng), angle, false);
            state.ball.speed_y *= -1.0;
        }
    }

    //Goals
    let ball_bounds = state.ball.bounds();
    if ball_bounds.max_x <= 0.0 {
        state.scores.1 += 1;
        state.ball.fire(&mut state.rng, false);
    } else if ball_bounds.min_x >= WIDTH as f32 {
        state.scores.0 += 1;
        state.ball.fire(&mut state.rng, true);
    }
}

fn draw(gfx: &mut Graphics, state: &mut State) {
    let alpha = if state.already_started { 1.0 } else { 0.6 };
    let width = WIDTH as f32;
    let height = HEIGHT as f32;

    let mut draw = gfx.create_draw();
    draw.clear(Color::BLACK);
    draw.set_alpha(alpha);

    draw.rect((0.0, 0.0), (width, WALL_SIZE));
    draw.rect((0.0, height - WALL_SIZE), (width, WALL_SIZE));

    let points = HEIGHT / WALL_SIZE as u32;
    for i in (0..points).step_by(2) {
        draw.rect(
            (width * 0.5 - WALL_SIZE * 0.5, WALL_SIZE * i as f32),
            (WALL_SIZE, WALL_SIZE),
        );
    }

    draw.rect(state.paddle_1.position(), (PADDLE_WIDTH, PADDLE_HEIGHT));
    draw.rect(state.paddle_2.position(), (PADDLE_WIDTH, PADDLE_HEIGHT));
    draw.rect(state.ball.position(), (BALL_SIZE, BALL_SIZE));

    draw.text(&state.font, "Use W/S to move")
        .h_align_center()
        .v_align_middle()
        .position(width * 0.25, height - 100.0)
        .size(20.0);

    draw.text(&state.font, "Use Up/Down to move")
        .h_align_center()
        .v_align_middle()
        .position(width - width * 0.25, height - 100.0)
        .size(20.0);

    if !state.already_started {
        draw.set_alpha(1.0);

        draw.text(&state.font, "Press SPACE to start")
            .h_align_center()
            .v_align_middle()
            .position(width * 0.5, height * 0.5)
            .size(80.0);
    }

    gfx.render(&draw);
}

#[derive(AppState)]

struct State {
    paddle_1: Paddle,
    paddle_2: Paddle,
    ball: Ball,
    scores: (i32, i32),
    rng: Random,
    already_started: bool,
    font: Font,
}

impl State {
    fn new(gfx: &mut Graphics) -> Self {
        let font = gfx
            .create_font(include_bytes!("assets/kenney_pixel-webfont.ttf"))
            .unwrap();
        Self {
            paddle_1: Paddle::new(10.0, HEIGHT as f32 * 0.5 - PADDLE_HEIGHT * 0.5),
            paddle_2: Paddle::new(
                WIDTH as f32 - 10.0 - PADDLE_WIDTH,
                HEIGHT as f32 * 0.5 - PADDLE_HEIGHT * 0.5,
            ),
            ball: Ball::new(
                WIDTH as f32 * 0.5 - BALL_SIZE * 0.5,
                HEIGHT as f32 * 0.5 - BALL_SIZE * 0.5,
            ),
            scores: (0, 0),
            rng: Random::default(),
            already_started: false,
            font,
        }
    }
}

struct Paddle {
    x: f32,
    y: f32,
}

impl Paddle {
    fn new(x: f32, y: f32) -> Self {
        Paddle { x, y }
    }
    fn position(&self) -> (f32, f32) {
        (self.x, self.y)
    }
}

impl BoundCalc for Paddle {
    fn bounds(&self) -> Bounds {
        Bounds {
            min_x: self.x,
            min_y: self.y,
            max_x: self.x + PADDLE_WIDTH,
            max_y: self.y + PADDLE_HEIGHT,
            center_y: self.y + PADDLE_HEIGHT * 0.5,
        }
    }
}

fn random_speed(rng: &mut Random) -> f32 {
    rng.random_range((BALL_SPEED - BALL_SPEED_THRESHOLD)..(BALL_SPEED + BALL_SPEED_THRESHOLD))
}

struct Ball {
    x: f32,
    y: f32,
    speed_x: f32,
    speed_y: f32,
}

impl Ball {
    fn new(x: f32, y: f32) -> Self {
        Self {
            x,
            y,
            speed_x: 0.0,
            speed_y: 0.0,
        }
    }

    fn position(&self) -> (f32, f32) {
        (self.x, self.y)
    }

    fn fire(&mut self, rng: &mut Random, left: bool) {
        self.x = if left { 50.0 } else { WIDTH as f32 - 50.0 };
        self.y = rng.random_range(WALL_SIZE..(HEIGHT as f32 - WALL_SIZE));

        let angle_to_fire: f32 = rng.random_range(0.0..FIRE_ANGLE_MAX);
        self.speed_from_angle(random_speed(rng), angle_to_fire, left);
    }

    fn speed_from_angle(&mut self, speed: f32, angle_to_fire: f32, left: bool) {
        let angle = if left {
            angle_to_fire - FIRE_ANGLE_MAX * 0.5
        } else {
            180.0 - FIRE_ANGLE_MAX * 0.5 + angle_to_fire
        };
        let rad = angle * PI / 180.0;
        self.speed_x = rad.cos() * speed;
        self.speed_y = rad.sin() * speed;
    }
}

impl BoundCalc for Ball {
    fn bounds(&self) -> Bounds {
        Bounds {
            min_x: self.x,
            min_y: self.y,
            max_x: self.x + BALL_SIZE,
            max_y: self.y + BALL_SIZE,
            center_y: self.y + BALL_SIZE * 0.5,
        }
    }
}

struct Bounds {
    min_x: f32,
    min_y: f32,
    max_x: f32,
    max_y: f32,
    center_y: f32,
}

trait BoundCalc {
    fn bounds(&self) -> Bounds;
}

#[inline]
fn check_intersection(ball_bounds: &Bounds, state: &mut State) -> bool {
    intersect_between(ball_bounds, &state.paddle_1.bounds())
        || intersect_between(ball_bounds, &state.paddle_2.bounds())
}

#[inline]
fn intersect_between(a: &Bounds, b: &Bounds) -> bool {
    a.min_x < b.max_x && a.max_x > b.min_x && a.min_y < b.max_y && a.max_y > b.min_y
}
