require_relative 'movement'

module MiniGL
  # This class represents an (optionally animated) image inside the game screen.
  class Sprite
    # The index of the current sprite in the spritesheet being drawn.
    attr_reader :img_index

    # The x-coordinate of the image in the screen.
    attr_accessor :x

    # The y-coordinate of the image in the screen.
    attr_accessor :y

    # Creates a new sprite.
    #
    # Parameters:
    # [x] The x-coordinate in the screen (or map) where the sprite will be
    #     drawn. This can be modified later via the +x+ attribute.
    # [y] The y-coordinate in the screen (or map) where the sprite will be
    #     drawn. This can be modified later via the +y+ attribute.
    # [img] The path to a PNG image or spritesheet, following the MiniGL
    #       convention: images must be inside a 'data/img' directory, relative
    #       to the code file, and you must only provide the file name, without
    #       extension, in this case. If the image is inside a subdirectory of
    #       'data/img', you must prefix the file name with each subdirectory
    #       name, followed by an underscore (so the file and directories names
    #       must not contain underscores). For example, if your image is
    #       'data/img/sprite/1.png', you must provide "sprite_1"
    #       or +:sprite_1+.
    # [sprite_cols] The number of columns in the spritesheet. Use +nil+ if the
    #               image is not a spritesheet.
    # [sprite_rows] The number of rows in the spritesheet. Use +nil+ if the
    #               image is not a spritesheet.
    def initialize(x, y, img, sprite_cols = nil, sprite_rows = nil, retro = nil)
      @x = x; @y = y
      retro = Res.retro_images if retro.nil?
      @img =
        if sprite_cols.nil?
          [Res.img(img, false, false, '.png', retro)]
        else
          Res.imgs img, sprite_cols, sprite_rows, false, '.png', retro
        end
      @anim_counter = 0
      @img_index = 0
      @index_index = 0
      @animate_once_control = 0
    end

    # Performs time checking to update the image index according to the
    # sequence of indices and the interval.
    #
    # Parameters:
    # [indices] The sequence of image indices used in the animation. The
    #           indices are determined from left to right, and from top to
    #           bottom, inside the spritesheet. All indices must be in the
    #           interval 0..(sprite_cols * sprite_rows).
    # [interval] The amount of frames between each change in the image index.
    #            A frame will usually represent 1/60 second (roughly 17
    #            milliseconds).
    def animate(indices, interval)
      @animate_once_control = 0 if @animate_once_control != 0

      @anim_counter += 1
      if @anim_counter >= interval
        @index_index += 1
        @index_index = 0 if @index_index >= indices.length
        @img_index = indices[@index_index]
        @anim_counter = 0
      end
    end

    # Causes the sprite to animate through the +indices+ array exactly once,
    # so that the animation stops once it reaches the last index in the array.
    # Subsequent calls with the same parameters will have no effect, but if
    # the index or interval changes, or if +set_animation+ is called, then a
    # new animation cycle will begin.
    #
    # Parameters:
    # [indices] The sequence of image indices used in the animation. See
    #           +animate+ for details.
    # [interval] The amount of frames between each change in the image index.
    #            See +animate+ for details.
    def animate_once(indices, interval)
      if @animate_once_control == 2
        return if indices == @animate_once_indices && interval == @animate_once_interval
        @animate_once_control = 0
      end

      unless @animate_once_control == 1
        @anim_counter = 0
        @img_index = indices[0]
        @index_index = 0
        @animate_once_indices = indices
        @animate_once_interval = interval
        @animate_once_control = 1
        return
      end

      @anim_counter += 1
      return unless @anim_counter >= interval

      @index_index += 1
      @img_index = indices[@index_index]
      @anim_counter = 0
      @animate_once_control = 2 if @index_index == indices.length - 1
    end

    # Resets the animation timer and immediately changes the image index to
    # the specified value.
    #
    # Parameters:
    # [index] The image index to be set.
    def set_animation(index)
      @anim_counter = 0
      @img_index = index
      @index_index = 0
      @animate_once_control = 0
    end

    # Draws the sprite in the screen
    #
    # Parameters:
    # [map] A Map object, relative to which the sprite will be drawn (the x
    #       and y coordinates of the sprite will be changed according to the
    #       position of the camera).
    # [scale_x] A scale factor to be applied horizontally to the image.
    # [scale_y] A scale factor to be applied vertically to the image.
    # [alpha] The opacity with which the image will be drawn. Valid values
    #         vary from 0 (fully transparent) to 255 (fully opaque).
    # [color] A color filter to apply to the image. A white (0xffffff) filter
    #         will keep all colors unchanged, while a black (0x000000) filter
    #         will turn all colors to black. A red (0xff0000) filter will keep
    #         reddish colors with slight or no change, whereas bluish colors
    #         will be darkened, for example.
    # [angle] A rotation, in degrees, to be applied to the image, relative to
    #         its center.
    # [flip] Specify +:horiz+ to draw the image horizontally flipped or +:vert+
    #        to draw it vertically flipped.
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    # [round] Specify whether the drawing coordinates should be rounded to an
    #         integer before drawing, to avoid little distortions of the image.
    #         Only applies when the image is not rotated.
    #
    # *Obs.:* This method accepts named parameters.
    def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, flip = nil, z_index = 0, round = false)
      if map.is_a? Hash
        scale_x = map.fetch(:scale_x, 1)
        scale_y = map.fetch(:scale_y, 1)
        alpha = map.fetch(:alpha, 0xff)
        color = map.fetch(:color, 0xffffff)
        angle = map.fetch(:angle, nil)
        flip = map.fetch(:flip, nil)
        z_index = map.fetch(:z_index, 0)
        round = map.fetch(:round, false)
        map = map.fetch(:map, nil)
      end

      color = (alpha << 24) | color
      if angle
        @img[@img_index].draw_rot @x - (map ? map.cam.x : 0) + @img[0].width * scale_x * 0.5,
                                  @y - (map ? map.cam.y : 0) + @img[0].height * scale_y * 0.5,
                                  z_index, angle, 0.5, 0.5, (flip == :horiz ? -scale_x : scale_x),
                                  (flip == :vert ? -scale_y : scale_y), color
      else
        x = @x - (map ? map.cam.x : 0) + (flip == :horiz ? scale_x * @img[0].width : 0)
        y = @y - (map ? map.cam.y : 0) + (flip == :vert ? scale_y * @img[0].height : 0)
        @img[@img_index].draw (round ? x.round : x), (round ? y.round : y),
                              z_index, (flip == :horiz ? -scale_x : scale_x),
                              (flip == :vert ? -scale_y : scale_y), color
      end
    end

    # Returns whether this sprite is visible in the given map (i.e., in the
    # viewport determined by the camera of the given map). If no map is given,
    # returns whether the sprite is visible on the screen.
    def visible?(map = nil)
      r = Rectangle.new @x, @y, @img[0].width, @img[0].height
      return Rectangle.new(0, 0, G.window.width, G.window.height).intersect? r if map.nil?
      map.cam.intersect? r
    end
  end

  # This class represents an object with a set of properties and methods
  # commonly used in games. It defines an object with a rectangular bounding
  # box, and having all the attributes required for using the Movement module.
  class GameObject < Sprite
    include Movement

    # Creates a new game object.
    #
    # Parameters:
    # [x] The x-coordinate of the object's bounding box. This can be modified
    #     later via the +x+ attribute.
    # [y] The y-coordinate of the object's bounding box. This can be modified
    #     later via the +y+ attribute.
    # [w] The width of the object's bounding box.
    # [h] The height of the object's bounding box.
    # [img] The image or spritesheet for the object.
    # [img_gap] A Vector object representing the difference between the top
    #           left corner of the bounding box and the coordinates of the
    #           image. For example, an object with x = 100,
    #           y = 50 and img_gap = Vector.new(-5, -5)
    #           will be drawn at position (95, 45) of the screen.
    # [sprite_cols] The number of columns in the spritesheet. Use +nil+ if the
    #               image is not a spritesheet.
    # [sprite_rows] The number of rows in the spritesheet. Use +nil+ if the
    #               image is not a spritesheet.
    # [mass] The mass of the object. Details on how it is used can be found
    #        in the Movement module.
    def initialize(x, y, w, h, img, img_gap = nil, sprite_cols = nil, sprite_rows = nil, mass = 1.0, retro = nil)
      super x, y, img, sprite_cols, sprite_rows, retro
      @w = w; @h = h
      @img_gap =
        if img_gap.nil?
          Vector.new 0, 0
        else
          img_gap
        end
      @mass = mass
      @speed = Vector.new 0, 0
      @max_speed = Vector.new 15, 15
      @stored_forces = Vector.new 0, 0
    end

    # Draws the game object in the screen.
    #
    # Parameters:
    # [map] A Map object, relative to which the object will be drawn (the x
    #       and y coordinates of the image will be changed according to the
    #       position of the camera).
    # [scale_x] A scale factor to be applied horizontally to the image.
    # [scale_y] A scale factor to be applied vertically to the image.
    # [alpha] The opacity with which the image will be drawn. Valid values
    #         vary from 0 (fully transparent) to 255 (fully opaque).
    # [color] A color filter to apply to the image. A white (0xffffff) filter
    #         will keep all colors unchanged, while a black (0x000000) filter
    #         will turn all colors to black. A red (0xff0000) filter will keep
    #         reddish colors with slight or no change, whereas bluish colors
    #         will be darkened, for example.
    # [angle] A rotation, in degrees, to be applied to the image, relative to
    #         its center.
    # [flip] Specify +:horiz+ to draw the image horizontally flipped or +:vert+
    #        to draw it vertically flipped.
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    # [round] Specify whether the drawing coordinates should be rounded to an
    #         integer before drawing, to avoid little distortions of the image.
    #         Only applies when the image is not rotated.
    #
    # *Obs.:* This method accepts named parameters.
    def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, flip = nil, z_index = 0, round = false)
      if map.is_a? Hash
        scale_x = map.fetch(:scale_x, 1)
        scale_y = map.fetch(:scale_y, 1)
        alpha = map.fetch(:alpha, 0xff)
        color = map.fetch(:color, 0xffffff)
        angle = map.fetch(:angle, nil)
        flip = map.fetch(:flip, nil)
        z_index = map.fetch(:z_index, 0)
        round = map.fetch(:round, false)
        map = map.fetch(:map, nil)
      end

      color = (alpha << 24) | color
      if angle
        center_x = @x + @w * 0.5
        center_y = @y + @h * 0.5
        o_x = center_x - @x - @img_gap.x
        o_y = center_y - @y - @img_gap.y
        @img[@img_index].draw_rot @x + (flip == :horiz ? -1 : 1) * (@img_gap.x + o_x) - (map ? map.cam.x : 0),
                                  @y + (flip == :vert ? -1 : 1) * (@img_gap.y + o_y) - (map ? map.cam.y : 0),
                                  z_index, angle, o_x.to_f / (@img[0].width * scale_x), o_y.to_f / (@img[0].height * scale_y),
                                  (flip == :horiz ? -scale_x : scale_x), (flip == :vert ? -scale_y : scale_y), color
      else
        x = @x + (flip == :horiz ? -1 : 1) * @img_gap.x - (map ? map.cam.x : 0) + (flip == :horiz ? @w : 0)
        y = @y + (flip == :vert ? -1 : 1) * @img_gap.y - (map ? map.cam.y : 0) + (flip == :vert ? @h : 0)
        @img[@img_index].draw (round ? x.round : x), (round ? y.round : y),
                              z_index, (flip == :horiz ? -scale_x : scale_x),
                              (flip == :vert ? -scale_y : scale_y), color
      end
    end

    # Returns whether this object is visible in the given map (i.e., in the
    # viewport determined by the camera of the given map). If no map is given,
    # returns whether the object is visible on the screen.
    def visible?(map = nil)
      r = Rectangle.new @x.round + @img_gap.x, @y.round + @img_gap.y, @img[0].width, @img[0].height
      return Rectangle.new(0, 0, G.window.width, G.window.height).intersect? r if map.nil?
      map.cam.intersect? r
    end
  end

  # Represents a visual effect, i.e., a graphic - usually animated - that shows
  # up in the screen, lasts for a given time and "disappears". You should
  # explicitly dispose of references to effects whose attribute +dead+ is set
  # to +true+.
  class Effect < Sprite
    # This is +true+ when the effect's lifetime has already passed.
    attr_reader :dead

    # Creates a new Effect.
    #
    # Parameters:
    # [x] The x-coordinate in the screen (or map) where the effect will be
    #     drawn. This can be modified later via the +x+ attribute.
    # [y] The y-coordinate in the screen (or map) where the effect will be
    #     drawn. This can be modified later via the +y+ attribute.
    # [img] The image or spritesheet to use for this effect (see Sprite for
    #       details on spritesheets).
    # [sprite_cols] (see Sprite)
    # [sprite_rows] (see Sprite)
    # [interval] The interval between steps of the animation, in updates.
    # [indices] The indices to use in the animation. See Sprite#animate for
    #           details. If +nil+, it will be the sequence from 0 to
    #           sprite_cols * sprite_rows - 1.
    # [lifetime] The lifetime of the effect, in updates. After +update+ is
    #            called this number of times, the effect will no longer
    #            be visible, even when +draw+ is called, and the +dead+ flag
    #            will be set to +true+, so you get to know when to dispose
    #            of the Effect object. If +nil+, it will be set to
    #            @indices.length * interval, i.e., the exact time
    #            needed for one animation cycle to complete.
    # [sound] The id of a sound to be played when the effect is created (id must
    #         be given in the format specified for the +Res.sound+ method).
    # [sound_ext] Extension of the sound file, if a sound is given. Default is
    #             '.wav'.
    # [sound_volume] The volume (from 0 to 1) to play the sound, if any. Default
    #                is 1.
    #
    # *Obs.:* This method accepts named parameters, but +x+, +y+ and +img+ are
    # mandatory.
    def initialize(x, y = nil, img = nil, sprite_cols = nil, sprite_rows = nil, interval = 10, indices = nil, lifetime = nil,
                   sound = nil, sound_ext = '.wav', sound_volume = 1)
      if x.is_a? Hash
        y = x[:y]
        img = x[:img]
        sprite_cols = x.fetch(:sprite_cols, nil)
        sprite_rows = x.fetch(:sprite_rows, nil)
        interval = x.fetch(:interval, 10)
        indices = x.fetch(:indices, nil)
        lifetime = x.fetch(:lifetime, nil)
        sound = x.fetch(:sound, nil)
        sound_ext = x.fetch(:sound_ext, '.wav')
        sound_volume = x.fetch(:sound_volume, 1)
        x = x[:x]
      end

      super x, y, img, sprite_cols, sprite_rows
      @timer = 0
      if indices
        @indices = indices
      else
        @indices = *(0..(@img.length - 1))
      end
      @interval = interval
      if lifetime
        @lifetime = lifetime
      else
        @lifetime = @indices.length * interval
      end
      Res.sound(sound, false, sound_ext).play(sound_volume) if sound
    end

    # Updates the effect, animating and counting its remaining lifetime.
    def update
      unless @dead
        animate @indices, @interval
        @timer += 1
        @dead = true if @timer == @lifetime
      end
    end

    def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, z_index = 0)
      super unless @dead
    end
  end
