#===============================================================================
#
#===============================================================================
class Player < Trainer
  attr_accessor :has_snag_machine
  attr_accessor :seen_purify_chamber

  alias __shadowPkmn__initialize initialize unless private_method_defined?(:__shadowPkmn__initialize)
  def initialize(name, trainer_type)
    __shadowPkmn__initialize(name, trainer_type)
    @has_snag_machine    = false
    @seen_purify_chamber = false
  end
end

class PokemonGlobalMetadata
  attr_writer :purifyChamber

  def purifyChamber
    @purifyChamber = PurifyChamber.new if !@purifyChamber
    return @purifyChamber
  end
end

#===============================================================================
# General purpose utilities
#===============================================================================
def pbDrawGauge(bitmap, rect, color, value, maxValue)
  return if !bitmap
  bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, Color.black)
  width = (maxValue <= 0) ? 0 : (rect.width - 4) * value / maxValue
  if rect.width >= 4 && rect.height >= 4
    bitmap.fill_rect(rect.x + 2, rect.y + 2, rect.width - 4, rect.height - 4, Color.new(248, 248, 248))
    bitmap.fill_rect(rect.x + 2, rect.y + 2, width, rect.height - 4, color)
  end
end

# angle is in degrees.
def calcPoint(x, y, distance, angle)
  angle -= (angle / 360.0).floor * 360   # normalize
  angle = (angle / 360.0) * (2 * Math::PI)   # convert to radians
  angle = -angle % (2 * Math::PI)   # normalize radians
  point = [distance * Math.cos(angle), distance * Math.sin(angle)]
  point[0] += x
  point[1] += y
  return point
end

#===============================================================================
#
#===============================================================================
class PurifyChamberSet
  attr_reader :shadow   # The Shadow Pokémon in the middle
  attr_reader :facing   # Index in list of Pokémon the Shadow Pokémon is facing

  def partialSum(x)
    return ((x * x) + x) / 2   # pattern: 1, 3, 6, 10, 15, 21, 28, ...
  end

  def length
    return @list.length
  end

  def initialize
    @list = []
    @facing = 0
  end

  def facing=(value)
    @facing = value if value >= 0 && value < @list.length
  end

  def shadow=(value)
    @shadow = value if value.nil? || value.shadowPokemon?
  end

  # Main component is tempo
  # Boosted if center has advantage over facing Pokemon
  # Boosted based on number of best circles
  def flow
    ret = 0
    return 0 if !@shadow
    @list.length.times do |i|
      ret += (PurifyChamberSet.isSuperEffective(@list[i], @list[(i + 1) % @list.length])) ? 1 : 0
    end
    if @list[@facing]
      ret += PurifyChamberSet.isSuperEffective(@shadow, @list[@facing]) ? 1 : 0
    end
    return ret + (@list.length / 2)
  end

  def shadowAffinity
    return 0 if @facing < 0 || @facing >= @list.length || !@shadow
    return (PurifyChamberSet.isSuperEffective(@shadow, @list[@facing])) ? 2 : 1
  end

  def affinity(i)
    return 0 if i < 0 || i >= @list.length
    return (PurifyChamberSet.isSuperEffective(@list[i], @list[(i + 1) % @list.length])) ? 2 : 1
  end

  # Tempo refers to the type advantages of each Pokemon in a certain set in a
  # clockwise direction. Tempo also depends on the number of Pokemon in the set
  def tempo
    ret = 0
    @list.length.times do |i|
      ret += (PurifyChamberSet.isSuperEffective(@list[i], @list[(i + 1) % @list.length])) ? 1 : 0
    end
    return partialSum(@list.length) + ret
  end

  def list
    return @list.clone
  end

  def [](index)
    return @list[index]
  end

  def insertAfter(index, value)
    return if self.length >= PurifyChamber::SETSIZE
    return if index < 0 || index >= PurifyChamber::SETSIZE
    unless value&.shadowPokemon?
      @list.insert(index + 1, value)
      @list.compact!
      @facing += 1 if @facing > index && value
      @facing = [[@facing, @list.length - 1].min, 0].max
    end
  end

  def insertAt(index, value)
    return if index < 0 || index >= PurifyChamber::SETSIZE
    unless value&.shadowPokemon?
      @list[index] = value
      @list.compact!
      @facing = [[@facing, @list.length - 1].min, 0].max
    end
  end

  # Purify Chamber treats Normal/Normal matchup as super effective
  def self.typeAdvantage(type1, type2)
    return true if type1 == :NORMAL && type2 == :NORMAL
    return Effectiveness.super_effective_type?(type1, type2)
  end

  def self.isSuperEffective(pkmn1, pkmn2)
    pkmn1.types.each do |type1|
      pkmn2.types.each do |type2|
        return true if typeAdvantage(type1, type2)
      end
    end
    return false
  end
end

