=begin

 APIs listing that haven't been encountered in previous sample apps:

 - lambda: A way to define a block and its parameters with special syntax.
   For example, the syntax of lambda looks like this:
   my_lambda = -> { puts "This is my lambda" }

 Reminders:
 - args.outputs.labels: An array. The values generate a label.
   The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]
   For more information about labels, go to mygame/documentation/02-labels.

 - ARRAY#inside_rect?: Returns whether or not the point is inside a rect.

 - product: Returns an array of all combinations of elements from all arrays.

 - find: Finds all elements of a collection that meet requirements.

 - abs: Returns the absolute value.

=end

# This sample app allows the player to move around in the dungeon, which becomes more or less visible
# depending on the player's location, and also has enemies.

class Game
  attr_accessor :args, :state, :inputs, :outputs, :grid

  # Calls all the methods needed for the game to run properly.
  def tick
    defaults
    render_canvas
    render_dungeon
    render_player
    render_enemies
    print_cell_coordinates
    calc_canvas
    input_move
    input_click_map
  end

  # Sets default values and initializes variables
  def defaults
    outputs.background_color = [0, 0, 0] # black background

    # Initializes empty canvas, dungeon, and enemies collections.
    state.canvas   ||= []
    state.dungeon  ||= []
    state.enemies  ||= []

    # If state.area doesn't have value, load_area_one and derive_dungeon_from_area methods are called
    if !state.area
      load_area_one
      derive_dungeon_from_area

      # Changing these values will change the position of player
      state.x = 7
      state.y = 5

      # Creates new enemies, sets their values, and adds them to the enemies collection.
      state.enemies << state.new_entity(:enemy) do |e| # declares each enemy as new entity
        e.x           = 13 # position
        e.y           = 5
        e.previous_hp = 3
        e.hp          = 3
        e.max_hp      = 3
        e.is_dead     = false # the enemy is alive
      end

      update_line_of_sight # updates line of sight by adding newly visible cells
    end
  end

  # Adds elements into the state.area collection
  # The dungeon is derived using the coordinates of this collection
  def load_area_one
    state.area ||= []
    state.area << [8, 6]
    state.area << [7, 6]
    state.area << [7, 7]
    state.area << [8, 9]
    state.area << [7, 8]
    state.area << [7, 9]
    state.area << [6, 4]
    state.area << [7, 3]
    state.area << [7, 4]
    state.area << [6, 5]
    state.area << [7, 5]
    state.area << [8, 5]
    state.area << [8, 4]
    state.area << [1, 1]
    state.area << [0, 1]
    state.area << [0, 2]
    state.area << [1, 2]
    state.area << [2, 2]
    state.area << [2, 1]
    state.area << [2, 3]
    state.area << [1, 3]
    state.area << [1, 4]
    state.area << [2, 4]
    state.area << [2, 5]
    state.area << [1, 5]
    state.area << [2, 6]
    state.area << [3, 6]
    state.area << [4, 6]
    state.area << [4, 7]
    state.area << [4, 8]
    state.area << [5, 8]
    state.area << [5, 9]
    state.area << [6, 9]
    state.area << [7, 10]
    state.area << [7, 11]
    state.area << [7, 12]
    state.area << [7, 12]
    state.area << [7, 13]
    state.area << [8, 13]
    state.area << [9, 13]
    state.area << [10, 13]
    state.area << [11, 13]
    state.area << [12, 13]
    state.area << [12, 12]
    state.area << [8, 12]
    state.area << [9, 12]
    state.area << [10, 12]
    state.area << [11, 12]
    state.area << [12, 11]
    state.area << [13, 11]
    state.area << [13, 10]
    state.area << [13, 9]
    state.area << [13, 8]
    state.area << [13, 7]
    state.area << [13, 6]
    state.area << [12, 6]
    state.area << [14, 6]
    state.area << [14, 5]
    state.area << [13, 5]
    state.area << [12, 5]
    state.area << [12, 4]
    state.area << [13, 4]
    state.area << [14, 4]
    state.area << [1, 6]
    state.area << [6, 6]
  end

  # Starts with an empty dungeon collection, and adds dungeon cells into it.
  def derive_dungeon_from_area
    state.dungeon = [] # starts as empty collection

    state.area.each do |a| # for each element of the area collection
      state.dungeon << state.new_entity(:dungeon_cell) do |d| # declares each dungeon cell as new entity
        d.x = a.x # dungeon cell position using coordinates from area
        d.y = a.y
        d.is_visible = false # cell is not visible
        d.alpha = 0 # not transparent at all
        d.border = [left_margin   + a.x * grid_size,
                    bottom_margin + a.y * grid_size,
                    grid_size,
                    grid_size,
                    *blue,
                    255] # sets border definition for dungeon cell
        d # returns dungeon cell
      end
    end
  end

  def left_margin
    40  # sets left margin
  end

  def bottom_margin
    60 # sets bottom margin
  end

  def grid_size
    40 # sets size of grid square
  end

  # Updates the line of sight by calling the thick_line_of_sight method and
  # adding dungeon cells to the newly_visible collection
  def update_line_of_sight
    variations = [-1, 0, 1]
    # creates collection of newly visible dungeon cells
    newly_visible = variations.product(variations).flat_map do |rise, run| # combo of all elements
      thick_line_of_sight state.x, state.y, rise, run, 15, # calls thick_line_of_sight method
                          lambda { |x, y| dungeon_cell_exists? x, y } # checks whether or not cell exists
    end.uniq# removes duplicates

    state.dungeon.each do |d| # perform action on each element of dungeons collection
      d.is_visible = newly_visible.find { |v| v.x == d.x && v.y == d.y } # finds match inside newly_visible collection
    end
  end

  #Returns a boolean value
  def dungeon_cell_exists? x, y
    # Finds cell coordinates inside dungeon collection to determine if dungeon cell exists
    state.dungeon.find { |d| d.x == x && d.y == y }
  end

  # Calls line_of_sight method to add elements to result collection
  def thick_line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result = []
    result += line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result += line_of_sight start_x - 1, start_y, rise, run, distance, cell_exists_lambda # one left
    result += line_of_sight start_x + 1, start_y, rise, run, distance, cell_exists_lambda # one right
    result
  end

  # Adds points to the result collection to create the player's line of sight
  def line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result = [] # starts as empty collection
    points = points_on_line start_x, start_y, rise, run, distance # calls points_on_line method
    points.each do |p| # for each point in collection
      if cell_exists_lambda.call(p.x, p.y) # if the cell exists
        result << p # add it to result collection
      else # if cell does not exist
        return result # return result collection as it is
      end
    end

    result # return result collection
  end

  # Finds the coordinates of the points on the line by performing calculations
  def points_on_line start_x, start_y, rise, run, distance
    distance.map do |i| # perform an action
      [start_x + run * i, start_y + rise * i] # definition of point
    end
  end

  def render_canvas
    return
    outputs.borders << state.canvas.map do |c| # on each element of canvas collection
      c.border # outputs border
    end
  end

  # Outputs the dungeon cells.
  def render_dungeon
    outputs.solids << [0, 0, grid.w, grid.h] # outputs black background for grid

    # Sets the alpha value (opacity) for each dungeon cell and calls the cell_border method.
    outputs.borders << state.dungeon.map do |d| # for each element in dungeon collection
      d.alpha += if d.is_visible # if cell is visible
                 255.fdiv(30) # increment opacity (transparency)
               else # if cell is not visible
                 255.fdiv(600) * -1 # decrease opacity
               end
      d.alpha = d.alpha.cap_min_max(0, 255)
      cell_border d.x, d.y, [*blue, d.alpha] # sets blue border using alpha value
    end.reject_nil
  end

  # Sets definition of a cell border using the parameters
  def cell_border x, y, color = nil
    [left_margin   + x * grid_size,
    bottom_margin + y * grid_size,
    grid_size,
    grid_size,
    *color]
  end

  # Sets the values for the player and outputs it as a label
  def render_player
    outputs.labels << [grid_x(state.x) + 20, # positions "@" text in center of grid square
                     grid_y(state.y) + 35,
                     "@", # player is represented by a white "@" character
                     1, 1, *white]
  end

  def grid_x x
    left_margin + x * grid_size # positions horizontally on grid
  end

  def grid_y y
    bottom_margin + y * grid_size # positions vertically on grid
  end

  # Outputs enemies onto the screen.
  def render_enemies
    state.enemies.map do |e| # for each enemy in the collection
      alpha = 255 # set opacity (full transparency)

      # Outputs an enemy using a label.
      outputs.labels << [
                   left_margin + 20 +  e.x * grid_size, # positions enemy's "r" text in center of grid square
                   bottom_margin + 35 + e.y * grid_size,
                   "r", # enemy's text
                   1, 1, *white, alpha]

      # Creates a red border around an enemy.
      outputs.borders << [grid_x(e.x), grid_y(e.y), grid_size, grid_size, *red]
    end
  end

  #White labels are output for the cell coordinates of each element in the dungeon collection.
  def print_cell_coordinates
    return unless state.debug
    state.dungeon.each do |d|
      outputs.labels << [grid_x(d.x) + 2,
                         grid_y(d.y) - 2,
                         "#{d.x},#{d.y}",
                         -2, 0, *white]
    end
  end

  # Adds new elements into the canvas collection and sets their values.
  def calc_canvas
    return if state.canvas.length > 0 # return if canvas collection has at least one element
    15.times do |x| # 15 times perform an action
      15.times do |y|
        state.canvas << state.new_entity(:canvas) do |c| # declare canvas element as new entity
          c.x = x # set position
          c.y = y
          c.border = [left_margin   + x * grid_size,
                      bottom_margin + y * grid_size,
                      grid_size,
                      grid_size,
                      *white, 30] # sets border definition
        end
      end
    end
  end

  # Updates x and y values of the player, and updates player's line of sight
  def input_move
    x, y, x_diff, y_diff = input_target_cell

    return unless dungeon_cell_exists? x, y # player can't move there if a dungeon cell doesn't exist in that location
    return if enemy_at x, y # player can't move there if there is an enemy in that location

    state.x += x_diff # increments x by x_diff (so player moves left or right)
    state.y += y_diff # same with y and y_diff ( so player moves up or down)
    update_line_of_sight # updates visible cells
  end

  def enemy_at x, y
    # Finds if coordinates exist in enemies collection and enemy is not dead
    state.enemies.find { |e| e.x == x && e.y == y && !e.is_dead }
  end

  #M oves the user based on their keyboard input and sets values for target cell
  def input_target_cell
    if inputs.keyboard.key_down.up # if "up" key is in "down" state
      [state.x, state.y + 1,  0,  1] # user moves up
    elsif inputs.keyboard.key_down.down # if "down" key is pressed
      [state.x, state.y - 1,  0, -1] # user moves down
    elsif inputs.keyboard.key_down.left # if "left" key is pressed
      [state.x - 1, state.y, -1,  0] # user moves left
    elsif inputs.keyboard.key_down.right # if "right" key is pressed
      [state.x + 1, state.y,  1,  0] # user moves right
    else
      nil  # otherwise, empty
    end
  end

  # Goes through the canvas collection to find if the mouse was clicked inside of the borders of an element.
  def input_click_map
    return unless inputs.mouse.click # return unless the mouse is clicked
    canvas_entry = state.canvas.find do |c| # find element from canvas collection that meets requirements
      inputs.mouse.click.inside_rect? c.border # find border that mouse was clicked inside of
    end
    puts canvas_entry # prints canvas_entry value
  end

  # Sets the definition of a label using the parameters.
  def label text, x, y, color = nil
    color ||= white # color is initialized to white
    [x, y, text, 1, 1, *color] # sets label definition
  end

  def green
    [60, 200, 100] # sets color saturation to shade of green
  end

  def blue
    [50, 50, 210] # sets color saturation to shade of blue
  end

  def white
    [255, 255, 255] # sets color saturation to white
  end

  def red
    [230, 80, 80] # sets color saturation to shade of red
  end

  def orange
    [255, 80, 60] # sets color saturation to shade of orange
  end

  def pink
    [255, 0, 200] # sets color saturation to shade of pink
  end

  def gray
    [75, 75, 75] # sets color saturation to shade of gray
  end

  # Recolors the border using the parameters.
  def recolor_border border, r, g, b
    border[4] = r
    border[5] = g
    border[6] = b
    border
  end

  # Returns a boolean value.
  def visible? cell
    # finds cell's coordinates inside visible_cells collections to determine if cell is visible
    state.visible_cells.find { |c| c.x == cell.x && c.y == cell.y}
  end

  # Exports dungeon by printing dungeon cell coordinates
  def export_dungeon
    state.dungeon.each do |d| # on each element of dungeon collection
      puts "state.dungeon << [#{d.x}, #{d.y}]" # prints cell coordinates
    end
  end

  def distance_to_cell cell
    distance_to state.x, cell.x, state.y, cell.y # calls distance_to method
  end

  def distance_to from_x, x, from_y, y
    (from_x - x).abs + (from_y - y).abs # finds distance between two cells using coordinates
  end
end

$game = Game.new

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