end


require_relative 'global'

module MiniGL
  module FormUtils # :nodoc:
    def self.check_anchor(anchor, x, y, w, h, area_w = G.window.width, area_h = G.window.height)
      if anchor
        case anchor
        when /^top(_center)?$|^north$/i then anchor_alias = :top_center; x += (area_w - w) / 2
        when /^top_right$|^northeast$/i then anchor_alias = :top_right; x = area_w - w - x
        when /^(center_)?left$|^west$/i then anchor_alias = :center_left; y += (area_h - h) / 2
        when /^center$/i then anchor_alias = :center; x += (area_w - w) / 2; y += (area_h - h) / 2
        when /^(center_)?right$|^east$/i then anchor_alias = :center_right; x = area_w - w - x; y += (area_h - h) / 2
        when /^bottom_left$|^southwest$/i then anchor_alias = :bottom_left; y = area_h - h - y
        when /^bottom(_center)?$|^south$/i then anchor_alias = :bottom_center; x += (area_w - w) / 2; y = area_h - h - y
        when /^bottom_right$|^southeast$/i then anchor_alias = :bottom_right; x = area_w - w - x; y = area_h - h - y
        else anchor_alias = :top_left
        end
      else
        anchor_alias = :top_left
      end
      [anchor_alias, x, y]
    end
  end

  # This class is an abstract ancestor for all form components (Button,
  # ToggleButton, TextField, DropDownList and ProgressBar).
  class Component
    # The horizontal coordinate of the component
    attr_reader :x

    # The vertical coordinate of the component
    attr_reader :y

    # The width of the component
    attr_reader :w

    # The height of the component
    attr_reader :h

    attr_reader :anchor, :anchor_offset_x, :anchor_offset_y # :nodoc:

    # Determines whether the control is enabled, i.e., will process user input.
    attr_accessor :enabled

    # Determines whether the control is visible, i.e., will be drawn in the
    # screen and process user input, if enabled.
    attr_accessor :visible

    # A container for any parameters to be passed to the code blocks called
    # in response to events of the control (click of a button, change of the
    # text in a text field, etc.). More detail can be found in the constructor
    # for each specific component class.
    attr_accessor :params

    def initialize(x, y, font, text, text_color, disabled_text_color) # :nodoc:
      @x = x
      @y = y
      @font = font
      @text = text
      @text_color = text_color
      @disabled_text_color = disabled_text_color
      @enabled = @visible = true
    end

    def update; end # :nodoc:

    # Sets the position of the component.
    # Parameters:
    # [x] The new x coordinate.
    # [y] The new y coordinate.
    def set_position(x, y)
      @x = x; @y = y
    end
  end

  # Represents a container of form components.
  class Panel
    # The horizontal position of the panel from the top left corner of the window.
    attr_reader :x

    # The vertical position of the panel from the top left corner of the window.
    attr_reader :y

    # The width of the panel in pixels.
    attr_reader :w

    # The height of the panel in pixels.
    attr_reader :h

    # Whether the components inside this panel are enabled.
    attr_reader :enabled

    # Gets or sets whether the panel (and thus all components inside it) are visible.
    attr_accessor :visible

    # Creates a new Panel.
    # Parameters:
    # [x] The horizontal coordinate of the top-left corner of the panel, or the horizontal offset from the anchor, if provided.
    # [y] The vertical coordinate of the top-left corner of the panel, or the vertical offset from the anchor, if provided.
    # [w] The width of the panel, in pixels.
    # [h] The height of the panel, in pixels.
    # [controls] An array of Components that will be initially inside this panel.
    # [img] Identifier of the image for the panel (see details in +Res::img+).
    # [img_mode] Mode to scale the image. If +:normal+ (default), the image will be loaded as a single image and scaled to fit the entire size of the panel;
    #            if +:tiled+, the image will be loaded as a 3x3 spritesheet, where the "corner" images (i.e., indices 0, 2, 6 and 8) will be scaled by the +scale_x+ and +scale_y+ parameters,
    #            the "border" images (indices 1, 3, 5 and 7) will be stretched in the corresponding direction (indices 1 and 7 will be horizontally stretched and indices 3 and 5, vertically),
    #            and the "center" image (index 4) will be stretched in both directions, as needed, to fill the width and height of the panel.
    # [retro] Whether the image should be loaded in retro mode.
    # [scale_x] The fixed horizontal scale for "corner" and left and right "border" images (if +img_mode+ is +:tiled+).
    # [scale_y] The fixed vertical scale for "corner" and top and bottom "border" images (if +img_mode+ is +:tiled+).
    # [anchor] An alias for a predefined position of the window to be used as "anchor", i.e., reference for the positioning of the panel.
    #          Following are the valid values and a description of the corresponding position if +x+ and +y+ are 0 (these will be offsets from the reference position):
    #          * +:north+ or +:top+ or +:top_center+: the panel will be horizontally centered and its top will be at the top of the window.
    #          * +:northeast+ or +:top_right+: the top-right corner of the panel will meet the top-right corner of the window.
    #          * +:west+ or +:left+ or +:center_left+: the panel will be vertically centered and its left edge will be at the left edge of the window.
    #          * +:center+: the panel will be horizontally and vertically centered on the window.
    #          * +:east+ or +:right+ or +:center_right+: the panel will be vertically centered and its right edge will be at the right edge of the window.
    #          * +:southwest+ or +:bottom_left+: the bottom-left corner of the panel will meet the bottom-left corner of the window.
    #          * +:south+ or +:bottom+ or +:bottom_center+: the panel will be horizontally centered and its bottom will be at the bottom of the window.
    #          * +:southeast+ or +:bottom_right+: the bottom-right corner of the panel will meet the bottom-right corner of the window.
    #          If a value is not provided, the reference is the top-left corner of the screen.
    #          Components added as children of Panels use the panel's coordinates as reference instead of the window.
    def initialize(x, y, w, h, controls = [], img = nil, img_mode = :normal, retro = nil, scale_x = 1, scale_y = 1, anchor = nil)
      _, x, y = FormUtils.check_anchor(anchor, x, y, w, h)
      @x = x; @y = y; @w = w; @h = h
      @controls = controls
      controls.each do |c|
        _, x, y = FormUtils.check_anchor(c.anchor, c.anchor_offset_x, c.anchor_offset_y, c.w, c.h, @w, @h)
        c.set_position(@x + x, @y + y)
      end

      if img
        retro = Res.retro_images if retro.nil?
        if img_mode == :tiled
          @img = Res.imgs(img, 3, 3, true, '.png', retro, true)
          @scale_x = scale_x
          @scale_y = scale_y
          @tile_w = @img[0].width * @scale_x
          @tile_h = @img[0].height * @scale_y
          @draw_center_x = @w > 2 * @tile_w
          @draw_center_y = @h > 2 * @tile_h
          @center_scale_x = (@w - 2 * @tile_w).to_f / @tile_w * @scale_x
          @center_scale_y = (@h - 2 * @tile_h).to_f / @tile_h * @scale_y
        else
          @img = Res.img(img, true, false, '.png', retro)
        end
      end

      @visible = @enabled = true
    end

    # Updates all child components of this panel.
    def update
      return unless @visible
      @controls.each(&:update)
    end

    # Enables or disables all child components of this panel.
    # Parameters:
    # [value] Whether the components should be enabled.
    def enabled=(value)
      @enabled = value
      @controls.each { |c| c.enabled = value }
    end

    # Adds a component to this panel.
    # Parameters:
    # [c] The component to add.
    def add_component(c)
      _, x, y = FormUtils.check_anchor(c.anchor, c.anchor_offset_x, c.anchor_offset_y, c.w, c.h, @w, @h)
      c.set_position(@x + x, @y + y)
      @controls << c
    end

    # Draws the panel and all its child components.
    # Parameters:
    # [alpha] The opacity of the panel (0 = fully transparent, 255 = fully opaque).
    # [z_index] The z-index to draw the panel.
    # [color] The color to apply as filter to the panel image and to all child components' images as well.
    def draw(alpha = 255, z_index = 0, color = 0xffffff)
      return unless @visible

      c = (alpha << 24) | color
      if @img
        if @img.is_a?(Array)
          @img[0].draw(@x, @y, z_index, @scale_x, @scale_y, c)
          @img[1].draw(@x + @tile_w, @y, z_index, @center_scale_x, @scale_y, c) if @draw_center_x
          @img[2].draw(@x + @w - @tile_w, @y, z_index, @scale_x, @scale_y, c)
          @img[3].draw(@x, @y + @tile_h, z_index, @scale_x, @center_scale_y, c) if @draw_center_y
          @img[4].draw(@x + @tile_w, @y + @tile_h, z_index, @center_scale_x, @center_scale_y, c) if @draw_center_x && @draw_center_y
          @img[5].draw(@x + @w - @tile_w, @y + @tile_h, z_index, @scale_x, @center_scale_y, c) if @draw_center_y
          @img[6].draw(@x, @y + @h - @tile_h, z_index, @scale_x, @scale_y, c)
          @img[7].draw(@x + @tile_w, @y + @h - @tile_h, z_index, @center_scale_x, @scale_y, c) if @draw_center_x
          @img[8].draw(@x + @w - @tile_w, @y + @h - @tile_h, z_index, @scale_x, @scale_y, c)
        else
          @img.draw(@x, @y, z_index, @w.to_f / @img.width, @h.to_f / @img.height)
        end
      end

      @controls.each { |k| k.draw(alpha, z_index, color) if k.visible }
    end
  end

  # This class represents a button.
  class Button < Component
    # The current state of the button.
    attr_reader :state

    # The text of the button.
    attr_accessor :text

    # Creates a button.
    #
    # Parameters:
    # [x] The x-coordinate where the button will be drawn in the screen.
    # [y] The y-coordinate where the button will be drawn in the screen.
    # [font] The Gosu::Font object that will be used to draw the
    #        button text.
    # [text] The button text. Can be +nil+ or empty.
    # [img] A spritesheet containing four images in a column, representing,
    #       from top to bottom, the default state, the hover state (when the
    #       mouse is over the button), the pressed state (when the mouse
    #       button is down and the cursor is over the button) and the disabled
    #       state. If +nil+, the +width+ and +height+ parameters must be
    #       provided.
    # [text_color] Color of the button text, in hexadecimal RRGGBB format.
    # [disabled_text_color] Color of the button text, when it's disabled, in
    #                       hexadecimal RRGGBB format.
    # [over_text_color] Color of the button text, when the cursor is over it
    #                   (hexadecimal RRGGBB).
    # [down_text_color] Color of the button text, when it is pressed
    #                   (hexadecimal RRGGBB).
    # [center_x] Whether the button text should be horizontally centered in its
    #            area (the area is defined by the image size, if an image is
    #            given, or by the +width+ and +height+ parameters, otherwise).
    # [center_y] Whether the button text should be vertically centered in its
    #            area (the area is defined by the image size, if an image is
    #            given, or by the +width+ and +height+ parameters, otherwise).
    # [margin_x] The x offset, from the button x-coordinate, to draw the text.
    #            This parameter is used only if +center+ is false.
    # [margin_y] The y offset, from the button y-coordinate, to draw the text.
    #            This parameter is used only if +center+ is false.
    # [width] Width of the button clickable area. This parameter is used only
    #         if +img+ is +nil+.
    # [height] Height of the button clickable area. This parameter is used
    #          only if +img+ is +nil+.
    # [params] An object containing any parameters you want passed to the
    #          +action+ block. When the button is clicked, the following is
    #          called:
    #            @action.call @params
    #          Note that this doesn't force you to declare a block that takes
    #          parameters.
    # [retro] Whether the image should be loaded with the 'retro' option set
    #         (see +Gosu::Image+ for details). If the value is omitted, the
    #         +Res.retro_images+ value will be used.
    # [scale_x] Horizontal scale to draw the component with.
    # [scale_y] Vertical scale to draw the component with.
    # [anchor] See parameter with the same name in Panel#initialize for details.
    # [action] The block of code executed when the button is clicked (or by
    #          calling the +click+ method).
    #
    # *Obs.:* This method accepts named parameters, but +x+ and +y+ are
    # mandatory (also, +img+ is mandatory when +width+ and +height+ are not
    # provided, and vice-versa).
    def initialize(x, y = nil, font = nil, text = nil, img = nil,
                   text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0,
                   center_x = true, center_y = true, margin_x = 0, margin_y = 0, width = nil, height = nil,
                   params = nil, retro = nil, scale_x = 1, scale_y = 1, anchor = nil, &action)
      if x.is_a? Hash
        y = x[:y]
        font = x[:font]
        text = x[:text]
        img = x[:img]
        text_color = x.fetch(:text_color, 0)
        disabled_text_color = x.fetch(:disabled_text_color, 0)
        over_text_color = x.fetch(:over_text_color, 0)
        down_text_color = x.fetch(:down_text_color, 0)
        center_x = x.fetch(:center_x, true)
        center_y = x.fetch(:center_y, true)
        margin_x = x.fetch(:margin_x, 0)
        margin_y = x.fetch(:margin_y, 0)
        width = x.fetch(:width, nil)
        height = x.fetch(:height, nil)
        params = x.fetch(:params, nil)
        retro = x.fetch(:retro, nil)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end

      retro = Res.retro_images if retro.nil?
      @scale_x = scale_x
      @scale_y = scale_y
      @img =
          if img; Res.imgs img, 1, 4, true, '.png', retro
          else; nil; end
      @w =
          if img; @img[0].width * @scale_x
          else; width * @scale_x; end
      @h =
          if img; @img[0].height * @scale_y
          else; height * @scale_y; end

      @anchor_offset_x = x; @anchor_offset_y = y
      @anchor, x, y = FormUtils.check_anchor(anchor, x, y, @w, @h)

      super x, y, font, text, text_color, disabled_text_color
      @over_text_color = over_text_color
      @down_text_color = down_text_color
      if center_x; @text_x = x + @w / 2 if @w
      else; @text_x = x + margin_x * @scale_x; end
      if center_y; @text_y = y + @h / 2 if @h
      else; @text_y = y + margin_y * @scale_y; end
      @center_x = center_x
      @center_y = center_y
      @action = action
      @params = params

      @state = :up
      @img_index = @enabled ? 0 : 3
    end

    # Updates the button, checking the mouse movement and buttons to define
    # the button state.
    def update
      return unless @enabled and @visible

      mouse_over = Mouse.over? @x, @y, @w, @h
      mouse_press = Mouse.button_pressed? :left
      mouse_rel = Mouse.button_released? :left

      if @state == :up
        if mouse_over
          @img_index = 1
          @state = :over
        else
          @img_index = 0
        end
      elsif @state == :over
        if not mouse_over
          @img_index = 0
          @state = :up
        elsif mouse_press
          @img_index = 2
          @state = :down
        else
          @img_index = 1
        end
      elsif @state == :down
        if not mouse_over
          @img_index = 0
          @state = :down_out
        elsif mouse_rel
          @img_index = 1
          @state = :over
          click
        else
          @img_index = 2
        end
      else # :down_out
        if mouse_over
          @img_index = 2
          @state = :down
        elsif mouse_rel
          @img_index = 0
          @state = :up
        else
          @img_index = 0
        end
      end
    end

    # Executes the button click action.
    def click
      @action.call @params if @action
    end

    # Sets the position of the button in the screen.
    #
    # Parameters:
    # [x] The new x-coordinate for the button.
    # [y] The new y-coordinate for the button.
    def set_position(x, y)
      if @center_x; @text_x = x + @w / 2
      else; @text_x += x - @x; end
      if @center_y; @text_y = y + @h / 2
      else; @text_y += y - @y; end
      @x = x; @y = y
    end

    # Draws the button in the screen.
    #
    # Parameters:
    # [alpha] The opacity with which the button will be drawn. Allowed values
    #         vary between 0 (fully transparent) and 255 (fully opaque).
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    # [color] Color to apply a filter to the image.
    def draw(alpha = 0xff, z_index = 0, color = 0xffffff)
      return unless @visible

      color = (alpha << 24) | color
      text_color =
        if @enabled
          if @state == :down
            @down_text_color
          else
            @state == :over ? @over_text_color : @text_color
          end
        else
          @disabled_text_color
        end
      text_color = (alpha << 24) | text_color
      @img[@img_index].draw @x, @y, z_index, @scale_x, @scale_y, color if @img
      if @text
        if @center_x or @center_y
          rel_x = @center_x ? 0.5 : 0
          rel_y = @center_y ? 0.5 : 0
          @font.draw_text_rel @text, @text_x, @text_y, z_index, rel_x, rel_y, @scale_x, @scale_y, text_color
        else
          @font.draw_text @text, @text_x, @text_y, z_index, @scale_x, @scale_y, text_color
        end
      end
    end

    def enabled=(value) # :nodoc:
      @enabled = value
      @state = :up
      @img_index = 3
    end
  end

  # This class represents a toggle button, which can be also interpreted as a
  # check box. It is always in one of two states, given as +true+ or +false+
  # by its property +checked+.
  class ToggleButton < Button
    # Defines the state of the button (returns +true+ or +false+).
    attr_reader :checked

    # Creates a ToggleButton. All parameters work the same as in Button,
    # except for the image, +img+, which now has to be composed of two columns
    # and four rows, the first column with images for the unchecked state,
    # and the second with images for the checked state, and for +checked+,
    # which defines the initial state of the ToggleButton.
    #
    # The +action+ block now will always receive a first boolean parameter
    # corresponding to the value of +checked+. So, if you want to pass
    # parameters to the block, you should declare it like this:
    #   b = ToggleButton.new ... { |checked, params|
    #     puts "button was checked" if checked
    #     # do something with params
    #   }
    #
    # *Obs.:* This method accepts named parameters, but +x+ and +y+ are
    # mandatory (also, +img+ is mandatory when +width+ and +height+ are not
    # provided, and vice-versa).
    def initialize(x, y = nil, font = nil, text = nil, img = nil, checked = false,
                   text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0,
                   center_x = true, center_y = true, margin_x = 0, margin_y = 0, width = nil, height = nil,
                   params = nil, retro = nil, scale_x = 1, scale_y = 1, anchor = nil, &action)
      if x.is_a? Hash
        y = x[:y]
        font = x[:font]
        text = x[:text]
        img = x[:img]
        checked = x.fetch(:checked, false)
        text_color = x.fetch(:text_color, 0)
        disabled_text_color = x.fetch(:disabled_text_color, 0)
        over_text_color = x.fetch(:over_text_color, 0)
        down_text_color = x.fetch(:down_text_color, 0)
        center_x = x.fetch(:center_x, true)
        center_y = x.fetch(:center_y, true)
        margin_x = x.fetch(:margin_x, 0)
        margin_y = x.fetch(:margin_y, 0)
        width = x.fetch(:width, nil)
        height = x.fetch(:height, nil)
        params = x.fetch(:params, nil)
        retro = x.fetch(:retro, nil)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end

      super x, y, font, text, nil, text_color, disabled_text_color, over_text_color, down_text_color,
            center_x, center_y, margin_x, margin_y, 0, 0, params, retro, scale_x, scale_y, anchor, &action
      @img =
        if img; Res.imgs img, 2, 4, true, '.png', retro
        else; nil; end
      @w =
        if img; @img[0].width * @scale_x
        else; width * @scale_x; end
      @h =
        if img; @img[0].height * @scale_y
        else; height * @scale_y; end
      _, x, y = FormUtils.check_anchor(anchor, @anchor_offset_x, @anchor_offset_y, @w, @h)
      set_position(x, y)
      @text_x = x + @w / 2 if center_x
      @text_y = y + @h / 2 if center_y
      @checked = checked
    end

    # Updates the button, checking the mouse movement and buttons to define
    # the button state.
    def update
      return unless @enabled and @visible

      super
      @img_index *= 2
      @img_index += 1 if @checked
    end

    # Executes the button click action, and toggles its state. The +action+
    # block always receives as a first parameter +true+, if the button has
    # been changed to checked, or +false+, otherwise.
    def click
      @checked = !@checked
      @action.call @checked, @params if @action
    end

    # Sets the state of the button to the value given.
    #
    # Parameters:
    # [value] The state to be set (+true+ for checked, +false+ for unchecked).
    def checked=(value)
      click if value != @checked
      @checked = value
    end

    def enabled=(value) # :nodoc:
      @enabled = value
      @state = :up
      @img_index = @checked ? 7 : 6
    end
  end

  # This class represents a text field (input).
  class TextField < Component
    # The current text inside the text field.
    attr_reader :text

    # The current 'locale' used for detecting the keys. THIS FEATURE IS
    # INCOMPLETE!
    attr_reader :locale

    # Creates a new text field.
    #
    # Parameters:
    # [x] The x-coordinate where the text field will be drawn in the screen.
    # [y] The y-coordinate where the text field will be drawn in the screen.
    # [font] The Gosu::Font object that will be used to draw the
    #        text inside the field.
    # [img] The image of the text field. For a good result, you would likely
    #       want something like a rectangle, horizontally wide, vertically
    #       short, and with a color that contrasts with the +text_color+.
    # [cursor_img] An image for the blinking cursor that stands in the point
    #              where text will be inserted. If +nil+, a simple black line
    #              will be drawn instead.
    # [disabled_img] Image for the text field when it's disabled. If +nil+,
    #                a darkened version of +img+ will be used.
    # [text_color] Color of the button text, in hexadecimal RRGGBB format.
    # [margin_x] The x offset, from the field x-coordinate, to draw the text.
    # [margin_y] The y offset, from the field y-coordinate, to draw the text.
    # [max_length] The maximum length of the text inside the field.
    # [active] Whether the text field must be focused by default. If +false+,
    #          focus can be granted by clicking inside the text field or by
    #          calling the +focus+ method.
    # [text] The starting text. Must not be +nil+.
    # [allowed_chars] A string containing all characters that can be typed
    #                 inside the text field. The complete set of supported
    #                 characters is given by the string
    #                 "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ'-=/[]\\\\,.;\"_+?{}|<>:!@#$%¨&*()".
    # [text_color] The color with which the text will be drawn, in hexadecimal
    #              RRGGBB format.
    # [disabled_text_color] The color with which the text will be drawn, when
    #                       the text field is disabled, in hexadecimal RRGGBB
    #                       format.
    # [selection_color] The color of the rectangle highlighting selected text,
    #                   in hexadecimal RRGGBB format. The rectangle will
    #                   always be drawn with 50% of opacity.
    # [locale] The locale to be used when detecting keys. By now, only 'en-US'
    #          and 'pt-BR' are **partially** supported. Default is 'en-US'. If
    #          any different value is supplied, all typed characters will be
    #          mapped to '#'.
    # [params] An object containing any parameters you want passed to the
    #          +on_text_changed+ block. When the text of the text field is
    #          changed, the following is called:
    #            @on_text_changed.call @text, @params
    #          Thus, +params+ will be the second parameter. Note that this
    #          doesn't force you to declare a block that takes parameters.
    # [retro] Whether the images should be loaded with the 'retro' option set
    #         (see +Gosu::Image+ for details). If the value is omitted, the
    #         +Res.retro_images+ value will be used.
    # [scale_x] Horizontal scale to draw the component with.
    # [scale_y] Vertical scale to draw the component with.
    # [anchor] See parameter with the same name in Panel#initialize for details.
    # [on_text_changed] The block of code executed when the text in the text
    #                   field is changed, either by user input or by calling
    #                   +text=+. The new text is passed as a first parameter
    #                   to this block, followed by +params+. Can be +nil+.
    #
    # *Obs.:* This method accepts named parameters, but +x+, +y+, +font+ and
    # +img+ are mandatory.
    def initialize(x, y = nil, font = nil, img = nil, cursor_img = nil, disabled_img = nil, margin_x = 0, margin_y = 0,
                   max_length = 100, active = false, text = '', allowed_chars = nil,
                   text_color = 0, disabled_text_color = 0, selection_color = 0, locale = 'en-us',
                   params = nil, retro = nil, scale_x = 1, scale_y = 1, anchor = nil, &on_text_changed)
      if x.is_a? Hash
        y = x[:y]
        font = x[:font]
        img = x[:img]
        cursor_img = x.fetch(:cursor_img, nil)
        disabled_img = x.fetch(:disabled_img, nil)
        margin_x = x.fetch(:margin_x, 0)
        margin_y = x.fetch(:margin_y, 0)
        max_length = x.fetch(:max_length, 100)
        active = x.fetch(:active, false)
        text = x.fetch(:text, '')
        allowed_chars = x.fetch(:allowed_chars, nil)
        text_color = x.fetch(:text_color, 0)
        disabled_text_color = x.fetch(:disabled_text_color, 0)
        selection_color = x.fetch(:selection_color, 0)
        locale = x.fetch(:locale, 'en-us')
        params = x.fetch(:params, nil)
        retro = x.fetch(:retro, nil)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end

      retro = Res.retro_images if retro.nil?
      @scale_x = scale_x
      @scale_y = scale_y
      @img = Res.img img, false, false, '.png', retro
      @w = @img.width * @scale_x
      @h = @img.height * @scale_y

      @anchor_offset_x = x; @anchor_offset_y = y
      @anchor, x, y = FormUtils.check_anchor(anchor, x, y, @w, @h)

      super x, y, font, text, text_color, disabled_text_color
      @cursor_img = Res.img(cursor_img, false, false, '.png', retro) if cursor_img
      @disabled_img = Res.img(disabled_img, false, false, '.png', retro) if disabled_img
      @max_length = max_length
      @active = active
      @text_x = x + margin_x * @scale_x
      @text_y = y + margin_y * @scale_y
      @selection_color = selection_color

      @nodes = [@text_x]
      send(:text=, text, false) if text

      @cur_node = 0
      @cursor_visible = false
      @cursor_timer = 0

      @k = [
        Gosu::KbA, Gosu::KbB, Gosu::KbC, Gosu::KbD, Gosu::KbE, Gosu::KbF,
        Gosu::KbG, Gosu::KbH, Gosu::KbI, Gosu::KbJ, Gosu::KbK, Gosu::KbL,
        Gosu::KbM, Gosu::KbN, Gosu::KbO, Gosu::KbP, Gosu::KbQ, Gosu::KbR,
        Gosu::KbS, Gosu::KbT, Gosu::KbU, Gosu::KbV, Gosu::KbW, Gosu::KbX,
        Gosu::KbY, Gosu::KbZ, Gosu::Kb1, Gosu::Kb2, Gosu::Kb3, Gosu::Kb4,
        Gosu::Kb5, Gosu::Kb6, Gosu::Kb7, Gosu::Kb8, Gosu::Kb9, Gosu::Kb0,
        Gosu::KbNumpad1, Gosu::KbNumpad2, Gosu::KbNumpad3, Gosu::KbNumpad4,
        Gosu::KbNumpad5, Gosu::KbNumpad6, Gosu::KbNumpad7, Gosu::KbNumpad8,
        Gosu::KbNumpad9, Gosu::KbNumpad0, Gosu::KbSpace, Gosu::KbBackspace,
        Gosu::KbDelete, Gosu::KbLeft, Gosu::KbRight, Gosu::KbHome,
        Gosu::KbEnd, Gosu::KbLeftShift, Gosu::KbRightShift,
        Gosu::KbBacktick, Gosu::KbMinus, Gosu::KbEqual, Gosu::KbBracketLeft,
        Gosu::KbBracketRight, Gosu::KbBackslash, Gosu::KbSemicolon,
        Gosu::KbApostrophe, Gosu::KbComma, Gosu::KbPeriod, Gosu::KbSlash,
        Gosu::KbNumpadAdd, Gosu::KbNumpadSubtract,
        Gosu::KbNumpadMultiply, Gosu::KbNumpadDivide
      ]
      @user_allowed_chars = allowed_chars
      self.locale = locale

      @on_text_changed = on_text_changed
      @params = params
    end

    # Updates the text field, checking for mouse events and keyboard input.
    def update
      return unless @enabled and @visible

      ################################ Mouse ################################
      if Mouse.over? @x, @y, @w, @h
        if not @active and Mouse.button_pressed? :left
          focus
        end
      elsif Mouse.button_pressed? :left
        unfocus
      end

      return unless @active

      if Mouse.double_click? :left
        if @nodes.size > 1
          @anchor1 = 0
          @anchor2 = @nodes.size - 1
          @cur_node = @anchor2
          @double_clicked = true
        end
        set_cursor_visible
      elsif Mouse.button_pressed? :left
        set_node_by_mouse
        @anchor1 = @cur_node
        @anchor2 = nil
        @double_clicked = false
        set_cursor_visible
      elsif Mouse.button_down? :left
        if @anchor1 and not @double_clicked
          set_node_by_mouse
          if @cur_node != @anchor1; @anchor2 = @cur_node
          else; @anchor2 = nil; end
          set_cursor_visible
        end
      elsif Mouse.button_released? :left
        if @anchor1 and not @double_clicked
          if @cur_node != @anchor1; @anchor2 = @cur_node
          else; @anchor1 = nil; end
        end
      end

      @cursor_timer += 1
      if @cursor_timer >= 30
        @cursor_visible = (not @cursor_visible)
        @cursor_timer = 0
      end

      ############################### Keyboard ##############################
      shift = (KB.key_down?(@k[53]) or KB.key_down?(@k[54]))
      if KB.key_pressed?(@k[53]) or KB.key_pressed?(@k[54]) # shift
        @anchor1 = @cur_node if @anchor1.nil?
      elsif KB.key_released?(@k[53]) or KB.key_released?(@k[54])
        @anchor1 = nil if @anchor2.nil?
      end
      inserted = false
      for i in 0..46 # alnum
        if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i])
          remove_interval true if @anchor1 and @anchor2
          if i < 26
            if shift
              insert_char @chars[i + 37]
            else
              insert_char @chars[i]
            end
          elsif i < 36
            if shift; insert_char @chars[i + 59]
            else; insert_char @chars[i]; end
          elsif shift
            insert_char(@chars[i + 49])
          else
            insert_char(@chars[i - 10])
          end
          inserted = true
          break
        end
      end

      return if inserted
      for i in 55..65 # special
        if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i])
          remove_interval true if @anchor1 and @anchor2
          if shift; insert_char @chars[i + 19]
          else; insert_char @chars[i + 8]; end
          inserted = true
          break
        end
      end

      return if inserted
      for i in 66..69 # numpad operators
        if KB.key_pressed?(@k[i]) or KB.key_held?(@k[i])
          remove_interval true if @anchor1 and @anchor2
          insert_char @chars[i + 19]
          inserted = true
          break
        end
      end

      return if inserted
      if KB.key_pressed?(@k[47]) or KB.key_held?(@k[47]) # back
        if @anchor1 and @anchor2
          remove_interval
        elsif @cur_node > 0
          remove_char true
        end
      elsif KB.key_pressed?(@k[48]) or KB.key_held?(@k[48]) # del
        if @anchor1 and @anchor2
          remove_interval
        elsif @cur_node < @nodes.size - 1
          remove_char false
        end
      elsif KB.key_pressed?(@k[49]) or KB.key_held?(@k[49]) # left
        if @anchor1
          if shift
            if @cur_node > 0
              @cur_node -= 1
              @anchor2 = @cur_node
              set_cursor_visible
            end
          elsif @anchor2
            @cur_node = @anchor1 < @anchor2 ? @anchor1 : @anchor2
            @anchor1 = nil
            @anchor2 = nil
            set_cursor_visible
          end
        elsif @cur_node > 0
          @cur_node -= 1
          set_cursor_visible
        end
      elsif KB.key_pressed?(@k[50]) or KB.key_held?(@k[50]) # right
        if @anchor1
          if shift
            if @cur_node < @nodes.size - 1
              @cur_node += 1
              @anchor2 = @cur_node
              set_cursor_visible
            end
          elsif @anchor2
            @cur_node = @anchor1 > @anchor2 ? @anchor1 : @anchor2
            @anchor1 = nil
            @anchor2 = nil
            set_cursor_visible
          end
        elsif @cur_node < @nodes.size - 1
          @cur_node += 1
          set_cursor_visible
        end
      elsif KB.key_pressed?(@k[51]) # home
        @cur_node = 0
        if shift; @anchor2 = @cur_node
        else
          @anchor1 = nil
          @anchor2 = nil
        end
        set_cursor_visible
      elsif KB.key_pressed?(@k[52]) # end
        @cur_node = @nodes.size - 1
        if shift; @anchor2 = @cur_node
        else
          @anchor1 = nil
          @anchor2 = nil
        end
        set_cursor_visible
      end
    end

    # Sets the text of the text field to the specified value.
    #
    # Parameters:
    # [value] The new text to be set. If it's longer than the +max_length+
    #         parameter used in the constructor, it will be truncated to
    #         +max_length+ characters.
    def text=(value, trigger_changed = true)
      @text = value[0...@max_length]
      @nodes.clear; @nodes << @text_x
      x = @nodes[0]
      @text.chars.each { |char|
        x += @font.text_width(char) * @scale_x
        @nodes << x
      }
      @cur_node = @nodes.size - 1
      @anchor1 = nil
      @anchor2 = nil
      set_cursor_visible
      @on_text_changed.call @text, @params if trigger_changed && @on_text_changed
    end

    # Sets the locale used by the text field to detect keys. Only 'en-us' and
    # 'pt-br' are **partially** supported. If any different value is supplied,
    # all typed characters will be mapped to '#'.
    def locale=(value)
      @locale = value.downcase
      @chars =
        case @locale
          when 'en-us' then "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ`-=[]\\;',./~_+{}|:\"<>?!@#$%^&*()+-*/"
          when 'pt-br' then "abcdefghijklmnopqrstuvwxyz1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ'-=/[]ç~,.;\"_+?{}Ç^<>:!@#$%¨&*()+-*/"
          else              '###################################################################################################'
        end
      @allowed_chars =
        if @user_allowed_chars
          @user_allowed_chars
        else
          @chars
        end
    end

    # Returns the currently selected text.
    def selected_text
      return '' if @anchor2.nil?
      min = @anchor1 < @anchor2 ? @anchor1 : @anchor2
      max = min == @anchor1 ? @anchor2 : @anchor1
      @text[min..max]
    end

    # Grants focus to the text field, so that it allows keyboard input.
    def focus
      @active = true
    end

    # Removes focus from the text field, so that no keyboard input will be
    # accepted.
    def unfocus
      @anchor1 = @anchor2 = nil
      @cursor_visible = false
      @cursor_timer = 0
      @active = false
    end

    # Sets the position of the text field in the screen.
    #
    # Parameters:
    # [x] The new x-coordinate for the text field.
    # [y] The new y-coordinate for the text field.
    def set_position(x, y)
      d_x = x - @x
      d_y = y - @y
      @x = x; @y = y
      @text_x += d_x
      @text_y += d_y
      @nodes.map! do |n|
        n + d_x
      end
    end

    # Draws the text field in the screen.
    #
    # Parameters:
    # [alpha] The opacity with which the text field will be drawn. Allowed
    #         values vary between 0 (fully transparent) and 255 (fully opaque).
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    # [color] Color to apply a filter to the image.
    # [disabled_color] Color to apply a filter to the image when the field is
    #                  disabled.
    def draw(alpha = 0xff, z_index = 0, color = 0xffffff, disabled_color = 0x808080)
      return unless @visible

      color = (alpha << 24) | ((@enabled or @disabled_img) ? color : disabled_color)
      text_color = (alpha << 24) | (@enabled ? @text_color : @disabled_text_color)
      img = ((@enabled or @disabled_img.nil?) ? @img : @disabled_img)
      img.draw @x, @y, z_index, @scale_x, @scale_y, color
      @font.draw_text @text, @text_x, @text_y, z_index, @scale_x, @scale_y, text_color

      if @anchor1 and @anchor2
        selection_color = ((alpha / 2) << 24) | @selection_color
        G.window.draw_quad @nodes[@anchor1], @text_y, selection_color,
                           @nodes[@anchor2] + 1, @text_y, selection_color,
                           @nodes[@anchor2] + 1, @text_y + @font.height * @scale_y, selection_color,
                           @nodes[@anchor1], @text_y + @font.height * @scale_y, selection_color, z_index
      end

      if @cursor_visible
        if @cursor_img
          @cursor_img.draw @nodes[@cur_node] - (@cursor_img.width * @scale_x) / 2, @text_y, z_index, @scale_x, @scale_y
        else
          cursor_color = alpha << 24
          G.window.draw_quad @nodes[@cur_node], @text_y, cursor_color,
                             @nodes[@cur_node] + 1, @text_y, cursor_color,
                             @nodes[@cur_node] + 1, @text_y + @font.height * @scale_y, cursor_color,
                             @nodes[@cur_node], @text_y + @font.height * @scale_y, cursor_color, z_index
        end
      end
    end

    def enabled=(value) # :nodoc:
      @enabled = value
      unfocus unless @enabled
    end

    def visible=(value) # :nodoc:
      @visible = value
      unfocus unless @visible
    end

  private

    def set_cursor_visible
      @cursor_visible = true
      @cursor_timer = 0
    end

    def set_node_by_mouse
      index = @nodes.size - 1
      @nodes.each_with_index do |n, i|
        if n >= Mouse.x
          index = i
          break
        end
      end
      if index > 0
        d1 = @nodes[index] - Mouse.x; d2 = Mouse.x - @nodes[index - 1]
        index -= 1 if d1 > d2
      end
      @cur_node = index
    end

    def insert_char(char)
      return unless @allowed_chars.index char and @text.length < @max_length
      @text.insert @cur_node, char
      @nodes.insert @cur_node + 1, @nodes[@cur_node] + @font.text_width(char) * @scale_x
      for i in (@cur_node + 2)..(@nodes.size - 1)
        @nodes[i] += @font.text_width(char) * @scale_x
      end
      @cur_node += 1
      set_cursor_visible
      @on_text_changed.call @text, @params if @on_text_changed
    end

    def remove_interval(will_insert = false)
      min = @anchor1 < @anchor2 ? @anchor1 : @anchor2
      max = min == @anchor1 ? @anchor2 : @anchor1
      interval_width = 0
      for i in min...max
        interval_width += @font.text_width(@text[i]) * @scale_x
        @nodes.delete_at min + 1
      end
      @text[min...max] = ''
      for i in (min + 1)..(@nodes.size - 1)
        @nodes[i] -= interval_width
      end
      @cur_node = min
      @anchor1 = nil
      @anchor2 = nil
      set_cursor_visible
      @on_text_changed.call @text, @params if @on_text_changed and not will_insert
    end

    def remove_char(back)
      @cur_node -= 1 if back
      char_width = @font.text_width(@text[@cur_node]) * @scale_x
      @text[@cur_node] = ''
      @nodes.delete_at @cur_node + 1
      for i in (@cur_node + 1)..(@nodes.size - 1)
        @nodes[i] -= char_width
      end
      set_cursor_visible
      @on_text_changed.call @text, @params if @on_text_changed
    end
  end

  # Represents a progress bar.
  class ProgressBar < Component
    # The maximum value for this progress bar (when the current value equals
    # the maximum, the bar is full).
    attr_reader :max_value

    # The current value of the progress bar (an integer greater than or equal
    # to zero, and less than or equal to +max_value+).
    attr_reader :value

    # Creates a progress bar.
    #
    # Parameters:
    # [x] The x-coordinate of the progress bar on the screen.
    # [y] The y-coordinate of the progress bar on the screen.
    # [w] Width of the progress bar, in pixels. This is the maximum space the
    #     bar foreground can occupy. Note that the width of the foreground image
    #     (+fg+) can be less than this, in which case the image will be
    #     horizontally repeated to fill all the needed space.
    # [h] Height of the progress bar. This will be the height of the bar
    #     foreground when +fg+ is a color (when it is an image, the height of
    #     the image will be kept).
    # [bg] A background image (string or symbol that will be passed to
    #      +Res.img+) or color (in RRGGBB hexadecimal format).
    # [fg] A foreground image (string or symbol that will be passed to
    #      +Res.img+) or color (in RRGGBB hexadecimal format). The image will
    #      be horizontally repeated when needed, if its width is less than +w+.
    # [max_value] The maximum value the progress bar can reach (an integer).
    # [value] The starting value for the progress bar.
    # [fg_margin_x] Horizontal margin between the background image and the
    #               foreground image (when these are provided).
    # [fg_margin_y] Vertical margin between the background image and the
    #               foreground image (when these are provided).
    # [font] Font that will be used to draw a text indicating the value of the
    #        progress bar.
    # [text_color] Color of the text.
    # [format] Format to display the value. Specify '%' for a percentage and
    #          anything else for absolute values (current/maximum).
    # [retro] Whether the images should be loaded with the 'retro' option set
    #         (see +Gosu::Image+ for details). If the value is omitted, the
    #         +Res.retro_images+ value will be used.
    # [scale_x] Horizontal scale to draw the component with.
    # [scale_y] Vertical scale to draw the component with.
    # [anchor] See parameter with the same name in Panel#initialize for details.
    #
    # *Obs.:* This method accepts named parameters, but +x+, +y+, +w+, +h+, +bg+
    # and +fg+ are mandatory.
    def initialize(x, y = nil, w = nil, h = nil, bg = nil, fg = nil,
                   max_value = 100, value = 100, fg_margin_x = 0, fg_margin_y = 0, # fg_left = nil, fg_right = nil,
                   font = nil, text_color = 0, format = nil, retro = nil, scale_x = 1, scale_y = 1, anchor = nil)
      if x.is_a? Hash
        y = x[:y]
        w = x[:w]
        h = x[:h]
        bg = x[:bg]
        fg = x[:fg]
        max_value = x.fetch(:max_value, 100)
        value = x.fetch(:value, 100)
        fg_margin_x = x.fetch(:fg_margin_x, 0)
        fg_margin_y = x.fetch(:fg_margin_y, 0)
        font = x.fetch(:font, nil)
        text_color = x.fetch(:text_color, 0)
        format = x.fetch(:format, nil)
        retro = x.fetch(:retro, nil)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end

      @scale_x = scale_x
      @scale_y = scale_y
      retro = Res.retro_images if retro.nil?
      if bg.is_a? Integer
        @bg_color = bg
      else # String or Symbol
        @bg = Res.img bg, false, false, '.png', retro
      end
      if fg.is_a? Integer
        @fg_color = fg
      else # String or Symbol
        @fg = Res.img fg, false, false, '.png', retro
        @fg_path = "#{Res.prefix}#{Res.img_dir}#{fg.to_s.gsub(Res.separator, '/')}.png"
      end
      @fg_margin_x = fg_margin_x * @scale_x
      @fg_margin_y = fg_margin_y * @scale_y

      @w = (@bg ? @bg.width : w) * @scale_x
      @h = (@bg ? @bg.height : h) * @scale_y

      @anchor_offset_x = x; @anchor_offset_y = y
      @anchor, x, y = FormUtils.check_anchor(anchor, x, y, @w, @h)

      super x, y, font, '', text_color, text_color
      # @fg_left = fg_left
      # @fg_right = fg_right
      @max_value = max_value
      self.value = value
      @format = format
      @retro = retro
    end

    # Increases the current value of the progress bar by the given amount.
    #
    # Parameters:
    # [amount] (+Integer+) The amount to be added to the current value. If the
    #          sum surpasses +max_value+, it is set to +max_value+.
    def increase(amount)
      @value += amount
      @value = @max_value if @value > @max_value
    end

    # Descreases the current value of the progress bar by the given amount.
    #
    # Parameters:
    # [amount] (+Integer+) The amount to be subtracted from the current value.
    #          If the result is less than zero, it is set to zero.
    def decrease(amount)
      @value -= amount
      @value = 0 if @value < 0
    end

    # Sets the value of the progress bar.
    #
    # Parameters:
    # [val] (+Integer+) The value to be set. It will be changed as needed to be
    #       between zero and +max_value+.
    def value=(val)
      @value = val
      if @value > @max_value
        @value = @max_value
      elsif @value < 0
        @value = 0
      end
    end

    # Sets the value of the progress bar to a given percentage of +max_value+.
    #
    # Parameters:
    # [pct] (+Numeric+) The percentage of +max_value+ to set the current value
    #       to. The final result will be changed as needed to be between zero
    #       and +max_value+.
    def percentage=(pct)
      self.value = (pct * @max_value).round
    end

    # Draws the progress bar.
    #
    # Parameters:
    # [alpha] (+Fixnum+) The opacity with which the progress bar will be drawn.
    #         Allowed values vary between 0 (fully transparent) and 255 (fully
    #         opaque).
    # [z_index] (+Fixnum+) The z-order to draw the object. Objects with larger
    #           z-orders will be drawn on top of the ones with smaller z-orders.
    # [color] Color to apply a filter to the images (when these are provided).
    def draw(alpha = 0xff, z_index = 0, color = 0xffffff)
      return unless @visible

      if @bg
        c = (alpha << 24) | color
        @bg.draw @x, @y, z_index, @scale_x, @scale_y, c
      else
        c = (alpha << 24) | @bg_color
        G.window.draw_quad @x, @y, c,
                           @x + @w, @y, c,
                           @x + @w, @y + @h, c,
                           @x, @y + @h, c, z_index
      end
      if @fg
        c = (alpha << 24) | color
        w1 = @fg.width * @scale_x
        w2 = (@value.to_f / @max_value * @w).round
        x0 = @x + @fg_margin_x
        x = 0
        while x <= w2 - w1
          @fg.draw x0 + x, @y + @fg_margin_y, z_index, @scale_x, @scale_y, c
          x += w1
        end
        if w2 - x > 0
          img = Gosu::Image.new(@fg_path, tileable: true, retro: @retro, rect: [0, 0, ((w2 - x) / @scale_x).round, @fg.height])
          img.draw x0 + x, @y + @fg_margin_y, z_index, @scale_x, @scale_y, c
        end
      else
        c = (alpha << 24) | @fg_color
        rect_r = @x + (@value.to_f / @max_value * @w).round
        G.window.draw_quad @x, @y, c,
                           rect_r, @y, c,
                           rect_r, @y + @h, c,
                           @x, @y + @h, c, z_index
      end
      if @font
        c = (alpha << 24) | @text_color
        @text = @format == '%' ? "#{(@value.to_f / @max_value * 100).round}%" : "#{@value}/#{@max_value}"
        @font.draw_text_rel @text, @x + @w / 2, @y + @h / 2, z_index, 0.5, 0.5, @scale_x, @scale_y, c
      end
    end
  end

  # This class represents a "drop-down list" form component, here composed of a
  # group of +Button+ objects.
  class DropDownList < Component
    # The selected value in the drop-down list. This is one of the +options+.
    attr_reader :value

    # An array containing all the options (each of them +String+s) that can be
    # selected in the drop-down list.
    attr_accessor :options

    # Creates a new drop-down list.
    #
    # Parameters:
    # [x] The x-coordinate of the object.
    # [y] The y-coordinate of the object.
    # [font] Font to be used by the buttons that compose the drop-down list.
    # [img] Image of the main button, i.e., the one at the top, that toggles
    #       visibility of the other buttons (the "option" buttons).
    # [opt_img] Image for the "option" buttons, as described above.
    # [options] Array of available options for this control (+String+s).
    # [option] Index of the firstly selected option.
    # [text_margin] Left margin of the text inside the buttons (vertically, the
    #               text will always be centered).
    # [width] Width of the control, used when no image is provided.
    # [height] Height of the control, used when no image is provided.
    # [text_color] Used as the +text_color+ parameter in the constructor of the
    #              buttons.
    # [disabled_text_color] Analogous to +text_color+.
    # [over_text_color] Same as above.
    # [down_text_color] Same as above.
    # [retro] Whether the images should be loaded with the 'retro' option set
    #         (see +Gosu::Image+ for details). If the value is omitted, the
    #         +Res.retro_images+ value will be used.
    # [scale_x] Horizontal scale to draw the component with.
    # [scale_y] Vertical scale to draw the component with.
    # [anchor] See parameter with the same name in Panel#initialize for details.
    # [on_changed] Action performed when the value of the dropdown is changed.
    #              It must be a block with two parameters, which will receive
    #              the old and the new value, respectively.
    #
    # *Obs.:* This method accepts named parameters, but +x+, +y+, +font+ and
    # +options+ are mandatory (also, +img+ and +opt_img+ are mandatory when
    # +width+ and +height+ are not provided, and vice-versa).
    def initialize(x, y = nil, font = nil, img = nil, opt_img = nil, options = nil,
                   option = 0, text_margin = 0, width = nil, height = nil,
                   text_color = 0, disabled_text_color = 0, over_text_color = 0, down_text_color = 0,
                   retro = nil, scale_x = 1, scale_y = 1, anchor = nil, &on_changed)
      if x.is_a? Hash
        y = x[:y]
        font = x[:font]
        img = x[:img]
        opt_img = x[:opt_img]
        options = x[:options]
        option = x.fetch(:option, 0)
        text_margin = x.fetch(:text_margin, 0)
        width = x.fetch(:width, nil)
        height = x.fetch(:height, nil)
        text_color = x.fetch(:text_color, 0)
        disabled_text_color = x.fetch(:disabled_text_color, 0)
        over_text_color = x.fetch(:over_text_color, 0)
        down_text_color = x.fetch(:down_text_color, 0)
        retro = x.fetch(:retro, nil)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end
      @img = img
      @opt_img = opt_img
      @options = options
      @value = @options[option]
      @open = false
      @buttons = []
      @buttons.push(
        Button.new(x, y, font, @value, img, text_color, disabled_text_color, over_text_color, down_text_color,
                   false, true, text_margin, 0, width, height, nil, retro, scale_x, scale_y) {
                     toggle
                   }
      )

      @scale_x = scale_x
      @scale_y = scale_y
      @w = @buttons[0].w
      @h = @buttons[0].h
      @max_h = (@options.size + 1) * @h

      @anchor_offset_x = x; @anchor_offset_y = y
      @anchor, x, y = FormUtils.check_anchor(anchor, x, y, @w, @h)
      super x, y, font, options[option], text_color, disabled_text_color
      @buttons[0].set_position(x, y)

      @options.each_with_index do |o, i|
        b = Button.new(x, y + (i+1) * @h, font, o, opt_img, text_color, disabled_text_color, over_text_color, down_text_color,
                       false, true, text_margin, 0, width, height, nil, retro, scale_x, scale_y) {
                         old = @value
                         @value = @buttons[0].text = o
                         @on_changed.call(old, o) if @on_changed
                         toggle
                       }
        b.visible = false
        @buttons.push b
      end

      @on_changed = on_changed
    end

    # Updates the control.
    def update
      return unless @enabled and @visible
      if @open and Mouse.button_pressed? :left and not Mouse.over?(@x, @y, @w, @max_h)
        toggle
        return
      end
      @buttons.each { |b| b.update }
    end

    # Sets the currently selected value of the drop-down list. It is ignored if
    # it is not among the available options.
    def value=(val)
      if @options.include? val
        old = @value
        @value = @buttons[0].text = val
        @on_changed.call(old, val) if @on_changed
      end
    end

    def enabled=(value) # :nodoc:
      toggle if @open
      @buttons[0].enabled = value
      @enabled = value
    end

    def set_position(x, y)
      @x = x; @y = y
      @buttons.each_with_index { |b, i| b.set_position(x, y + i * @h) }
    end

    # Draws the drop-down list.
    #
    # Parameters:
    # [alpha] (+Fixnum+) The opacity with which the drop-down list will be
    #         drawn. Allowed values vary between 0 (fully transparent) and 255
    #         (fully opaque).
    # [z_index] (+Fixnum+) The z-order to draw the object. Objects with larger
    #           z-orders will be drawn on top of the ones with smaller z-orders.
    # [color] Color of the buttons, if no image was provided, or color to apply
    #         a filter to the images.
    # [over_color] Color of the buttons when the mouse is over them (when no
    #              image was provided).
    def draw(alpha = 0xff, z_index = 0, color = 0xffffff, over_color = 0xcccccc)
      return unless @visible
      unless @img
        bottom = @y + (@open ? @max_h : @h) + @scale_y
        b_color = (alpha << 24)
        G.window.draw_quad @x - @scale_x, @y - @scale_y, b_color,
                           @x + @w + @scale_x, @y - @scale_y, b_color,
                           @x + @w + @scale_x, bottom, b_color,
                           @x - @scale_x, bottom, b_color, z_index
        @buttons.each do |b|
          c = (alpha << 24) | (b.state == :over ? over_color : color)
          G.window.draw_quad b.x, b.y, c,
                             b.x + b.w, b.y, c,
                             b.x + b.w, b.y + b.h, c,
                             b.x, b.y + b.h, c, z_index + 1 if b.visible
        end
      end
      @buttons[0].draw(alpha, z_index, color)
      @buttons[1..-1].each { |b| b.draw alpha, z_index + 1, color }
    end

    private

    def toggle
      if @open
        @buttons[1..-1].each { |b| b.visible = false }
        @open = false
      else
        @buttons[1..-1].each { |b| b.visible = true }
        @open = true
      end
    end
  end

  # This class represents a label.
  class Label < Component
    # Creates a new label.
    #
    # Parameters:
    # [x] The x-coordinate of the label.
    # [y] The x-coordinate of the label.
    # [font] Font that will be used to draw the label's text.
    # [text] The label's text.
    # [text_color] The default text color.
    # [disabled_text_color] The text color when the label is disabled.
    # [scale_x] The horizontal scale factor.
    # [scale_y] The vertical scale factor.
    # [anchor] See parameter with the same name in Panel#initialize for details.
    def initialize(x, y = nil, font = nil, text = nil, text_color = 0, disabled_text_color = 0, scale_x = 1, scale_y = 1, anchor = nil)
      if x.is_a? Hash
        y = x[:y]
        font = x[:font]
        text = x[:text]
        text_color = x.fetch(:text_color, 0)
        disabled_text_color = x.fetch(:disabled_text_color, 0)
        scale_x = x.fetch(:scale_x, 1)
        scale_y = x.fetch(:scale_y, 1)
        anchor = x.fetch(:anchor, nil)
        x = x[:x]
      end

      @scale_x = scale_x
      @scale_y = scale_y
      @w = font.text_width(text) * scale_x
      @h = font.height * scale_y
      @anchor_offset_x = x; @anchor_offset_y = y
      @anchor, x, y = FormUtils.check_anchor(anchor, x, y, @w, @h)
      super(x, y, font, text, text_color, disabled_text_color)
    end

    # Draws the label.
    #
    # Parameters:
    # [alpha] The opacity with which the label will be drawn. Allowed values
    #         vary between 0 (fully transparent) and 255 (fully opaque).
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    # [color] Color to apply a filter to the text.
    def draw(alpha = 255, z_index = 0, color = 0xffffff)
      c = @enabled ? @text_color : @disabled_text_color
      r1 = c >> 16
      g1 = (c & 0xff00) >> 8
      b1 = (c & 0xff)
      r2 = color >> 16
      g2 = (color & 0xff00) >> 8
      b2 = (color & 0xff)
      r1 *= r2; r1 /= 255
      g1 *= g2; g1 /= 255
      b1 *= b2; b1 /= 255
      color = (alpha << 24) | (r1 << 16) | (g1 << 8) | b1
      @font.draw_text(@text, @x, @y, z_index, @scale_x, @scale_y, color)
    end
  end
