# frozen_string_literal: true

module BCDice
  # 乱数生成器
  class Randomizer
    UPPER_LIMIT_DICE_TIMES = 200
    UPPER_LIMIT_DICE_SIDES = 10000

    UPPER_LIMIT_RANDS = 10000

    def initialize
      @rand_results = []
      @detailed_rand_results = []
    end

    # @return [Array<Array<(Integer, Integer)>>] ダイスの出目一覧
    attr_reader :rand_results

    # @return [Array<DetailedRandResult>]
    attr_reader :detailed_rand_results

    # 実行したダイスロールの詳細
    # @!attribute [rw] kind
    #   @return [Symbol]
    # @!attribute [rw] sides
    #   @return [Integer] ダイスロールしたダイスの面数
    # @!attribute [rw] value
    #   @return [Integer] 値
    DetailedRandResult = Struct.new(:kind, :sides, :value)

    # 複数個のダイスを振る
    #
    # @param times [Integer] 振るダイスの個数
    # @param sides [Integer] ダイスの面数
    # @return [Array<Integer>] ダイスの出目一覧
    def roll_barabara(times, sides)
      if @rand_results.size + times > UPPER_LIMIT_RANDS
        raise TooManyRandsError
      end

      if times <= 0 || times > UPPER_LIMIT_DICE_TIMES
        return []
      end

      Array.new(times) { roll_once(sides) }
    end

    # 複数個のダイスを振って、その合計を求める
    #
    # @param times [Integer] 振るダイスの個数
    # @param sides [Integer] ダイスの面数
    # @return [Integer] 出目の合計
    def roll_sum(times, sides)
      roll_barabara(times, sides).sum()
    end

    # 1回だけダイスロールを行う
    #
    # @param sides [Integer] ダイスの面数
    # @return [Integer] 1以上 *sides* 以下の値のいずれか
    def roll_once(sides)
      if sides <= 0 || sides > UPPER_LIMIT_DICE_SIDES
        return 0
      end

      dice = rand_inner(sides)
      push_to_detail(:normal, sides, dice)

      return dice
    end

    # ダイス表などでindexを参照する用のダイスロール
    # @param sides [Integer]
    # @return [Integer] 0以上 *sides* 未満の整数
    def roll_index(sides)
      roll_once(sides) - 1
    end

    # 十の位をd10を使って決定するためのダイスロール
    # @return [Integer] 0以上90以下で10の倍数となる整数
    def roll_tens_d10()
      # rand_innerの戻り値を10倍すればすむ話なのだが、既存のテストとの互換性の為に処理をする
      dice = rand_inner(10)
      if dice == 10
        dice = 0
      end

      ret = dice * 10

      push_to_detail(:tens_d10, 10, ret)
      return ret
    end

    # d10を0~9として扱うダイスロール
    # @return [Integer] 0以上9以下の整数
    def roll_d9()
      dice = rand_inner(10) - 1

      push_to_detail(:d9, 10, dice)
      return dice
    end

    # D66のダイスロールを行う
    # @param sort_type [Symbol] BCDice::D66SortType
    # @return [Integer]
    def roll_d66(sort_type)
      dice_list = Array.new(2) { roll_once(6) }

      case sort_type
      when D66SortType::ASC
        dice_list.sort!
      when D66SortType::DESC
        dice_list.sort!.reverse!
      end

      return dice_list[0] * 10 + dice_list[1]
    end

    private

    # @param sides [Integer]
    # @return [Integer] 1以上sides以下の整数
    def rand_inner(sides)
      if @rand_results.size >= UPPER_LIMIT_RANDS
        raise TooManyRandsError
      end

      dice = random(sides)

      @rand_results << [dice, sides]
      return dice
    end

    # モックで上書きする用
    # @param sides [Integer]
    # @return [Integer] 1以上sides以下の整数
    def random(sides)
      Kernel.rand(sides) + 1
    end

    # @param [Symbol] kind
    # @param [Integer] sides
    # @param [Integer] value
    def push_to_detail(kind, sides, value)
      detail = DetailedRandResult.new(kind, sides, value)
      @detailed_rand_results.push(detail)
    end
  end

  class TooManyRandsError < StandardError; end
end