#===============================================================================
#
#===============================================================================
class PurifyChamber
  attr_reader :sets
  attr_reader :currentSet

  NUMSETS = 9
  SETSIZE = 4

  # Calculates the maximum possible tempo.
  def self.maximumTempo
    x = SETSIZE + 1
    return (((x * x) + x) / 2) - 1
  end

  def initialize
    @sets = []
    @currentSet = 0
    NUMSETS.times { |i| @sets[i] = PurifyChamberSet.new }
  end

  def currentSet=(value)
    @currentSet = value if value >= 0 && value < NUMSETS
  end

  # Number of regular Pokemon in a set
  def setCount(set)
    return @sets[set].length
  end

  def setList(set)
    return [] if set < 0 || set >= NUMSETS
    return @sets[set].list
  end

  # For speeding up purification.
  def chamberFlow(chamber)
    return 0 if chamber < 0 || chamber >= NUMSETS
    return @sets[chamber].flow
  end

  def getShadow(chamber)
    return nil if chamber < 0 || chamber >= NUMSETS
    return @sets[chamber].shadow
  end

  # Allow only Shadow Pokemon.
  def setShadow(chamber, value)
    return if chamber < 0 || chamber >= NUMSETS
    @sets[chamber].shadow = value
  end

  def switch(set1, set2)
    return if set1 < 0 || set1 >= NUMSETS
    return if set2 < 0 || set2 >= NUMSETS
    s = @sets[set1]
    @sets[set1] = @sets[set2]
    @sets[set2] = s
  end

  def insertAfter(set, index, value)
    return if set < 0 || set >= NUMSETS
    @sets[set].insertAfter(index, value)
  end

  def insertAt(set, index, value)
    return if set < 0 || set >= NUMSETS
    @sets[set].insertAt(index, value)
  end

  def [](chamber, slot = nil)
    return @sets[chamber] if slot.nil?
    return nil if chamber < 0 || chamber >= NUMSETS
    return nil if slot < 0 || slot >= SETSIZE
    return @sets[chamber][slot]
  end

  def isPurifiableIgnoreRegular?(set)
    shadow = getShadow(set)
    return false if !shadow
    return false if shadow.heart_gauge != 0
    # Define an exception for Lugia
    if shadow.isSpecies?(:LUGIA)
      maxtempo = PurifyChamber.maximumTempo
      NUMSETS.times do |i|
        return false if @sets[i].tempo != maxtempo
      end
    end
    return true
  end

  def isPurifiable?(set)
    isPurifiableIgnoreRegular?(set) && setCount(set) > 0
  end

  # Called upon each step taken in the overworld
  def update
    NUMSETS.times do |set|
      next if !@sets[set].shadow || @sets[set].shadow.heart_gauge <= 0
      # If a Shadow Pokemon and a regular Pokemon are on the same set
      flow = self.chamberFlow(set)
      @sets[set].shadow.adjustHeart(-flow)
      next if !isPurifiable?(set)
      pbMessage(_INTL("Your {1} in the Purify Chamber is ready for purification!",
                      @sets[set].shadow.name))
    end
  end

  def debugAddShadow(set, species)
    pkmn = Pokemon.new(species, 1)
    pkmn.makeShadow
    setShadow(set, pkmn)
  end

  def debugAddNormal(set, species)
    pkmn = Pokemon.new(species, 1)
    insertAfter(set, setCount(set), pkmn)
  end

  def debugAdd(set, shadow, type1, type2 = nil)
    pkmn = PseudoPokemon.new(shadow, type1, type2 || type1)
    if pkmn.shadowPokemon?
      self.setShadow(set, pkmn)
    else
      self.insertAfter(set, setCount(set), pkmn)
    end
  end
end

#===============================================================================
#
#===============================================================================
module PurifyChamberHelper
  def self.pbGetPokemon2(chamber, set, position)
    if position == 0
      return chamber.getShadow(set)
    elsif position > 0
      position -= 1
      if position.even?
        return chamber[set, position / 2]
      else # In between two indices
        return nil
      end
    end
    return nil
  end

  def self.pbGetPokemon(chamber, position)
    if position == 0
      return chamber.getShadow(chamber.currentSet)
    elsif position > 0
      position -= 1
      if position.even?
        return chamber[chamber.currentSet, position / 2]
      else # In between two indices
        return nil
      end
    end
    return nil
  end

  def self.adjustOnInsert(position)
    if position > 0
      position -= 1
      oldpos = position / 2
      if position.even?
        return position + 1
      else
        return ((oldpos + 1) * 2) + 1
      end
    end
    return position
  end

  def self.pbSetPokemon(chamber, position, value)
    if position == 0
      chamber.setShadow(chamber.currentSet, value)
    elsif position > 0
      position -= 1
      if position.even?
        chamber.insertAt(chamber.currentSet, position / 2, value)
      else # In between two indices
        chamber.insertAfter(chamber.currentSet, position / 2, value)
      end
    end
  end
end

#===============================================================================
#
#===============================================================================
class PurifyChamberScreen
  def initialize(scene)
    @scene = scene
    @chamber = $PokemonGlobal.purifyChamber