end


module MiniGL
  # This class provides methods for easily drawing one or multiple lines of
  # text, with control over the text alignment and coloring.
  class TextHelper
    # Creates a TextHelper.
    #
    # Parameters:
    # [font] A Gosu::Font that will be used to draw the text.
    # [line_spacing] When drawing multiple lines, the distance, in pixels,
    #                between each line.
    def initialize(font, line_spacing = 0)
      @font = font
      @line_spacing = line_spacing
    end

    # Draws a single line of text.
    #
    # Parameters:
    # [text] The text to be drawn. No line breaks are allowed. You can use the
	#        `<b>` tag for bold, `<i>` for italic and `<c=rrggbb>` for colors.
    # [x] The horizontal reference for drawing the text. If +mode+ is +:left+,
    #     all text will be drawn from this point to the right; if +mode+ is
    #     +:right+, all text will be drawn from this point to the left; and if
    #     +mode+ is +:center+, the text will be equally distributed to the
    #     left and to the right of this point.
    # [y] The vertical reference for drawing the text. All text will be drawn
    #     from this point down.
    # [mode] The alignment of the text. Valid values are +:left+, +:right+ and
    #        +:center+.
    # [color] The color of the text, in hexadecimal RRGGBB format.
    # [alpha] The opacity of the text. Valid values vary from 0 (fully
    #         transparent) to 255 (fully opaque).
    # [effect] Effect to add to the text. It can be either +nil+, for no effect,
    #          +:border+ for bordered text, or +:shadow+ for shadowed text (the
    #          shadow is always placed below and to the right of the text).
    # [effect_color] Color of the effect, if any.
    # [effect_size] Size of the effect, if any. In the case of +:border+, this
    #               will be the width of the border (the border will only look
    #               good when +effect_size+ is relatively small, compared to the
    #               size of the font); in the case of +:shadow+, it will be the
    #               distance between the text and the shadow.
    # [effect_alpha] Opacity of the effect, if any. For shadows, it is usual to
    #                provide less than 255.
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    #
    # *Obs.:* This method accepts named parameters, but +text+, +x+ and +y+ are
    # mandatory.
    def write_line(text, x = nil, y = nil, mode = :left, color = 0, alpha = 0xff,
                   effect = nil, effect_color = 0, effect_size = 1, effect_alpha = 0xff,
                   z_index = 0)
      if text.is_a? Hash
        x = text[:x]
        y = text[:y]
        mode = text.fetch(:mode, :left)
        color = text.fetch(:color, 0)
        alpha = text.fetch(:alpha, 0xff)
        effect = text.fetch(:effect, nil)
        effect_color = text.fetch(:effect_color, 0)
        effect_size = text.fetch(:effect_size, 1)
        effect_alpha = text.fetch(:effect_alpha, 0xff)
        z_index = text.fetch(:z_index, 0)
        text = text[:text]
      end

      color = (alpha << 24) | color
      rel =
        case mode
        when :left then 0
        when :center then 0.5
        when :right then 1
        else 0
        end
      if effect
        effect_color = (effect_alpha << 24) | effect_color
        if effect == :border
          @font.draw_markup_rel text, x - effect_size, y - effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x, y - effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x + effect_size, y - effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x + effect_size, y, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x + effect_size, y + effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x, y + effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x - effect_size, y + effect_size, z_index, rel, 0, 1, 1, effect_color
          @font.draw_markup_rel text, x - effect_size, y, z_index, rel, 0, 1, 1, effect_color
        elsif effect == :shadow
          @font.draw_markup_rel text, x + effect_size, y + effect_size, z_index, rel, 0, 1, 1, effect_color
        end
      end
      @font.draw_markup_rel text, x, y, z_index, rel, 0, 1, 1, color
    end

    # Draws text, breaking lines when needed and when explicitly caused by the
    # "\n" character.
    #
    # Parameters:
    # [text] The text to be drawn. Line breaks are allowed. You can use the
	#        `<b>` tag for bold, `<i>` for italic and `<c=rrggbb>` for colors.
    # [x] The horizontal reference for drawing the text. Works like in
    #     +write_line+ for the +:left+, +:right+ and +:center+ modes. For the
    #     +:justified+ mode, works the same as for +:left+.
    # [y] The vertical reference for drawing the text. All text will be drawn
    #     from this point down.
    # [width] The maximum width for the lines of text. Line is broken when
    #         this width is exceeded.
    # [mode] The alignment of the text. Valid values are +:left+, +:right+,
    #        +:center+ and +:justified+.
    # [color] The color of the text, in hexadecimal RRGGBB format.
    # [alpha] The opacity of the text. Valid values vary from 0 (fully
    #         transparent) to 255 (fully opaque).
    # [z_index] The z-order to draw the object. Objects with larger z-orders
    #           will be drawn on top of the ones with smaller z-orders.
    def write_breaking(text, x, y, width, mode = :left, color = 0, alpha = 0xff, z_index = 0)
      color = (alpha << 24) | color
      text.split("\n").each do |p|
        if mode == :justified
          y = write_paragraph_justified p, x, y, width, color, z_index
        else
          rel =
            case mode
            when :left then 0
            when :center then 0.5
            when :right then 1
            else 0
            end
          y = write_paragraph p, x, y, width, rel, color, z_index
        end
      end
    end

  private

    def write_paragraph(text, x, y, width, rel, color, z_index)
      line = ''
      line_width = 0
      text.split(' ').each do |word|
        w = @font.markup_width word
        if line_width + w > width
          @font.draw_markup_rel line.chop, x, y, z_index, rel, 0, 1, 1, color
          line = ''
          line_width = 0
          y += @font.height + @line_spacing
        end
        line += "#{word} "
        line_width += @font.markup_width "#{word} "
      end
      @font.draw_markup_rel line.chop, x, y, z_index, rel, 0, 1, 1, color unless line.empty?
      y + @font.height + @line_spacing
    end

    def write_paragraph_justified(text, x, y, width, color, z_index)
      space_width = @font.text_width ' '
      spaces = [[]]
      line_index = 0
      new_x = x
      words = text.split(' ')
      words.each do |word|
        w = @font.markup_width word
        if new_x + w > x + width
          space = x + width - new_x + space_width
          index = 0
          while space > 0
            spaces[line_index][index] += 1
            space -= 1
            index += 1
            index = 0 if index == spaces[line_index].size - 1
          end

          spaces << []
          line_index += 1

          new_x = x
        end
        new_x += @font.markup_width(word) + space_width
        spaces[line_index] << space_width
      end

      index = 0
      spaces.each do |line|
        new_x = x
        line.each do |s|
          @font.draw_markup words[index], new_x, y, z_index, 1, 1, color
          new_x += @font.markup_width(words[index]) + s
          index += 1
        end
        y += @font.height + @line_spacing
      end
      y
    end
  end
