#===============================================================================
# "Tile Puzzle" mini-games
# By Maruno
# Graphics by the__end
#-------------------------------------------------------------------------------
# Run with:      pbTilePuzzle(game,board,width,height)
# game = 1 (Ruins of Alph puzzle),
#        2 (Ruins of Alph puzzle plus tile rotations),
#        3 (Mystic Square),
#        4 (swap two adjacent tiles),
#        5 (swap two adjacent tiles plus tile rotations),
#        6 (Rubik's square),
#        7 (rotate selected tile plus adjacent tiles at once).
# board = The name/number of the graphics to be used.
# width,height = Optional, the number of tiles wide/high the puzzle is (0 for
#                the default value of 4).
#===============================================================================
class TilePuzzleCursor < BitmapSprite
  attr_accessor :game
  attr_accessor :position
  attr_accessor :arrows
  attr_accessor :selected
  attr_accessor :holding

  def initialize(game, position, tilewidth, tileheight, boardwidth, boardheight)
    @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
    @viewport.z = 99999
    super(Graphics.width, Graphics.height, @viewport)
    @game = game
    @position = position
    @tilewidth = tilewidth
    @tileheight = tileheight
    @boardwidth = boardwidth
    @boardheight = boardheight
    @arrows = []
    @selected = false
    @holding = false
    @cursorbitmap = AnimatedBitmap.new("Graphics/UI/Tile Puzzle/cursor")
    update
  end

  def update
    self.bitmap.clear
    x = (Graphics.width - (@tilewidth * @boardwidth)) / 2
    if @position >= @boardwidth * @boardheight
      x = ((x - (@tilewidth * (@boardwidth / 2).ceil)) / 2) - 10
      if (@position % @boardwidth) >= (@boardwidth / 2).ceil
        x = Graphics.width - x - (@tilewidth * @boardwidth)
      end
    end
    x += @tilewidth * (@position % @boardwidth)
    y = ((Graphics.height - (@tileheight * @boardheight)) / 2) - 32
    y += @tileheight * ((@position % (@boardwidth * @boardheight)) / @boardwidth)
    self.tone = Tone.new(0, (@holding ? 64 : 0), (@holding ? 64 : 0), 0)
    # Cursor
    if @game != 3
      expand = (@holding) ? 0 : 4
      4.times do |i|
        self.bitmap.blt(
          x + ((i % 2) * (@tilewidth - (@cursorbitmap.width / 4))) + (expand * (((i % 2) * 2) - 1)),
          y + ((i / 2) * (@tileheight - (@cursorbitmap.height / 2))) + (expand * (((i / 2) * 2) - 1)),
          @cursorbitmap.bitmap, Rect.new((i % 2) * @cursorbitmap.width / 4,
                                         (i / 2) * @cursorbitmap.height / 2,
                                         @cursorbitmap.width / 4, @cursorbitmap.height / 2)
        )
      end
    end
    # Arrows
    if @selected || @game == 3
      expand = (@game == 3) ? 0 : 4
      xin = [(@tilewidth - (@cursorbitmap.width / 4)) / 2, -expand,
             @tilewidth - (@cursorbitmap.width / 4) + expand, (@tilewidth - (@cursorbitmap.width / 4)) / 2]
      yin = [@tileheight - (@cursorbitmap.height / 2) + expand, (@tileheight - (@cursorbitmap.height / 2)) / 2,
             (@tileheight - (@cursorbitmap.height / 2)) / 2, -expand]
      4.times do |i|
        next if !@arrows[i]
        self.bitmap.blt(x + xin[i], y + yin[i], @cursorbitmap.bitmap,
                        Rect.new((@cursorbitmap.width / 2) + ((i % 2) * (@cursorbitmap.width / 4)),
                                 (i / 2) * (@cursorbitmap.height / 2),
                                 @cursorbitmap.width / 4, @cursorbitmap.height / 2))
      end
    end
  end
end