#    for j in 0...PurifyChamber::NUMSETS
#      @chamber.debugAddShadow(j,rand(100)+1)
#      @chamber[j].shadow.heart_gauge = 0
#      for i in 0...PurifyChamber::SETSIZE
#        @chamber.debugAddNormal(j,rand(100)+1)
#      end
#    end
  end

  def pbPlace(pkmn, position)
    return false if !pkmn
    if pkmn.egg?
      @scene.pbDisplay(_INTL("Can't place an egg there."))
      return false
    end
    if position == 0
      if pkmn.shadowPokemon?
        # Remove from storage and place in set
        oldpkmn = PurifyChamberHelper.pbGetPokemon(@chamber, position)
        if oldpkmn
          @scene.pbShift(position, pkmn)
        else
          @scene.pbPlace(position, pkmn)
        end
        PurifyChamberHelper.pbSetPokemon(@chamber, position, pkmn)
        @scene.pbRefresh
      else
        @scene.pbDisplay(_INTL("Only a Shadow Pokémon can go there."))
        return false
      end
    elsif position >= 1
      if pkmn.shadowPokemon?
        @scene.pbDisplay(_INTL("Can't place a Shadow Pokémon there."))
        return false
      else
        oldpkmn = PurifyChamberHelper.pbGetPokemon(@chamber, position)
        if oldpkmn
          @scene.pbShift(position, pkmn)
        else
          @scene.pbPlace(position, pkmn)
        end
        PurifyChamberHelper.pbSetPokemon(@chamber, position, pkmn)
        @scene.pbRefresh
      end
    end
    return true
  end

  def pbPlacePokemon(pos, position)
    return false if !pos
    pkmn = $PokemonStorage[pos[0], pos[1]]
    if pbPlace(pkmn, position)
      $PokemonStorage.pbDelete(pos[0], pos[1])
      return true
    end
    return false
  end

  def pbOnPlace(pkmn)
    set = @chamber.currentSet
    if @chamber.setCount(set) == 0 && @chamber.isPurifiableIgnoreRegular?(set)
      pkmn = @chamber.getShadow(set)
      @scene.pbDisplay(
        _INTL("This {1} is ready to open its heart. However, there must be at least one regular Pokémon in the set to perform a purification ceremony.",
              pkmn.name)
      )
    end
  end

  def pbOpenSetDetail
    chamber = @chamber
    @scene.pbOpenSetDetail(chamber.currentSet)
    heldpkmn = nil
    loop do
      # Commands
      # array[0]==0 - a position was chosen
      # array[0]==1 - a new set was chosen
      # array[0]==2 - choose Pokemon command
      cmd = @scene.pbSetScreen
      case cmd[0]
      when 0   # Place Pokemon in the set
        curpkmn = PurifyChamberHelper.pbGetPokemon(@chamber, cmd[1])
        if curpkmn || heldpkmn
          commands = [_INTL("MOVE"), _INTL("SUMMARY"), _INTL("WITHDRAW")]
          if curpkmn && heldpkmn
            commands[0] = _INTL("EXCHANGE")
          elsif heldpkmn
            commands[0] = _INTL("PLACE")
          end
          cmdReplace = -1
          cmdRotate = -1
          if !heldpkmn && curpkmn && cmd[1] == 0 &&
             @chamber[@chamber.currentSet].length > 0
            commands[cmdRotate = commands.length] = _INTL("ROTATE")
          end
          if !heldpkmn && curpkmn
            commands[cmdReplace = commands.length] = _INTL("REPLACE")
          end
          commands.push(_INTL("CANCEL"))
          choice = @scene.pbShowCommands(
            _INTL("What shall I do with this {1}?", heldpkmn ? heldpkmn.name : curpkmn.name),
            commands
          )
          if choice == 0
            if heldpkmn
              if pbPlace(heldpkmn, cmd[1]) # calls place or shift as appropriate
                if curpkmn
                  heldpkmn = curpkmn # Pokemon was shifted
                else
                  pbOnPlace(heldpkmn)
                  @scene.pbPositionHint(PurifyChamberHelper.adjustOnInsert(cmd[1]))
                  heldpkmn = nil # Pokemon was placed
                end
              end
            else
              @scene.pbMove(cmd[1])
              PurifyChamberHelper.pbSetPokemon(@chamber, cmd[1], nil)
              @scene.pbRefresh
              heldpkmn = curpkmn
            end
          elsif choice == 1
            @scene.pbSummary(cmd[1], heldpkmn)
          elsif choice == 2
            if pbBoxesFull?
              @scene.pbDisplay(_INTL("All boxes are full."))
            elsif heldpkmn
              @scene.pbWithdraw(cmd[1], heldpkmn)
              $PokemonStorage.pbStoreCaught(heldpkmn)
              heldpkmn = nil
              @scene.pbRefresh
            else
              # Store and delete Pokemon.
              @scene.pbWithdraw(cmd[1], heldpkmn)
              $PokemonStorage.pbStoreCaught(curpkmn)
              PurifyChamberHelper.pbSetPokemon(@chamber, cmd[1], nil)
              @scene.pbRefresh
            end
          elsif cmdRotate >= 0 && choice == cmdRotate
            count = @chamber[@chamber.currentSet].length
            nextPos = @chamber[@chamber.currentSet].facing
            if count > 0
              @scene.pbRotate((nextPos + 1) % count)
              @chamber[@chamber.currentSet].facing = (nextPos + 1) % count
              @scene.pbRefresh
            end
          elsif cmdReplace >= 0 && choice == cmdReplace
            pos = @scene.pbChoosePokemon
            if pos
              newpkmn = $PokemonStorage[pos[0], pos[1]]
              if newpkmn
                if (newpkmn.shadowPokemon?) == (curpkmn.shadowPokemon?)
                  @scene.pbReplace(cmd, pos)
                  PurifyChamberHelper.pbSetPokemon(@chamber, cmd[1], newpkmn)
                  $PokemonStorage[pos[0], pos[1]] = curpkmn
                  @scene.pbRefresh
                  pbOnPlace(curpkmn)
                else
                  @scene.pbDisplay(_INTL("That Pokémon can't be placed there."))
                end
              end
            end
          end
        else   # No current Pokemon
          pos = @scene.pbChoosePokemon
          if pbPlacePokemon(pos, cmd[1])
            curpkmn = PurifyChamberHelper.pbGetPokemon(@chamber, cmd[1])
            pbOnPlace(curpkmn)
            @scene.pbPositionHint(PurifyChamberHelper.adjustOnInsert(cmd[1]))
          end
        end
      when 1   # Change the active set
        @scene.pbChangeSet(cmd[1])
        chamber.currentSet = cmd[1]
      when 2   # Choose a Pokemon
        pos = @scene.pbChoosePokemon
        pkmn = pos ? $PokemonStorage[pos[0], pos[1]] : nil
        heldpkmn = pkmn if pkmn
      else   # cancel
        if heldpkmn
          @scene.pbDisplay(_INTL("You're holding a Pokémon!"))
        else
          break if !@scene.pbConfirm(_INTL("Continue editing sets?"))
        end
      end
    end
    if pbCheckPurify
      @scene.pbDisplay(_INTL("There is a Pokémon that is ready to open its heart!") + "\1")
      @scene.pbCloseSetDetail
      pbDoPurify
      return false
    else
      @scene.pbCloseSetDetail
      return true
    end
  end

  def pbDisplay(msg)
    @scene.pbDisplay(msg)
  end

  def pbConfirm(msg)
    @scene.pbConfirm(msg)
  end

  def pbRefresh
    @scene.pbRefresh
  end

  def pbCheckPurify
    purifiables = []
    PurifyChamber::NUMSETS.times do |set|
      if @chamber.isPurifiable?(set) # if ready for purification
        purifiables.push(set)
      end
    end
    return purifiables.length > 0
  end

  def pbDoPurify
    purifiables = []
    PurifyChamber::NUMSETS.times do |set|
      if @chamber.isPurifiable?(set) # if ready for purification
        purifiables.push(set)
      end
    end
    purifiables.length.times do |i|
      set = purifiables[i]
      @chamber.currentSet = set
      @scene.pbOpenSet(set)
      @scene.pbPurify
      pbPurify(@chamber[set].shadow, self)
      pbStorePokemon(@chamber[set].shadow)
      @chamber.setShadow(set, nil) # Remove shadow Pokemon from set
      if (i + 1) != purifiables.length
        @scene.pbDisplay(_INTL("There is another Pokémon that is ready to open its heart!") + "\1")
        if !@scene.pbConfirm(_INTL("Would you like to switch sets?"))
          @scene.pbCloseSet
          break
        end
      end
      @scene.pbCloseSet
    end
  end

  def pbStartPurify
    chamber = @chamber
    @scene.pbStart(chamber)
    if pbCheckPurify
      pbDoPurify
      @scene.pbEnd
      return
    end
    @scene.pbOpenSet(chamber.currentSet)
    loop do
      set = @scene.pbChooseSet
      if set < 0
        break if !@scene.pbConfirm(_INTL("Continue viewing holograms?"))
      else
        chamber.currentSet = set
        cmd = @scene.pbShowCommands(_INTL("What do you want to do?"),
                                    [_INTL("EDIT"), _INTL("SWITCH"), _INTL("CANCEL")])
        case cmd
        when 0   # edit
          break if !pbOpenSetDetail
        when 1   # switch
          chamber.currentSet = set
          newSet = @scene.pbSwitch(set)
          chamber.switch(set, newSet)
          chamber.currentSet = newSet
          @scene.pbRefresh
        end
      end
    end
    @scene.pbCloseSet
    @scene.pbEnd
  end