end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = FitMessageIdMapper.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

module Fit4Ruby

  # The FIT file maps GlobalFitMessage numbers to local numbers. Due to
  # restrictions in the format, only 16 local messages can be active at any
  # point in the file. If a GlobalFitMessage is needed that is currently not
  # mapped, a new entry is generated and the least recently used message is
  # evicted. The FitMessageIdMapper is the objects that stores those 16 active
  # entries and can map global to local message numbers.
  class FitMessageIdMapper

    # The entry in the mapper.
    class Entry < Struct.new(:global_message, :last_use)
    end

    def initialize
      @entries = Array.new(16, nil)
    end

    # Add a new GlobalFitMessage to the mapper and return the local message
    # number.
    def add_global(message)
      unless (slot = @entries.index { |e| e.nil? })
        # No more free slots. We have to find the least recently used one.
        slot = 0
        0.upto(15) do |i|
          if i != slot && @entries[slot].last_use > @entries[i].last_use
            slot = i
          end
        end
      end
      @entries[slot] = Entry.new(message, Time.now)

      slot
    end

    # Get the local message number for a given GlobalFitMessage. If there is
    # no message number, nil is returned.
    def get_local(message)
      0.upto(15) do |i|
        if (entry = @entries[i]) && entry.global_message == message
          entry.last_use = Time.now
          return i
        end
      end
      nil
    end

  end

