class Game
  attr_gtk

  def tick
    outputs.labels << { x: 30, y: 30.from_top,
                        text: "left/right arrow keys to spin, up arrow to jump, ctrl+r to reset, click two points to place terrain" }
    defaults
    calc
    render
  end

  def defaults
    state.terrain ||= []

    state.player ||= { x: 100,
                       y: 640,
                       dx: 0,
                       dy: 0,
                       radius: 12,
                       drag: 0.05477,
                       gravity: 0.03,
                       entropy: 0.9,
                       angle: 0,
                       facing: 1,
                       angle_velocity: 0,
                       elasticity: 0.5 }

    state.grid_points ||= (1280.idiv(40) + 1).flat_map do |x|
      (720.idiv(40) + 1).map do |y|
        { x: x * 40,
          y: y * 40,
          w: 40,
          h: 40,
          anchor_x: 0.5,
          anchor_y: 0.5 }
      end
    end
  end

  def calc
    player.y = 720  if player.y < 0
    player.x = 1280 if player.x < 0
    player.x = 0    if player.x > 1280
    player.angle_velocity = player.angle_velocity.clamp(-30, 30)
    calc_edit_mode
    calc_play_mode
  end

  def calc_edit_mode
    state.current_grid_point = geometry.find_intersect_rect(inputs.mouse, state.grid_points)
    calc_edit_mode_click
  end

  def calc_edit_mode_click
    return if !state.current_grid_point
    return if !inputs.mouse.click

    if !state.start_point
      state.start_point = state.current_grid_point
    else
      state.terrain << { x: state.start_point.x,
                         y: state.start_point.y,
                         x2: state.current_grid_point.x,
                         y2: state.current_grid_point.y }
      state.start_point = nil
    end
  end

  def calc_play_mode
    player.x += player.dx
    player.dy -= player.gravity
    player.y += player.dy
    player.angle += player.angle_velocity
    player.dy += player.dy * player.drag ** 2 * -1
    player.dx += player.dx * player.drag ** 2 * -1
    player.colliding = false
    player.colliding_with = nil

    if inputs.keyboard.key_down.up
      player.dy += 5 * player.angle.vector_y
      player.dx += 5 * player.angle.vector_x
    end
    player.angle_velocity += inputs.left_right * -1
    player.facing = if inputs.left_right == -1
                      -1
                    elsif inputs.left_right == 1
                      1
                    else
                      player.facing
                    end

    collisions = player_terrain_collisions
    collisions.each do |collision|
      collide! player, collision
    end

    if player.colliding_with
      roll! player, player.colliding_with
    end
  end

  def reflect_velocity! circle, line
    slope = geometry.line_slope line, replace_infinity: 1000
    slope_angle = geometry.line_angle line
    if slope_angle == 90 || slope_angle == 270
      circle.dx *= -circle.elasticity
    else
      circle.angle_velocity += slope * (circle.dx.abs + circle.dy.abs)
      vec = line.x2 - line.x, line.y2 - line.y
      len = Math.sqrt(vec.x**2 + vec.y**2)

      vec.x /= len
      vec.y /= len

      n = geometry.vec2_normal vec

      v_dot_n = geometry.vec2_dot_product({ x: circle.dx, y: circle.dy }, n)

      circle.dx = circle.dx - n.x * (2 * v_dot_n)
      circle.dy = circle.dy - n.y * (2 * v_dot_n)
      circle.dx *= circle.elasticity
      circle.dy *= circle.elasticity
      half_terminal_velocity = 10
      impact_intensity = (circle.dy.abs) / half_terminal_velocity
      impact_intensity = 1 if impact_intensity > 1

      final = (0.9 - 0.8 * impact_intensity)
      next_angular_velocity = circle.angle_velocity * final
      circle.angle_velocity *= final

      if (circle.dx.abs + circle.dy.abs) <= 0.2
        circle.dx = 0
        circle.dy = 0
        circle.angle_velocity *= 0.99
      end

      if circle.angle_velocity.abs <= 0.1
        circle.angle_velocity = 0
      end
    end
  end

  def position_on_line! circle, line
    circle.colliding = true
    point = geometry.line_normal line, circle
    if point.y > circle.y
      circle.colliding_from_above = true
    else
      circle.colliding_from_above = false
    end

    circle.colliding_with = line

    if !geometry.point_on_line? point, line
      distance_from_start_of_line = geometry.distance_squared({ x: line.x, y: line.y }, point)
      distance_from_end_of_line = geometry.distance_squared({ x: line.x2, y: line.y2 }, point)
      if distance_from_start_of_line < distance_from_end_of_line
        point = { x: line.x, y: line.y }
      else
        point = { x: line.x2, y: line.y2 }
      end
    end
    angle = geometry.angle_to point, circle
    circle.y = point.y + angle.vector_y * (circle.radius)
    circle.x = point.x + angle.vector_x * (circle.radius)
  end

  def collide! circle, line
    return if !line
    position_on_line! circle, line
    reflect_velocity! circle, line
    next_player = { x: player.x + player.dx,
                    y: player.y + player.dy,
                    radius: player.radius }
  end

  def roll! circle, line
    slope_angle = geometry.line_angle line
    return if slope_angle == 90 || slope_angle == 270

    ax = -circle.gravity * slope_angle.vector_y
    ay = -circle.gravity * slope_angle.vector_x

    if ax.abs < 0.05 && ay.abs < 0.05
      ax = 0
      ay = 0
    end

    friction_coefficient = 0.0001
    friction_force = friction_coefficient * circle.gravity * slope_angle.vector_x

    circle.dy += ay
    circle.dx += ax

    if circle.colliding_from_above
      circle.dx += circle.angle_velocity * slope_angle.vector_x * 0.1
      circle.dy += circle.angle_velocity * slope_angle.vector_y * 0.1
    else
      circle.dx += circle.angle_velocity * slope_angle.vector_x * -0.1
      circle.dy += circle.angle_velocity * slope_angle.vector_y * -0.1
    end

    if circle.dx != 0
      circle.dx -= friction_force * (circle.dx / circle.dx.abs)
    end

    if circle.dy != 0
      circle.dy -= friction_force * (circle.dy / circle.dy.abs)
    end
  end

  def player_terrain_collisions
    terrain.find_all do |terrain|
             geometry.circle_intersect_line? player, terrain
           end
           .sort_by do |terrain|
             if player.facing == -1
               -terrain.x
             else
               terrain.x
             end
           end
  end

  def render
    render_current_grid_point
    render_preview_line
    render_grid_points
    render_terrain
    render_player
    render_player_terrain_collisions
  end

  def render_player_terrain_collisions
    collisions = player_terrain_collisions
    outputs.lines << collisions.map do |collision|
                       { x: collision.x,
                         y: collision.y,
                         x2: collision.x2,
                         y2: collision.y2,
                         r: 255,
                         g: 0,
                         b: 0 }
                     end
  end

  def render_current_grid_point
    return if state.game_mode == :play
    return if !state.current_grid_point
    outputs.sprites << state.current_grid_point
                            .merge(w: 8,
                                   h: 8,
                                   anchor_x: 0.5,
                                   anchor_y: 0.5,
                                   path: :solid,
                                   g: 0,
                                   r: 0,
                                   b: 0,
                                   a: 128)
  end

  def render_preview_line
    return if state.game_mode == :play
    return if !state.start_point
    return if !state.current_grid_point

    outputs.lines << { x: state.start_point.x,
                       y: state.start_point.y,
                       x2: state.current_grid_point.x,
                       y2: state.current_grid_point.y }
  end

  def render_grid_points
    outputs
      .sprites << state
                    .grid_points
                    .map do |point|
      point.merge w: 8,
                  h: 8,
                  anchor_x: 0.5,
                  anchor_y: 0.5,
                  path: :solid,
                  g: 255,
                  r: 255,
                  b: 255,
                  a: 128
    end
  end

  def render_terrain
    outputs.lines << state.terrain
  end

  def render_player
    outputs.sprites << player_prefab
  end

  def player_prefab
    flip_horizontally = player.facing == -1
    { x: player.x,
      y: player.y,
      w: player.radius * 2,
      h: player.radius * 2,
      angle: player.angle,
      anchor_x: 0.5,
      anchor_y: 0.5,
      path: "sprites/circle/blue.png" }
  end

  def player
    state.player
  end

  def terrain
    state.terrain
  end
end

def tick args
  $game ||= Game.new
  $game.args = args
  $game.tick
end

def reset args
  $terrain = args.state.terrain
  $game = nil
end