end

#===============================================================================
#
#===============================================================================
class Window_PurifyChamberSets < Window_DrawableCommand
  attr_reader :switching

  def initialize(chamber, x, y, width, height, viewport = nil)
    @chamber = chamber
    @switching = -1
    super(x, y, width, height, viewport)
  end

  def itemCount
    return PurifyChamber::NUMSETS
  end

  def switching=(value)
    @switching = value
    refresh
  end

  def drawItem(index, _count, rect)
    textpos = []
    rect = drawCursor(index, rect)
    if index == @switching
      textpos.push([(index + 1).to_s, rect.x, rect.y, :left, Color.new(248, 0, 0), self.shadowColor])
    else
      textpos.push([(index + 1).to_s, rect.x, rect.y, :left, self.baseColor, self.shadowColor])
    end
    if @chamber.setCount(index) > 0
      pbDrawGauge(self.contents, Rect.new(rect.x + 16, rect.y + 6, 48, 8),
                  Color.new(0, 0, 256), @chamber[index].tempo, PurifyChamber.maximumTempo)
    end
    if @chamber.getShadow(index)
      pbDrawGauge(self.contents, Rect.new(rect.x + 16, rect.y + 18, 48, 8),
                  Color.new(192, 0, 256),
                  @chamber.getShadow(index).heart_gauge,
                  @chamber.getShadow(index).max_gauge_size)
    end
    pbDrawTextPositions(self.contents, textpos)
  end