end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = Monitoring_B.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'fit4ruby/FitDataRecord'
require 'fit4ruby/FileId'
require 'fit4ruby/DeviceInfo'
require 'fit4ruby/Software'
require 'fit4ruby/MonitoringInfo'
require 'fit4ruby/Monitoring'

module Fit4Ruby

  # The Monitoring_B object is a FIT file class. It's a top-level object that
  # holds all references to other FIT records that are part of the FIT file.
  # Each of the objects it references are direct equivalents of the message
  # record structures used in the FIT file.
  class Monitoring_B < FitDataRecord

    attr_accessor :file_id, :field_descriptions, :device_infos, :software,
      :monitoring_infos, :monitorings

    # Create a new Monitoring_B object.
    # @param field_values [Hash] A Hash that provides initial values for
    #        certain fields of the FitDataRecord.
    def initialize(field_values = {})
      super('activity')
      @num_sessions = 0

      @file_id = FileId.new
      @field_descriptions = []
      @device_infos = []
      @softwares = nil
      @monitoring_infos = []
      @monitorings = []
    end

    # Perform some basic logical checks on the object and all references sub
    # objects. Any errors will be reported via the Log object.
    def check
      last_timestamp = ts_16_offset = nil
      last_ts_16 = nil

      # The timestamp_16 is a 2 byte time stamp value that is used instead of
      # the 4 byte timestamp field for monitoring records that have
      # current_activity_type_intensity values with an activity type of 6. The
      # value seems to be in seconds, but the 0 value reference does not seem
      # to be included in the file. However, it can be approximated using the
      # surrounding timestamp values.
      @monitorings.each do |record|
        if last_ts_16 && ts_16_offset && record.timestamp_16 &&
           record.timestamp_16 < last_ts_16
          # Detect timestamp_16 wrap-arounds. timestamp_16 is a 16 bit value.
          # In case of a wrap-around we adjust the ts_16_offset accordingly.
          ts_16_offset += 2 ** 16
        end
        if ts_16_offset
          # We have already found the offset. Adjust all timestamps according
          # to 'offset + timestamp_16'
          if record.timestamp_16
            record.timestamp = ts_16_offset + record.timestamp_16
            last_ts_16 = record.timestamp_16
          end
        else
          # We are still looking for the offset.
          if record.timestamp_16 && last_timestamp
            # We have a previous timestamp and found the first record with a
            # timestamp_16 value set. We assume that the timestamp of this
            # record is one minute after the previously found timestamp.
            # That's just a guess. Who knows what the Garmin engineers were
            # thinking here?
            ts_16_offset = last_timestamp + 60 - record.timestamp_16
            record.timestamp = ts_16_offset + record.timestamp_16
            last_ts_16 = record.timestamp_16
          else
            # Just save the timestamp of the current record.
            last_timestamp = record.timestamp
          end
        end
      end
    end

    # Create a new FitDataRecord.
    # @param record_type [String] Type that identifies the FitDataRecord
    #        derived class to create.
    # @param field_values [Hash] A Hash that provides initial values for
    #        certain fields of the FitDataRecord.
    # @return FitDataRecord
    def new_fit_data_record(record_type, field_values = {})
      case record_type
      when 'file_id'
        @file_id = (record = FileId.new(field_values))
      when 'software'
        @software = (record = Software.new(field_values))
      when 'device_info'
        @device_infos << (record = DeviceInfo.new(field_values))
      when 'monitoring_info'
        @monitoring_infos << (record = MonitoringInfo.new(field_values))
      when 'monitoring'
        @monitorings << (record = Monitoring.new(field_values))
      else
        record = nil
      end

      record
    end

  end

