class FlappyDragon
  attr_accessor :grid, :inputs, :state, :outputs

  def tick
    defaults
    render
    calc
    process_inputs
  end

  def defaults
    state.flap_power              = 11
    state.gravity                 = 0.9
    state.ceiling                 = 600
    state.ceiling_flap_power      = 6
    state.wall_countdown_length   = 100
    state.wall_gap_size           = 100
    state.wall_countdown        ||= 0
    state.hi_score              ||= 0
    state.score                 ||= 0
    state.walls                 ||= []
    state.x                     ||= 50
    state.y                     ||= 500
    state.dy                    ||= 0
    state.scene                 ||= :menu
    state.scene_at              ||= 0
    state.difficulty            ||= :normal
    state.new_difficulty        ||= :normal
    state.countdown             ||= 4.seconds
    state.flash_at              ||= 0
  end

  def render
    outputs.sounds << "sounds/flappy-song.ogg" if state.tick_count == 1
    render_score
    render_menu
    render_game
  end

  def render_score
    outputs.primitives << { x: 10, y: 710, text: "HI SCORE: #{state.hi_score}", **large_white_typeset }
    outputs.primitives << { x: 10, y: 680, text: "SCORE: #{state.score}", **large_white_typeset }
    outputs.primitives << { x: 10, y: 650, text: "DIFFICULTY: #{state.difficulty.upcase}", **large_white_typeset }
  end

  def render_menu
    return unless state.scene == :menu
    render_overlay

    outputs.labels << { x: 640, y: 700, text: "Flappy Dragon", size_enum: 50, alignment_enum: 1, **white }
    outputs.labels << { x: 640, y: 500, text: "Instructions: Press Spacebar to flap. Don't die.", size_enum: 4, alignment_enum: 1, **white }
    outputs.labels << { x: 430, y: 430, text: "[Tab]    Change difficulty", size_enum: 4, alignment_enum: 0, **white }
    outputs.labels << { x: 430, y: 400, text: "[Enter]  Start at New Difficulty ", size_enum: 4, alignment_enum: 0, **white }
    outputs.labels << { x: 430, y: 370, text: "[Escape] Cancel/Resume ", size_enum: 4, alignment_enum: 0, **white }
    outputs.labels << { x: 640, y: 300, text: "(mouse, touch, and game controllers work, too!) ", size_enum: 4, alignment_enum: 1, **white }
    outputs.labels << { x: 640, y: 200, text: "Difficulty: #{state.new_difficulty.capitalize}", size_enum: 4, alignment_enum: 1, **white }

    outputs.labels << { x: 10, y: 100, text: "Code:   @amirrajan",     **white }
    outputs.labels << { x: 10, y:  80, text: "Art:    @mobypixel",     **white }
    outputs.labels << { x: 10, y:  60, text: "Music:  @mobypixel",     **white }
    outputs.labels << { x: 10, y:  40, text: "Engine: DragonRuby GTK", **white }
  end

  def render_overlay
    overlay_rect = grid.rect.scale_rect(1.1, 0, 0)
    outputs.primitives << { x: overlay_rect.x,
                            y: overlay_rect.y,
                            w: overlay_rect.w,
                            h: overlay_rect.h,
                            r: 0, g: 0, b: 0, a: 230 }.solid!
  end

  def render_game
    render_game_over
    render_background
    render_walls
    render_dragon
    render_flash
  end

  def render_game_over
    return unless state.scene == :game
    outputs.labels << { x: 638, y: 358, text: score_text,     size_enum: 20, alignment_enum: 1 }
    outputs.labels << { x: 635, y: 360, text: score_text,     size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
    outputs.labels << { x: 638, y: 428, text: countdown_text, size_enum: 20, alignment_enum: 1 }
    outputs.labels << { x: 635, y: 430, text: countdown_text, size_enum: 20, alignment_enum: 1, r: 255, g: 255, b: 255 }
  end

  def render_background
    outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: 'sprites/background.png' }

    scroll_point_at   = state.tick_count
    scroll_point_at   = state.scene_at if state.scene == :menu
    scroll_point_at   = state.death_at if state.countdown > 0
    scroll_point_at ||= 0

    outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_back.png',   0.25)
    outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_middle.png', 0.50)
    outputs.sprites << scrolling_background(scroll_point_at, 'sprites/parallax_front.png',  1.00, -80)
  end

  def scrolling_background at, path, rate, y = 0
    [
      { x:    0 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path },
      { x: 1440 - at.*(rate) % 1440, y: y, w: 1440, h: 720, path: path }
    ]
  end

  def render_walls
    state.walls.each do |w|
      w.sprites = [
        { x: w.x, y: w.bottom_height - 720, w: 100, h: 720, path: 'sprites/wall.png',       angle: 180 },
        { x: w.x, y: w.top_y,               w: 100, h: 720, path: 'sprites/wallbottom.png', angle: 0 }
      ]
    end
    outputs.sprites << state.walls.map(&:sprites)
  end

  def render_dragon
    state.show_death = true if state.countdown == 3.seconds

    if state.show_death == false || !state.death_at
      animation_index = state.flapped_at.frame_index 6, 2, false if state.flapped_at
      sprite_name = "sprites/dragon_fly#{animation_index.or(0) + 1}.png"
      state.dragon_sprite = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
    else
      sprite_name = "sprites/dragon_die.png"
      state.dragon_sprite = { x: state.x, y: state.y, w: 100, h: 80, path: sprite_name, angle: state.dy * 1.2 }
      sprite_changed_elapsed    = state.death_at.elapsed_time - 1.seconds
      state.dragon_sprite.angle += (sprite_changed_elapsed ** 1.3) * state.death_fall_direction * -1
      state.dragon_sprite.x     += (sprite_changed_elapsed ** 1.2) * state.death_fall_direction
      state.dragon_sprite.y     += (sprite_changed_elapsed * 14 - sprite_changed_elapsed ** 1.6)
    end

    outputs.sprites << state.dragon_sprite
  end

  def render_flash
    return unless state.flash_at

    outputs.primitives << { **grid.rect.to_hash,
                            **white,
                            a: 255 * state.flash_at.ease(20, :flip) }.solid!

    state.flash_at = 0 if state.flash_at.elapsed_time > 20
  end

  def calc
    return unless state.scene == :game
    reset_game if state.countdown == 1
    state.countdown -= 1 and return if state.countdown > 0
    calc_walls
    calc_flap
    calc_game_over
  end

  def calc_walls
    state.walls.each { |w| w.x -= 8 }

    walls_count_before_removal = state.walls.length

    state.walls.reject! { |w| w.x < -100 }

    state.score += 1 if state.walls.count < walls_count_before_removal

    state.wall_countdown -= 1 and return if state.wall_countdown > 0

    state.walls << state.new_entity(:wall) do |w|
      w.x             = grid.right
      w.opening       = grid.top
                            .randomize(:ratio)
                            .greater(200)
                            .lesser(520)
      w.bottom_height = w.opening - state.wall_gap_size
      w.top_y         = w.opening + state.wall_gap_size
    end

    state.wall_countdown = state.wall_countdown_length
  end

  def calc_flap
    state.y += state.dy
    state.dy = state.dy.lesser state.flap_power
    state.dy -= state.gravity
    return if state.y < state.ceiling
    state.y  = state.ceiling
    state.dy = state.dy.lesser state.ceiling_flap_power
  end

  def calc_game_over
    return unless game_over?

    state.death_at = state.tick_count
    state.death_from = state.walls.first
    state.death_fall_direction = -1
    state.death_fall_direction =  1 if state.x > state.death_from.x
    outputs.sounds << "sounds/hit-sound.wav"
    begin_countdown
  end

  def process_inputs
    process_inputs_menu
    process_inputs_game
  end

  def process_inputs_menu
    return unless state.scene == :menu

    changediff = inputs.keyboard.key_down.tab || inputs.controller_one.key_down.select
    if inputs.mouse.click
      p = inputs.mouse.click.point
      if (p.y >= 165) && (p.y < 200) && (p.x >= 500) && (p.x < 800)
        changediff = true
      end
    end

    if changediff
      case state.new_difficulty
      when :easy
        state.new_difficulty = :normal
      when :normal
        state.new_difficulty = :hard
      when :hard
        state.new_difficulty = :flappy
      when :flappy
        state.new_difficulty = :easy
      end
    end

    if inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start || inputs.controller_one.key_down.a
      state.difficulty = state.new_difficulty
      change_to_scene :game
      reset_game false
      state.hi_score = 0
      begin_countdown
    end

    if inputs.keyboard.key_down.escape || (inputs.mouse.click && !changediff) || inputs.controller_one.key_down.b
      state.new_difficulty = state.difficulty
      change_to_scene :game
    end
  end

  def process_inputs_game
    return unless state.scene == :game

    clicked_menu = false
    if inputs.mouse.click
      p = inputs.mouse.click.point
      clicked_menu = (p.y >= 620) && (p.x < 275)
    end

    if clicked_menu || inputs.keyboard.key_down.escape || inputs.keyboard.key_down.enter || inputs.controller_one.key_down.start
      change_to_scene :menu
    elsif (inputs.mouse.down || inputs.mouse.click || inputs.keyboard.key_down.space || inputs.controller_one.key_down.a) && state.countdown == 0
      state.dy = 0
      state.dy += state.flap_power
      state.flapped_at = state.tick_count
      outputs.sounds << "sounds/fly-sound.wav"
    end
  end

  def white
    { r: 255, g: 255, b: 255 }
  end

  def large_white_typeset
    { size_enum: 5, alignment_enum: 0, r: 255, g: 255, b: 255 }
  end

  def at_beginning?
    state.walls.count == 0
  end

  def dragon_collision_box
    state.dragon_sprite
         .scale_rect(1.0 - collision_forgiveness, 0.5, 0.5)
         .rect_shift_right(10)
         .rect_shift_up(state.dy * 2)
  end

  def game_over?
    return true if state.y <= 0.-(500 * collision_forgiveness) && !at_beginning?

    state.walls
        .flat_map { |w| w.sprites }
        .any? do |s|
          s && s.intersect_rect?(dragon_collision_box)
        end
  end

  def collision_forgiveness
    case state.difficulty
    when :easy
      0.9
    when :normal
      0.7
    when :hard
      0.5
    when :flappy
      0.3
    else
      0.9
    end
  end

  def countdown_text
    state.countdown ||= -1
    return ""          if state.countdown == 0
    return "GO!"       if state.countdown.idiv(60) == 0
    return "GAME OVER" if state.death_at
    return "READY?"
  end

  def begin_countdown
    state.countdown = 4.seconds
  end

  def score_text
    return ""                        unless state.countdown > 1.seconds
    return ""                        unless state.death_at
    return "SCORE: 0 (LOL)"          if state.score == 0
    return "HI SCORE: #{state.score}" if state.score == state.hi_score
    return "SCORE: #{state.score}"
  end

  def reset_game set_flash = true
    state.flash_at = state.tick_count if set_flash
    state.walls = []
    state.y = 500
    state.dy = 0
    state.hi_score = state.hi_score.greater(state.score)
    state.score = 0
    state.wall_countdown = state.wall_countdown_length.fdiv(2)
    state.show_death = false
    state.death_at = nil
  end

  def change_to_scene scene
    state.scene = scene
    state.scene_at = state.tick_count
    inputs.keyboard.clear
    inputs.controller_one.clear
  end
end

$flappy_dragon = FlappyDragon.new

def tick args
  $flappy_dragon.grid = args.grid
  $flappy_dragon.inputs = args.inputs
  $flappy_dragon.state = args.state
  $flappy_dragon.outputs = args.outputs
  $flappy_dragon.tick
end