end

#===============================================================================
#
#===============================================================================
class DirectFlowDiagram
  # Distance travelled by a dot in 1 second.
  DOT_SPEED = 80

  def initialize(viewport = nil)
    @points = []
    @angles = []
    @viewport = viewport
    @strength = 0
    @offset = 0
    @x = 306
    @y = 158
    @distance = 96
  end

  def dispose
    @points.each { |point| point.dispose }
  end

  # 0=none, 1=weak, 2=strong
  def setFlowStrength(strength)
    @strength = strength
  end

  def visible=(value)
    @points.each { |point| point.visible = value }
  end

  def color=(value)
    @points.each { |point| point.color = value }
  end

  def setAngle(angle1)
    @angle = angle1 - ((angle1 / 360).floor * 360)
  end

  def ensurePoint(j)
    if !@points[j] || @points[j].disposed?
      @points[j] = BitmapSprite.new(8, 8, @viewport)
      @points[j].bitmap.fill_rect(0, 0, 8, 8, Color.black)
    end
    @points[j].tone = (@strength == 2) ? Tone.new(232, 232, 248) : Tone.new(16, 16, 232)
    @points[j].visible = (@strength != 0)
  end

  def update
    @points.each do |point|
      point.update
      point.visible = false
    end
    j = 0
    i = 0
    while i < @distance
      o = (i + @offset) % @distance
      if o >= 0 && o < @distance
        ensurePoint(j)
        pt = calcPoint(@x, @y, o, @angle)
        @points[j].x = pt[0]
        @points[j].y = pt[1]
        j += 1
      end
      i += (@strength == 2) ? 16 : 32
    end
    offset_delta = System.uptime * DOT_SPEED
    offset_delta *= 1.5 if @strength == 2
    @offset = offset_delta % @distance
  end
end

#===============================================================================
#
#===============================================================================
class FlowDiagram
  # Distance travelled by a dot in 1 second.
  DOT_SPEED = 80

  def initialize(viewport = nil)
    @points = []
    @angles = []
    @viewport = viewport
    @strength = 0
    @offset = 0
    @x = 306
    @y = 158
    @distance = 96
  end

  def dispose
    @points.each { |point| point.dispose }
  end

  def visible=(value)
    @points.each { |point| point.visible = value }
  end

  def color=(value)
    @points.each { |point| point.color = value }
  end

  # 0=none, 1=weak, 2=strong
  def setFlowStrength(strength)
    @strength = strength
  end

  def ensurePoint(j)
    if !@points[j] || @points[j].disposed?
      @points[j] = BitmapSprite.new(8, 8, @viewport)
      @points[j].bitmap.fill_rect(0, 0, 8, 8, Color.black)
    end
    @points[j].tone = (@strength == 2) ? Tone.new(232, 232, 248) : Tone.new(16, 16, 232)
    @points[j].visible = (@strength != 0)
  end

  def setRange(angle1, angle2)
    @startAngle = angle1 - ((angle1 / 360).floor * 360)
    @endAngle = angle2 - ((angle2 / 360).floor * 360)
    if @startAngle == @endAngle && angle1 != angle2
      @startAngle = 0
      @endAngle = 359.99
    end
  end

  def withinRange(angle, startAngle, endAngle)
    if startAngle > endAngle
      return (angle >= startAngle || angle <= endAngle) &&
             (angle >= 0 && angle <= 360)
    else
      return (angle >= startAngle && angle <= endAngle)
    end
  end

  def update
    @points.each do |point|
      point.update
      point.visible = false
    end
    j = 0
    i = 0
    while i < 360
      angle = (i + @offset) % 360
      if withinRange(angle, @startAngle, @endAngle)
        ensurePoint(j)
        pt = calcPoint(@x, @y, @distance, angle)
        @points[j].x = pt[0]
        @points[j].y = pt[1]
        j += 1
      end
      i += (@strength == 2) ? 10 : 20
    end
    offset_delta = -System.uptime * DOT_SPEED
    offset_delta *= 1.5 if @strength == 2
    @offset = offset_delta % (360 * 6)
  end
end