end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = FieldDescription.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2017, 2018, 2019 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'fit4ruby/FitDataRecord'
require 'fit4ruby/FitDefinitionFieldBase'
require 'fit4ruby/FitTypeDefs'

module Fit4Ruby

  # This class corresponds to the FieldDescription FIT message.
  class FieldDescription < FitDataRecord

    # Create a new FieldDescription object.
    # @param field_values [Hash] Hash that provides initial values for certain
    #        fields.
    def initialize(field_values = {})
      super('field_description')
      set_field_values(field_values)
    end

    def create_global_definition(fit_entity)
      messages = fit_entity.developer_fit_messages
      unless (gfm = GlobalFitMessages[@native_mesg_num])
        Log.error "Developer field description references unknown global " +
          "message number #{@native_mesg_num}"
        return
      end

      if @developer_data_index >=
           fit_entity.top_level_record.developer_data_ids.size
         Log.error "Developer data index #{@developer_data_index} is too large"
         return
      end

      msg = messages[@native_mesg_num] ||
        messages.message(@native_mesg_num, gfm.name)
      unless (@fit_base_type_id & 0x7F) < FIT_TYPE_DEFS.size
        Log.error "fit_base_type_id #{@fit_base_type_id} is too large"
        return
      end
      options = {}
      options[:scale] = @scale if @scale
      options[:offset] = @offset if @offset
      options[:array] = @array if @array
      options[:unit] = @units
      msg.field(@field_definition_number,
                FIT_TYPE_DEFS[@fit_base_type_id & 0x7F][1],
                "_#{@developer_data_index}_#{@field_name}", options)
    end

  end