#===============================================================================
#
#===============================================================================
class TilePuzzleScene
  def initialize(game, board, width, height)
    @game = game
    @board = board
    @boardwidth = (width > 0) ? width : 4
    @boardheight = (height > 0) ? height : 4
  end

  def update
    xtop = (Graphics.width - (@tilewidth * @boardwidth)) / 2
    ytop = ((Graphics.height - (@tileheight * @boardheight)) / 2) + (@tileheight / 2) - 32
    (@boardwidth * @boardheight).times do |i|
      pos = -1
      @tiles.length.times do |j|
        pos = j if @tiles[j] == i
      end
      @sprites["tile#{i}"].z = 0
      @sprites["tile#{i}"].tone = Tone.new(0, 0, 0, 0)
      if @heldtile == i
        pos = @sprites["cursor"].position
        @sprites["tile#{i}"].z = 1
        @sprites["tile#{i}"].tone = Tone.new(64, 0, 0, 0) if @tiles[pos] >= 0
      end
      thisx = xtop
      if pos >= 0
        if pos >= @boardwidth * @boardheight
          thisx = ((xtop - (@tilewidth * (@boardwidth / 2).ceil)) / 2) - 10
          if (pos % @boardwidth) >= (@boardwidth / 2).ceil
            thisx = Graphics.width - thisx - (@tilewidth * @boardwidth)
          end
        end
        @sprites["tile#{i}"].x = thisx + (@tilewidth * (pos % @boardwidth)) + (@tilewidth / 2)
        @sprites["tile#{i}"].y = ytop + (@tileheight * ((pos % (@boardwidth * @boardheight)) / @boardwidth))
        next if @game == 3
        rotatebitmaps = [@tilebitmap, @tilebitmap1, @tilebitmap2, @tilebitmap3]
        @sprites["tile#{i}"].bitmap.clear
        if rotatebitmaps[@angles[i]]
          @sprites["tile#{i}"].bitmap.blt(0, 0, rotatebitmaps[@angles[i]].bitmap,
                                          Rect.new(@tilewidth * (i % @boardwidth), @tileheight * (i / @boardwidth), @tilewidth, @tileheight))
          @sprites["tile#{i}"].angle = 0
        else
          @sprites["tile#{i}"].bitmap.blt(0, 0, @tilebitmap.bitmap,
                                          Rect.new(@tilewidth * (i % @boardwidth), @tileheight * (i / @boardwidth), @tilewidth, @tileheight))
          @sprites["tile#{i}"].angle = @angles[i] * 90
        end
      end
    end
    updateCursor
    pbUpdateSpriteHash(@sprites)
  end

  def updateCursor
    arrows = []
    4.times do |i|
      arrows.push(pbCanMoveInDir?(@sprites["cursor"].position, (i + 1) * 2, @game == 6))
    end
    @sprites["cursor"].arrows = arrows
  end

  def pbStartScene
    @sprites = {}
    @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
    @viewport.z = 99999
    if pbResolveBitmap("Graphics/UI/Tile Puzzle/bg#{@board}")
      addBackgroundPlane(@sprites, "bg", "Tile Puzzle/bg#{@board}", @viewport)
    else
      addBackgroundPlane(@sprites, "bg", "Tile Puzzle/bg", @viewport)
    end
    @tilebitmap = AnimatedBitmap.new("Graphics/UI/Tile Puzzle/tiles#{@board}")
    @tilebitmap1 = nil
    @tilebitmap2 = nil
    @tilebitmap3 = nil
    if pbResolveBitmap("Graphics/UI/Tile Puzzle/tiles#{@board}_1")
      @tilebitmap1 = AnimatedBitmap.new("Graphics/UI/Tile Puzzle/tiles#{@board}_1")
    end
    if pbResolveBitmap("Graphics/UI/Tile Puzzle/tiles#{@board}_2")
      @tilebitmap2 = AnimatedBitmap.new("Graphics/UI/Tile Puzzle/tiles#{@board}_2")
    end
    if pbResolveBitmap("Graphics/UI/Tile Puzzle/tiles#{@board}_3")
      @tilebitmap3 = AnimatedBitmap.new("Graphics/UI/Tile Puzzle/tiles#{@board}_3")
    end
    @tilewidth = @tilebitmap.width / @boardwidth
    @tileheight = @tilebitmap.height / @boardheight
    (@boardwidth * @boardheight).times do |i|
      @sprites["tile#{i}"] = BitmapSprite.new(@tilewidth, @tileheight, @viewport)
      @sprites["tile#{i}"].ox = @tilewidth / 2
      @sprites["tile#{i}"].oy = @tileheight / 2
      break if @game == 3 && i >= (@boardwidth * @boardheight) - 1
      @sprites["tile#{i}"].bitmap.blt(0, 0, @tilebitmap.bitmap,
                                      Rect.new(@tilewidth * (i % @boardwidth), @tileheight * (i / @boardwidth), @tilewidth, @tileheight))
    end
    @heldtile = -1
    @angles = []
    @tiles = pbShuffleTiles
    @sprites["cursor"] = TilePuzzleCursor.new(@game, pbDefaultCursorPosition,
                                              @tilewidth, @tileheight, @boardwidth, @boardheight)
    update
    pbFadeInAndShow(@sprites)
  end

  def pbShuffleTiles
    ret = []
    (@boardwidth * @boardheight).times do |i|
      ret.push(i)
      @angles.push(0)
    end
    case @game
    when 6
      @tiles = ret
      5.times do
        pbShiftLine([2, 4, 6, 8][rand(4)], rand(@boardwidth * @boardheight), false)
      end
      return @tiles
    when 7
      @tiles = ret
      5.times do
        pbRotateTile(rand(@boardwidth * @boardheight), false)
      end
    else
      ret.shuffle!
      if @game == 3  # Make sure only solvable Mystic Squares are allowed.
        num = 0
        blank = -1
        (ret.length - 1).times do |i|
          blank = i if ret[i] == (@boardwidth * @boardheight) - 1
          (i...ret.length).each do |j|
            num += 1 if ret[j] < ret[i] && ret[i] != (@boardwidth * @boardheight) - 1 &&
                        ret[j] != (@boardwidth * @boardheight) - 1
          end
        end
        if @boardwidth.odd?
          ret = pbShuffleTiles if num.odd?
        elsif num.even? == (@boardheight - (blank / @boardwidth)).even?
          ret = pbShuffleTiles
        end
      end
      if @game == 1 || @game == 2
        ret2 = []
        (@boardwidth * @boardheight).times do |i|
          ret2.push(-1)
        end
        ret = ret2 + ret
      end
      if @game == 2 || @game == 5
        @angles.length.times do |i|
          @angles[i] = rand(4)
        end
      end
    end
    return ret
  end

  def pbDefaultCursorPosition
    if @game == 3
      (@boardwidth * @boardheight).times do |i|
        return i if @tiles[i] == (@boardwidth * @boardheight) - 1
      end
    end
    return 0
  end

  def pbMoveCursor(pos, dir)
    case dir
    when 2
      pos += @boardwidth
    when 4
      if pos >= @boardwidth * @boardheight
        if pos % @boardwidth == (@boardwidth / 2).ceil
          pos = (((pos % (@boardwidth * @boardheight)) / @boardwidth) * @boardwidth) + @boardwidth - 1
        else
          pos -= 1
        end
      elsif (pos % @boardwidth) == 0
        pos = (((pos / @boardwidth) + @boardheight) * @boardwidth) + (@boardwidth / 2).ceil - 1
      else
        pos -= 1
      end
    when 6
      if pos >= @boardwidth * @boardheight
        if pos % @boardwidth == (@boardwidth / 2).ceil - 1
          pos = ((pos % (@boardwidth * @boardheight)) / @boardwidth) * @boardwidth
        else
          pos += 1
        end
      elsif pos % @boardwidth >= @boardwidth - 1
        pos = (((pos / @boardwidth) + @boardheight) * @boardwidth) + (@boardwidth / 2).ceil
      else
        pos += 1
      end
    when 8
      pos -= @boardwidth
    end
    return pos
  end

  def pbCanMoveInDir?(pos, dir, swapping)
    return true if @game == 6 && swapping
    case dir
    when 2
      return false if (pos / @boardwidth) % @boardheight >= @boardheight - 1
    when 4
      if @game == 1 || @game == 2
        return false if pos >= @boardwidth * @boardheight && pos % @boardwidth == 0
      else
        return false if pos % @boardwidth == 0
      end
    when 6
      if @game == 1 || @game == 2
        return false if pos >= @boardwidth * @boardheight && pos % @boardwidth >= @boardwidth - 1
      else
        return false if pos % @boardwidth >= @boardwidth - 1
      end
    when 8
      return false if (pos / @boardwidth) % @boardheight == 0
    end
    return true
  end

  def pbRotateTile(pos, anim = true)
    if @heldtile >= 0
      if anim
        @sprites["cursor"].visible = false
        @sprites["tile#{@heldtile}"].z = 1
        old_angle = @sprites["tile#{@heldtile}"].angle
        timer_start = System.uptime
        loop do
          @sprites["tile#{@heldtile}"].angle = lerp(old_angle, old_angle - 90, 0.25, timer_start, System.uptime)
          pbUpdateSpriteHash(@sprites)
          Graphics.update
          Input.update
          break if @sprites["tile#{@heldtile}"].angle == old_angle - 90
        end
        @sprites["tile#{@heldtile}"].z = 0
        @sprites["cursor"].visible = true if !pbCheckWin
      end
      @angles[@heldtile] -= 1
      @angles[@heldtile] += 4 if @angles[@heldtile] < 0
    else
      return if @tiles[pos] < 0
      group = pbGetNearTiles(pos)
      if anim
        @sprites["cursor"].visible = false
        old_angles = []
        group.each do |i|
          @sprites["tile#{@tiles[i]}"].z = 1
          old_angles.push(@sprites["tile#{@tiles[i]}"].angle)
        end
        timer_start = System.uptime
        loop do
          group.each_with_index do |idx, i|
            @sprites["tile#{@tiles[idx]}"].angle = lerp(old_angles[i], old_angles[i] - 90, 0.25, timer_start, System.uptime)
          end
          pbUpdateSpriteHash(@sprites)
          Graphics.update
          Input.update
          break if @sprites["tile#{@tiles[group[0]]}"].angle == old_angles[0] - 90
        end
        group.each do |i|
          @sprites["tile#{@tiles[i]}"].z = 0
        end
        @sprites["cursor"].visible = true if !pbCheckWin
      end
      group.each do |i|
        tile = @tiles[i]
        @angles[tile] -= 1
        @angles[tile] += 4 if @angles[tile] < 0
      end
    end
  end

  def pbGetNearTiles(pos)
    ret = [pos]
    if @game == 7
      [2, 4, 6, 8].each do |i|
        ret.push(pbMoveCursor(pos, i)) if pbCanMoveInDir?(pos, i, true)
      end
    end
    return ret
  end

  def pbSwapTiles(dir)
    cursor = @sprites["cursor"].position
    return pbShiftLine(dir, cursor) if @game == 6
    movetile = pbMoveCursor(cursor, dir)
    @sprites["cursor"].visible = false
    @sprites["tile#{@tiles[cursor]}"].z = 1
    duration = 0.3
    timer_start = System.uptime
    if [2, 8].include?(dir)   # Swap vertically
      start_sprite_pos = @sprites["tile#{@tiles[movetile]}"].y
      start_cursor_pos = @sprites["tile#{@tiles[cursor]}"].y
      dist = (dir / 4).floor - 1
      loop do
        delta_y = lerp(0, @tileheight * dist, duration, timer_start, System.uptime)
        @sprites["tile#{@tiles[movetile]}"].y = start_sprite_pos + delta_y
        @sprites["tile#{@tiles[cursor]}"].y = start_cursor_pos - delta_y
        pbUpdateSpriteHash(@sprites)
        Graphics.update
        Input.update
        break if @sprites["tile#{@tiles[movetile]}"].y == start_sprite_pos + (@tileheight * dist)
      end
    else   # Swap horizontally
      start_sprite_pos = @sprites["tile#{@tiles[movetile]}"].x
      start_cursor_pos = @sprites["tile#{@tiles[cursor]}"].x
      dist = dir - 5
      loop do
        delta_x = lerp(0, @tilewidth * dist, duration, timer_start, System.uptime)
        @sprites["tile#{@tiles[movetile]}"].x = start_sprite_pos - delta_x
        @sprites["tile#{@tiles[cursor]}"].x = start_cursor_pos + delta_x
        pbUpdateSpriteHash(@sprites)
        Graphics.update
        Input.update
        break if @sprites["tile#{@tiles[movetile]}"].x == start_sprite_pos - (@tilewidth * dist)
      end
    end
    @tiles[cursor], @tiles[movetile] = @tiles[movetile], @tiles[cursor]
    @sprites["tile#{@tiles[cursor]}"].z = 0
    @sprites["cursor"].position = movetile
    @sprites["cursor"].selected = false
    @sprites["cursor"].visible = true if !pbCheckWin
    return true
  end

  def pbShiftLine(dir, cursor, anim = true)
    # Get tiles involved
    tiles = []
    dist = 0
    if [2, 8].include?(dir)
      dist = (dir / 4).floor - 1
      while (dist > 0 && cursor < (@boardwidth - 1) * @boardheight) ||
            (dist < 0 && cursor >= @boardwidth)
        cursor += (@boardwidth * dist)
      end
      @boardheight.times do |i|
        tiles.push(cursor - (i * dist * @boardwidth))
      end
    else
      dist = dir - 5
      while (dist > 0 && cursor % @boardwidth > 0) ||
            (dist < 0 && cursor % @boardwidth < @boardwidth - 1)
        cursor -= dist
      end
      @boardwidth.times do |i|
        tiles.push(cursor + (i * dist))
      end
    end
    # Shift tiles
    fade_duration = 0.4
    if anim
      @sprites["cursor"].visible = false
      timer_start = System.uptime
      loop do
        end_tile = @sprites["tile#{@tiles[tiles[tiles.length - 1]]}"]
        end_tile.opacity = lerp(255, 0, fade_duration, timer_start, System.uptime)
        Graphics.update
        Input.update
        break if end_tile.opacity == 0
      end
      duration = 0.3
      timer_start = System.uptime
      start_pos = []
      if [2, 8].include?(dir)
        tiles.each { |i| start_pos.push(@sprites["tile#{@tiles[i]}"].y) }
        loop do
          tiles.each_with_index do |idx, i|
            @sprites["tile#{@tiles[idx]}"].y = lerp(start_pos[i], start_pos[i] - (@tileheight * dist), duration, timer_start, System.uptime)
          end
          pbUpdateSpriteHash(@sprites)
          Graphics.update
          Input.update
          break if @sprites["tile#{@tiles[tiles[0]]}"].y == start_pos[0] - (@tileheight * dist)
        end
      else
        tiles.each { |i| start_pos.push(@sprites["tile#{@tiles[i]}"].x) }
        loop do
          tiles.each_with_index do |idx, i|
            @sprites["tile#{@tiles[idx]}"].x = lerp(start_pos[i], start_pos[i] + (@tilewidth * dist), duration, timer_start, System.uptime)
          end
          pbUpdateSpriteHash(@sprites)
          Graphics.update
          Input.update
          break if @sprites["tile#{@tiles[tiles[0]]}"].x == start_pos[0] + (@tilewidth * dist)
        end
      end
    end
    temp = []
    tiles.each do |i|
      temp.push(@tiles[i])
    end
    temp.length.times do |i|
      @tiles[tiles[(i + 1) % (temp.length)]] = temp[i]
    end
    if anim
      update
      timer_start = System.uptime
      loop do
        end_tile = @sprites["tile#{@tiles[tiles[0]]}"]
        end_tile.opacity = lerp(0, 255, fade_duration, timer_start, System.uptime)
        Graphics.update
        Input.update
        break if end_tile.opacity == 255
      end
      @sprites["cursor"].selected = false
      @sprites["cursor"].visible = true if !pbCheckWin
    end
    return true
  end

  def pbGrabTile(pos)
    @heldtile, @tiles[pos] = @tiles[pos], @heldtile
    @sprites["cursor"].holding = (@heldtile >= 0)
    @sprites["cursor"].visible = false if pbCheckWin
  end

  def pbCheckWin
    (@boardwidth * @boardheight).times do |i|
      return false if @tiles[i] != i
      return false if @angles[i] != 0
    end
    return true
  end

  def pbMain
    loop do
      update
      Graphics.update
      Input.update
      # Check end conditions
      if pbCheckWin
        @sprites["cursor"].visible = false
        if @game == 3
          extratile = @sprites["tile#{(@boardwidth * @boardheight) - 1}"]
          extratile.bitmap.clear
          extratile.bitmap.blt(0, 0, @tilebitmap.bitmap,
                               Rect.new(@tilewidth * (@boardwidth - 1), @tileheight * (@boardheight - 1),
                                        @tilewidth, @tileheight))
          extratile.opacity = 0
          timer_start = System.uptime
          loop do
            extratile.opacity = lerp(0, 255, 0.8, timer_start, System.uptime)
            Graphics.update
            Input.update
            break if extratile.opacity >= 255
          end
        else
          pbWait(0.5)
        end
        loop do
          Graphics.update
          Input.update
          break if Input.trigger?(Input::USE) || Input.trigger?(Input::BACK)
        end
        return true
      end
      # Input
      @sprites["cursor"].selected = (Input.press?(Input::USE) && @game >= 3 && @game <= 6)
      dir = 0
      dir = 2 if Input.trigger?(Input::DOWN) || Input.repeat?(Input::DOWN)
      dir = 4 if Input.trigger?(Input::LEFT) || Input.repeat?(Input::LEFT)
      dir = 6 if Input.trigger?(Input::RIGHT) || Input.repeat?(Input::RIGHT)
      dir = 8 if Input.trigger?(Input::UP) || Input.repeat?(Input::UP)
      if dir > 0
        if @game == 3 || (@game != 3 && @sprites["cursor"].selected)
          if pbCanMoveInDir?(@sprites["cursor"].position, dir, true)
            pbSEPlay("Tile Game cursor")
            pbSwapTiles(dir)
          end
        else
          if pbCanMoveInDir?(@sprites["cursor"].position, dir, false)
            pbSEPlay("Tile Game cursor")
            @sprites["cursor"].position = pbMoveCursor(@sprites["cursor"].position, dir)
          end
        end
      elsif (@game == 1 || @game == 2) && Input.trigger?(Input::USE)
        pbGrabTile(@sprites["cursor"].position)
      elsif (@game == 2 && Input.trigger?(Input::ACTION)) ||
            (@game == 5 && Input.trigger?(Input::ACTION)) ||
            (@game == 7 && Input.trigger?(Input::USE))
        pbRotateTile(@sprites["cursor"].position)
      elsif Input.trigger?(Input::BACK)
        return false
      end
    end
  end

  def pbEndScene
    pbFadeOutAndHide(@sprites)
    pbDisposeSpriteHash(@sprites)
    @viewport.dispose
  end
end

#===============================================================================
#
#===============================================================================
class TilePuzzle
  def initialize(scene)
    @scene = scene
  end

  def pbStartScreen
    @scene.pbStartScene
    ret = @scene.pbMain
    @scene.pbEndScene
    return ret
  end
end

#===============================================================================
#
#===============================================================================
def pbTilePuzzle(game, board, width = 0, height = 0)
  ret = false
  pbFadeOutIn do
    scene = TilePuzzleScene.new(game, board, width, height)
    screen = TilePuzzle.new(scene)
    ret = screen.pbStartScreen
  end
  return ret
end