#===============================================================================
#
#===============================================================================
class PurifyChamberSetView < Sprite
  attr_reader :set
  attr_reader :cursor
  attr_reader :heldpkmn

  def initialize(chamber, set, viewport = nil)
    super(viewport)
    @set = set
    @heldpkmn = nil
    @cursor = -1
    @view = BitmapSprite.new(64, 64, viewport)
    @view.bitmap.fill_rect(8, 8, 48, 48, Color.white)
    @view.bitmap.fill_rect(10, 10, 44, 44, Color.new(255, 255, 255, 128))
    @info = BitmapSprite.new(Graphics.width - 112, 48, viewport)
    @flows = []
    @directflow = DirectFlowDiagram.new(viewport)
    @directflow.setAngle(0)
    @directflow.setFlowStrength(1)
    @__sprites = []
    @__sprites[0] = PokemonIconSprite.new(nil, viewport)
    @__sprites[0].setOffset
    (PurifyChamber::SETSIZE * 2).times do |i|
      @__sprites[i + 1] = PokemonIconSprite.new(nil, viewport)
      @__sprites[i + 1].setOffset
    end
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)] = PokemonIconSprite.new(nil, viewport)
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].setOffset
    @chamber = chamber
    refresh
  end

  def refreshFlows
    @flows.each do |flow|
      flow.setFlowStrength(0)
    end
    setcount = @chamber.setCount(@set)
    setcount.times do |i|
      @flows[i] = FlowDiagram.new(self.viewport) if !@flows[i]
      angle = 360 - (i * 360 / setcount)
      angle += 90   # start at 12 not 3 o'clock
      endAngle = angle - (360 / setcount)
      @flows[i].setRange(endAngle, angle)
      @flows[i].setFlowStrength(@chamber[@set].affinity(i))
    end
  end

  def moveCursor(button)
    points = [@chamber.setCount(@set) * 2, 1].max
    oldcursor = @cursor
    if @cursor == 0 && points > 0
      @cursor = 1 if button == Input::UP
      @cursor = (points * 1 / 4) + 1 if button == Input::RIGHT
      @cursor = (points * 2 / 4) + 1 if button == Input::DOWN
      @cursor = (points * 3 / 4) + 1 if button == Input::LEFT
    elsif @cursor > 0
      pos = @cursor - 1
      if @chamber.setCount(@set) == PurifyChamber::SETSIZE
        points = [points / 2, 1].max
        pos /= 2
      end
      seg = pos * 8 / points
      case seg
      when 7, 0
        pos -= 1 if button == Input::LEFT
        pos += 1 if button == Input::RIGHT
        pos = nil if button == Input::DOWN
      when 1, 2
        pos -= 1 if button == Input::UP
        pos += 1 if button == Input::DOWN
        pos = nil if button == Input::LEFT
      when 3, 4
        pos -= 1 if button == Input::RIGHT
        pos += 1 if button == Input::LEFT
        pos = nil if button == Input::UP
      when 5, 6
        pos -= 1 if button == Input::DOWN
        pos += 1 if button == Input::UP
        pos = nil if button == Input::RIGHT
      end
      if pos.nil?
        @cursor = 0
      else
        pos -= (pos / points).floor * points   # modulus
        pos *= 2 if @chamber.setCount(@set) == PurifyChamber::SETSIZE
        @cursor = pos + 1
      end
    end
    refresh if @cursor != oldcursor
  end

  def checkCursor(index)
    if @cursor == index
      @view.x = @__sprites[index].x - (@view.bitmap.width / 2)
      @view.y = @__sprites[index].y - (@view.bitmap.height / 2)
      @view.visible = true
    end
  end

  def refresh
    pkmn = @chamber.getShadow(@set)
    @view.visible = false
    @info.bitmap.fill_rect(0, 0, @info.bitmap.width, @info.bitmap.height, Color.new(0, 248, 0))
    pbSetSmallFont(@info.bitmap)
    textpos = []
    if pkmn
      type_string = ""
      pkmn.types.each_with_index do |type, i|
        type_string += "/" if i > 0
        type_string += GameData::Type.get(type).name
      end
      textpos.push([_INTL("{1}  Lv.{2}  {3}", pkmn.name, pkmn.level, type_string),
                    2, 6, :left, Color.new(248, 248, 248), Color.new(128, 128, 128)])
      textpos.push([_INTL("FLOW"), 2 + (@info.bitmap.width / 2), 30, :left,
                    Color.new(248, 248, 248), Color.new(128, 128, 128)])
      # draw heart gauge
      pbDrawGauge(@info.bitmap, Rect.new(@info.bitmap.width * 3 / 4, 8, @info.bitmap.width * 1 / 4, 8),
                  Color.new(192, 0, 256), pkmn.heart_gauge, pkmn.max_gauge_size)
      # draw flow gauge
      pbDrawGauge(@info.bitmap, Rect.new(@info.bitmap.width * 3 / 4, 32, @info.bitmap.width * 1 / 4, 8),
                  Color.new(0, 0, 248), @chamber.chamberFlow(@set), 7)
    end
    if @chamber.setCount(@set) > 0
      textpos.push([_INTL("TEMPO"), 2, 30, :left, Color.new(248, 248, 248), Color.new(128, 128, 128)])
      # draw tempo gauge
      pbDrawGauge(@info.bitmap, Rect.new(@info.bitmap.width * 1 / 4, 32, @info.bitmap.width * 1 / 4, 8),
                  Color.new(0, 0, 248), @chamber[@set].tempo, PurifyChamber.maximumTempo)
    end
    pbDrawTextPositions(@info.bitmap, textpos)
    @info.x = Graphics.width - @info.bitmap.width
    @info.y = Graphics.height - @info.bitmap.height
    @__sprites[0].pokemon = pkmn
    @__sprites[0].x = 306
    @__sprites[0].y = 158
    @__sprites[0].z = 2
    @directflow.setAngle(angle)
    @directflow.setFlowStrength(0)
    checkCursor(0)
    points = [@chamber.setCount(@set) * 2, 1].max
    setList = @chamber.setList(@set)
    refreshFlows
    (PurifyChamber::SETSIZE * 2).times do |i|
      pkmn = (i.odd? || i >= points) ? nil : setList[i / 2]
      angle = 360 - (i * 360 / points)
      angle += 90   # start at 12 not 3 o'clock
      if pkmn && @chamber[@set].facing == i / 2
        @directflow.setAngle(angle)
        @directflow.setFlowStrength(@chamber[@set].shadowAffinity)
      end
      point = calcPoint(306, 158, 96, angle)
      @__sprites[i + 1].x = point[0]
      @__sprites[i + 1].y = point[1]
      @__sprites[i + 1].z = 2
      @__sprites[i + 1].pokemon = pkmn
      checkCursor(i + 1)
    end
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].pokemon = @heldpkmn
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].visible = @view.visible
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].x = @view.x + (@view.bitmap.width / 2)
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].y = @view.y + (@view.bitmap.height / 2)
    @__sprites[1 + (PurifyChamber::SETSIZE * 2)].z = 3
  end

  def getCurrent
    return PurifyChamberHelper.pbGetPokemon(@chamber, @cursor)
  end

  def cursor=(value)
    @cursor = value
    refresh
  end

  def heldpkmn=(value)
    @heldpkmn = value
    refresh
  end

  def set=(value)
    @set = value
    refresh
  end

  def visible=(value)
    super
    @__sprites.each do |sprite|
      sprite.visible = value
    end
    @flows.each do |flow|
      flow.visible = value
    end
    @directflow.visible = value
    @view.visible = value
    @info.visible = value
  end

  def color=(value)
    super
    @__sprites.each do |sprite|
      sprite.color = value.clone
    end
    @flows.each do |flow|
      flow.color = value.clone
    end
    @directflow.color = value.clone
    @view.color = value.clone
    @info.color = value.clone
  end

  def update
    super
    @__sprites.each do |sprite|
      sprite&.update
    end
    @flows.each do |flow|
      flow.update
    end
    @directflow.update
    @view.update
    @info.update
  end

  def dispose
    @__sprites.each do |sprite|
      sprite&.dispose
    end
    @flows.each do |flow|
      flow.dispose
    end
    @directflow.dispose
    @view.dispose
    @info.dispose
    @__sprites.clear
    super
  end