end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = DeviceInfo.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'fit4ruby/FitDataRecord'

module Fit4Ruby

  class DeviceInfo < FitDataRecord

    def initialize(field_values = {})
      super('device_info')
      set_field_values(field_values)
    end

    # Ensure that FitDataRecords have a deterministic sequence. Device infos
    # are sorted by device_index.
    def <=>(fdr)
      @timestamp == fdr.timestamp ?
        @message.name == fdr.message.name ?
          @device_index <=> fdr.device_index :
          RecordOrder.index(@message.name) <=>
            RecordOrder.index(fdr.message.name) :
        @timestamp <=> fdr.timestamp
    end

    def check(index)
      unless @device_index
        Log.fatal 'device info record must have a device_index'
      end
      if @device_index == 0
        unless @manufacturer
          Log.fatal 'device info record 0 must have a manufacturer field set'
        end
        if @manufacturer == 'garmin'
          unless @garmin_product
            Log.fatal 'device info record 0 must have a garman_product ' +
                      'field set'
          end
        else
          unless @product
            Log.fatal 'device info record 0 must have a product field set'
          end
        end
        if @serial_number.nil?
          Log.fatal 'device info record 0 must have a serial number set'
        end
      end
    end

  end

end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = Log.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'monitor'
require 'logger'
require 'singleton'