end

#===============================================================================
#
#===============================================================================
class PurifyChamberScene
  def pbUpdate
    pbUpdateSpriteHash(@sprites)
  end

  def pbRefresh
    if @sprites["setview"]
      @sprites["setview"].refresh
      @sprites["setwindow"].refresh
    end
  end

  def pbStart(chamber)
    @chamber = chamber
  end

  def pbEnd; end

  def pbOpenSet(set)
    @sprites = {}
    @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height)
    @viewport.z = 99999
    @viewportmsg = Viewport.new(0, 0, Graphics.width, Graphics.height)
    @viewportmsg.z = 99999
    addBackgroundOrColoredPlane(@sprites, "bg", "purifychamber_bg",
                                Color.new(64, 48, 96), @viewport)
    @sprites["setwindow"] = Window_PurifyChamberSets.new(
      @chamber, 0, 0, 112, Graphics.height, @viewport
    )
    @sprites["setview"] = PurifyChamberSetView.new(@chamber, set, @viewport)
    @sprites["msgwindow"] = Window_AdvancedTextPokemon.new("")
    @sprites["msgwindow"].viewport = @viewportmsg
    @sprites["msgwindow"].visible = false
    @sprites["setwindow"].index = set
    pbDeactivateWindows(@sprites)
    pbFadeInAndShow(@sprites) { pbUpdate }
  end

  def pbCloseSet
    pbFadeOutAndHide(@sprites) { pbUpdate }
    pbDisposeSpriteHash(@sprites)
    @viewport.dispose
    @viewportmsg.dispose
  end

  def pbOpenSetDetail(set)
    @sprites["setwindow"].index = set
    @sprites["setview"].set = set
    @sprites["setview"].cursor = 0
  end

  def pbCloseSetDetail; end

  def pbPurify
    pbRefresh
  end

  def pbMove(_pos)
    @sprites["setview"].heldpkmn = @sprites["setview"].getCurrent
    pbRefresh
  end

  def pbShift(_pos, _heldpoke)
    @sprites["setview"].heldpkmn = @sprites["setview"].getCurrent
    pbRefresh
  end

  def pbPlace(_pos, _heldpoke)
    @sprites["setview"].heldpkmn = nil
    pbRefresh
  end

  def pbReplace(_pos, _storagePos)
    @sprites["setview"].heldpkmn = nil
    pbRefresh
  end

  def pbRotate(facing); end

  def pbWithdraw(_pos, _heldpoke)
    @sprites["setview"].heldpkmn = nil
    pbRefresh
  end

  def pbDisplay(msg)
    UIHelper.pbDisplay(@sprites["msgwindow"], msg, false) { pbUpdate }
  end

  def pbConfirm(msg)
    UIHelper.pbConfirm(@sprites["msgwindow"], msg) { pbUpdate }
  end

  def pbShowCommands(msg, commands)
    UIHelper.pbShowCommands(@sprites["msgwindow"], msg, commands) { pbUpdate }
  end

  def pbSetScreen
    pbDeactivateWindows(@sprites) do
      loop do
        Graphics.update
        Input.update
        pbUpdate
        btn = 0
        btn = Input::DOWN if Input.repeat?(Input::DOWN)
        btn = Input::UP if Input.repeat?(Input::UP)
        btn = Input::RIGHT if Input.repeat?(Input::RIGHT)
        btn = Input::LEFT if Input.repeat?(Input::LEFT)
        if btn != 0
          pbPlayCursorSE
          @sprites["setview"].moveCursor(btn)
        end
        if Input.repeat?(Input::JUMPUP)
          nextset = (@sprites["setview"].set == 0) ? PurifyChamber::NUMSETS - 1 : @sprites["setview"].set - 1
          pbPlayCursorSE
          return [1, nextset]
        elsif Input.repeat?(Input::JUMPDOWN)
          nextset = (@sprites["setview"].set == PurifyChamber::NUMSETS - 1) ? 0 : @sprites["setview"].set + 1
          pbPlayCursorSE
          return [1, nextset]
        elsif Input.trigger?(Input::USE)
          pbPlayDecisionSE
          return [0, @sprites["setview"].cursor]
        elsif Input.trigger?(Input::BACK)
          pbPlayCancelSE
          return [3, 0]
        end
      end
    end
  end

  def pbChooseSet
    pbActivateWindow(@sprites, "setwindow") do
      oldindex = @sprites["setwindow"].index
      loop do
        if oldindex != @sprites["setwindow"].index
          oldindex = @sprites["setwindow"].index
          @sprites["setview"].set = oldindex
        end
        Graphics.update
        Input.update
        pbUpdate
        if Input.trigger?(Input::USE)
          pbPlayDecisionSE
          return @sprites["setwindow"].index
        end
        if Input.trigger?(Input::BACK)
          pbPlayCancelSE
          return -1
        end
      end
    end
  end

  def pbSwitch(set)
    @sprites["setwindow"].switching = set
    ret = pbChooseSet
    @sprites["setwindow"].switching = -1
    return ret < 0 ? set : ret
  end

  def pbSummary(pos, heldpkmn)
    if heldpkmn
      oldsprites = pbFadeOutAndHide(@sprites)
      scene = PokemonSummary_Scene.new
      screen = PokemonSummaryScreen.new(scene)
      screen.pbStartScreen([heldpkmn], 0)
      pbFadeInAndShow(@sprites, oldsprites)
      return
    end
    party = []
    indexes = []
    startindex = 0
    set = @sprites["setview"].set
    (@chamber.setCount(set) * 2).times do |i|
      p = PurifyChamberHelper.pbGetPokemon2(@chamber, set, i)
      next if !p
      startindex = party.length if i == pos
      party.push(p)
      indexes.push(i)
    end
    return if party.length == 0
    oldsprites = pbFadeOutAndHide(@sprites)
    scene = PokemonSummary_Scene.new
    screen = PokemonSummaryScreen.new(scene)
    selection = screen.pbStartScreen(party, startindex)
    @sprites["setview"].cursor = indexes[selection]
    pbFadeInAndShow(@sprites, oldsprites)
  end

  def pbPositionHint(pos)
    @sprites["setview"].cursor = pos
    pbRefresh
  end

  def pbChangeSet(set)
    @sprites["setview"].set = set
    @sprites["setwindow"].index = set
    @sprites["setwindow"].refresh
    pbRefresh
  end

  def pbChoosePokemon
    visible = pbFadeOutAndHide(@sprites)
    scene = PokemonStorageScene.new
    screen = PokemonStorageScreen.new(scene, $PokemonStorage)
    pos = screen.pbChoosePokemon
    pbRefresh
    pbFadeInAndShow(@sprites, visible)
    return pos
  end
end

#===============================================================================
#
#===============================================================================
def pbPurifyChamber
  $player.seen_purify_chamber = true
  pbFadeOutIn do
    scene = PurifyChamberScene.new
    screen = PurifyChamberScreen.new(scene)
    screen.pbStartPurify
  end
end

#===============================================================================
#
#===============================================================================
MenuHandlers.add(:pc_menu, :purify_chamber, {
  "name"      => _INTL("Purify Chamber"),
  "order"     => 30,
  "condition" => proc { next $player.seen_purify_chamber },
  "effect"    => proc { |menu|
    pbMessage("\\se[PC access]" + _INTL("Accessed the Purify Chamber."))
    pbPurifyChamber
    next false
  }
})