module Fit4Ruby

  # This is the Exception type that will be thrown for all unrecoverable
  # errors.
  class Error < StandardError ; end

  # This is the Exception type that will be thrown for all program errors that
  # are caused by user error rather than program logic errors.
  class Abort < StandardError ; end

  # The ILogger class is a singleton that provides a common logging mechanism
  # to all objects. It exposes essentially the same interface as the Logger
  # class, just as a singleton and with some additional methods like 'fatal'
  # and 'cricital'.
  class ILogger < Monitor

    include Singleton

    @@logger = Logger.new($stdout)

    # Redirect all log messages to the given IO.
    # @param io [IO] Output file descriptor
    def open(io)
      begin
        @@logger = Logger.new(io)
      rescue => e
        @@logger = Logger.new($stderr)
        Log.fatal "Cannot open log file: #{e.message}"
      end
    end

    # Pass all calls to unknown methods to the @@logger object.
    def method_missing(method, *args, &block)
      @@logger.send(method, *args, &block)
    end

    # Make it properly introspectable.
    def respond_to?(method, include_private = false)
      @@logger.respond_to?(method)
    end

    # Print an error message via the Logger and raise a Fit4Ruby::Abort.
    # This method should be used to abort the program in case of user errors.
    def abort(msg, &block)
      @@logger.error(msg, &block)
      raise Abort, msg
    end

    # Print an error message via the Logger and raise a Fit4Ruby::Error.
    # This method should be used to abort the program in case of program logic
    # errors.
    def fatal(msg, &block)
      @@logger.error(msg, &block)
      raise Error, msg
    end

  end

  Log = ILogger.instance
  Log.level = Logger::WARN
  Log.formatter = proc do |severity, time, progname, msg|
    msg + "\n"
  end

end


#!/usr/bin/env ruby -w
# encoding: UTF-8
#
# = Metrics.rb -- Fit4Ruby - FIT file processing library for Ruby
#
# Copyright (c) 2018 by Chris Schlaeger <cs@taskjuggler.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#

require 'fit4ruby/FitDataRecord'
require 'fit4ruby/FileId'
require 'fit4ruby/FileCreator'
require 'fit4ruby/DeviceInfo'
require 'fit4ruby/TrainingStatus'

module Fit4Ruby

  # The Metrics object is a FIT file class. It's a top-level object that
  # holds all references to other FIT records that are part of the FIT file.
  # Each of the objects it references are direct equivalents of the message
  # record structures used in the FIT file.
  #
  # This is not part of the officially documented FIT API. Names may change in
  # the future if the real Garmin names get known.
  class Metrics < FitDataRecord

    attr_accessor :field_descriptions, :file_id, :file_creator, :device_infos

    # Create a new Metrics object.
    # @param field_values [Hash] A Hash that provides initial values for
    #        certain fields of the FitDataRecord.
    def initialize(field_values = {})
      super('metrics')

      @field_descriptions = []

      @file_id = FileId.new
      @device_infos = []
      @file_creator = nil
      @training_statuses = []
    end

    # Perform some basic logical checks on the object and all references sub
    # objects. Any errors will be reported via the Log object.
    def check
    end

    # Create a new FitDataRecord.
    # @param record_type [String] Type that identifies the FitDataRecord
    #        derived class to create.
    # @param field_values [Hash] A Hash that provides initial values for
    #        certain fields of the FitDataRecord.
    # @return FitDataRecord
    def new_fit_data_record(record_type, field_values = {})
      case record_type
      when 'file_id'
        @file_id = (record = FileId.new(field_values))
      when 'file_creator'
        @software = (record = FileCreator.new(field_values))
      when 'device_info'
        @device_infos << (record = DeviceInfo.new(field_values))
      when 'training_status'
        @training_statuses << (record = TrainingStatus.new(field_values))
      else
        record = nil
      end

      record
    end

  end

end