require 'duplicate'
module CryptoconditionsRuby
  module Types
    CONDITION = 'condition'
    FULFILLMENT = 'fulfillment'

    class ThresholdSha256Fulfillment < BaseSha256Fulfillment
      TYPE_ID = 2
      FEATURE_BITMASK = 0x09

      attr_accessor :bitmask, :threshold, :subconditions
      private :bitmask
      def initialize(threshold = nil)
        if threshold && (!threshold.is_a?(Integer) || threshold < 1)
          raise StandardError, "Threshold must be a integer greater than zero, was: #{threshold}"
        end
        self.threshold = threshold
        self.subconditions = []
      end

      def add_subcondition(subcondition, weight = 1)
        if subcondition.is_a?(String)
          subcondition = Condition.from_uri(subcondition)
        end
        unless subcondition.is_a?(Condition)
          raise TypeError, 'Subconditions must be URIs or objects of type Condition'
        end
        unless weight.is_a?(Integer) || weight < 1
          raise StandardError, "Invalid weight: #{weight}"
        end

        subconditions.push(
          'type' => CONDITION,
          'body' => subcondition,
          'weight' => weight
        )
      end

      def add_subcondition_uri(subcondition_uri)
        unless subcondition_uri.is_a?(String)
          raise TypeError, "Subcondition must be provided as a URI string, was #{subcondition_uri}"
        end

        add_subcondition(Condition.from_uri(subcondition_uri))
      end

      def add_subfulfillment(subfulfillment, weight = 1)
        if subfulfillment.is_a?(String)
          subfulfillment = Fulfillment.from_uri(subfulfillment)
        end
        unless subfulfillment.is_a?(Fulfillment)
          raise TypeError, 'Subfulfillments must be URIs or objects of type Fulfillment'
        end
        if !weight.is_a?(Integer) || weight < 0
          raise StandardError, "Invalid weight: #{weight}"
        end
        subconditions.push(
          'type' => FULFILLMENT,
          'body' => subfulfillment,
          'weight' => weight
        )
      end

      def add_subfulfillment_uri(subfulfillment_uri)
        unless subfulfillment_uri.is_a?(String)
          raise TypeError, "Subfulfillment must be provided as a URI string, was: #{subfulfillment_uri}"
        end
        add_subfulfillment(Fulfillment.from_uri(subfulfillment_uri))
      end

      def bitmask
        bitmask = super
        subconditions.each do |cond|
          bitmask |= cond['body'].bitmask
        end
        bitmask
      end

      def get_subcondition_from_vk(vk)
        subconditions.inject([]) do |store, c|
          if c['body'].is_a?(Ed25519Fulfillment) && Utils::Base58.encode(c['body'].public_key.to_s) == vk
            store.push(c)
          elsif c['body'].is_a?(ThresholdSha256Fulfillment)
            result = c['body'].get_subcondition_from_vk(vk)
            store += result if result
          end
          store
        end
      end

      def write_hash_payload(hasher)
        raise StandardError, 'Requires subconditions' if subconditions.empty?

        _subconditions = subconditions.inject([]) do |store, c|
          writer = Utils::Writer.new
          writer.write_var_uint(c['weight'])
          writer.write(
            c['type'] == FULFILLMENT ? c['body'].condition_binary : c['body'].serialize_binary
          )
          store.push(writer.buffer)
        end
        sorted_subconditions = ThresholdSha256Fulfillment.sort_buffers(_subconditions)

        hasher.write_uint32(threshold)
        hasher.write_var_uint(sorted_subconditions.length)
        sorted_subconditions.each do |cond|
          hasher.write(cond)
        end
        hasher
      end

      def calculate_max_fulfillment_length
        total_condition_len = 0

        _subconditions = subconditions.map do |c|
          condition_len = ThresholdSha256Fulfillment.predict_subcondition_length(c)
          fulfillment_len = ThresholdSha256Fulfillment.predict_subfulfillment_length(c)
          total_condition_len += condition_len
          {
            'weight' => c['weight'],
            'size' => fulfillment_len - condition_len
          }
        end

        _subconditions.sort_by! { |x| x['weight'].abs }

        worst_case_fulfillments_length = total_condition_len + ThresholdSha256Fulfillment.calculate_worst_case_length(threshold, _subconditions)

        if worst_case_fulfillments_length == -Float::INFINITY
          raise StandardError, 'Insufficient subconditions/weights to meet the threshold'
        end

        # Calculate resulting total maximum fulfillment size
        predictor = Utils::Predictor.new
        predictor.write_uint32(threshold)
        predictor.write_var_uint(subconditions.length)
        subconditions.each do |c|
          predictor.write_uint8(nil)
          predictor.write_var_uint(c['weight']) unless c['weight'] == 1
        end

        predictor.skip(worst_case_fulfillments_length)

        predictor.size
      end

      def self.predict_subcondition_length(cond)
        return cond['body'].condition_binary.length if cond['type'] == FULFILLMENT

        cond['body'].serialize_binary.length
      end

      def self.predict_subfulfillment_length(cond)
        fulfillment_len = if cond['type'] == FULFILLMENT
                            cond['body'].condition.max_fulfillment_length
                          else
                            cond['body'].max_fulfillment_length
                          end

        predictor = Utils::Predictor.new
        predictor.write_uint16(nil)
        predictor.write_var_octet_string('0' * fulfillment_len)
        predictor.size
      end

      def self.calculate_worst_case_length(threshold, subconditions, index = 0)
        return 0 if threshold <= 0
        if index < subconditions.length
          next_condition = subconditions[index]

          [
            next_condition['size'] + ThresholdSha256Fulfillment.calculate_worst_case_length(
              threshold - next_condition['weight'].abs,
              subconditions,
              index + 1
            ),
            ThresholdSha256Fulfillment.calculate_worst_case_length(
              threshold,
              subconditions,
              index + 1
            )
          ].max
        else
          -Float::INFINITY
        end
      end

      def parse_payload(reader, *args)
        raise TypeError, 'reader must be a Reader instance' unless reader.is_a?(Utils::Reader)

        self.threshold = reader.read_var_uint
        condition_count = reader.read_var_uint

        condition_count.times do
          weight = reader.read_var_uint
          fulfillment = reader.read_var_octet_string
          condition = reader.read_var_octet_string
          if !fulfillment.empty? && !condition.empty?
            raise TypeError, 'Subconditions may not provide both subcondition and fulfillment.'
          elsif
            if !fulfillment.empty?
              add_subfulfillment(Fulfillment.from_binary(fulfillment), weight)
            elsif !condition.empty?
              add_subcondition(Condition.from_binary(condition), weight)
            else
              raise TypeError, 'Subconditions must provide either subcondition or fulfillment.'
            end
          end
        end
      end

      def write_payload(writer)
        raise TypeError, 'writer must be a Writer instance' unless writer.is_a?(Utils::Writer)

        subfulfillments = subconditions.each_with_index.map do |c, i|
          next unless c['type'] == FULFILLMENT

          subfulfillment = c.dup
          subfulfillment.merge!(
            'index' => i,
            'size' => c['body'].serialize_binary.length,
            'omit_size' => c['body'].condition_binary.length
          )
        end.compact

        smallest_set = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
          threshold, subfulfillments
        )['set']

        optimized_subfulfillments = subconditions.each_with_index.map do |c, i|
          if c['type'] == FULFILLMENT && !smallest_set.include?(i)
            subfulfillment = c.dup
            subfulfillment.update(
              'type' => CONDITION,
              'body' => c['body'].condition
            )
          else
            c
          end
        end

        serialized_subconditions = optimized_subfulfillments.map do |c|
          writer_ = Utils::Writer.new
          writer_.write_var_uint(c['weight'])
          writer_.write_var_octet_string(c['type'] == FULFILLMENT ? c['body'].serialize_binary : '')
          writer_.write_var_octet_string(c['type'] == CONDITION ? c['body'].serialize_binary : '')
          writer_.buffer
        end

        sorted_subconditions = ThresholdSha256Fulfillment.sort_buffers(serialized_subconditions)

        writer.write_var_uint(threshold)
        writer.write_var_uint(sorted_subconditions.length)
        sorted_subconditions.each { |c| writer.write(c) }
        writer
      end

      def self.calculate_smallest_valid_fulfillment_set(threshold, fulfillments, state = nil)
        state ||= { 'index' => 0, 'size' => 0, 'set' => [] }

        if threshold <= 0
          { 'size' => state['size'], 'set' => state['set'] }
        elsif state['index'] < fulfillments.length
          next_fulfillment = fulfillments[state['index']]
          with_next = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
            threshold - next_fulfillment['weight'].abs,
            fulfillments,
            'size' => state['size'] + next_fulfillment['size'],
            'index' => state['index'] + 1,
            'set' => state['set'] + [next_fulfillment['index']]
          )

          without_next = ThresholdSha256Fulfillment.calculate_smallest_valid_fulfillment_set(
            threshold,
            fulfillments,
            'size' => state['size'] + next_fulfillment['omit_size'],
            'index' => state['index'] + 1,
            'set' => state['set']
          )
          with_next['size'] < without_next['size'] ? with_next : without_next
        else
          { 'size' => Float::INFINITY }
        end
      end

      def self.sort_buffers(buffers)
        Duplicate.duplicate(buffers).sort_by { |item| [item.length, item] }
      end

      def to_dict
        subfulfillments = subconditions.map do |c|
          subcondition = c['body'].to_dict
          subcondition.merge!('weight' => c['weight'])
        end

        {
          'type' => 'fulfillment',
          'type_id' => type_id,
          'bitmask' => bitmask,
          'threshold' => threshold,
          'subfulfillments' => subfulfillments
        }
      end

      def parse_dict(data)
        raise TypeError, 'reader must be a dict instance' unless data.is_a?(Hash)
        self.threshold = data['threshold']

        data['subfulfillments'].each do |subfulfillments|
          weight = subfulfillments['weight']
          if subfulfillments['type'] == FULFILLMENT
            add_subfulfillment(Fulfillment.from_dict(subfulfillments), weight)
          elsif subfulfillments['type'] == CONDITION
            add_subcondition(Condition.from_dict(subfulfillments), weight)
          else
            raise TypeError, 'Subconditions must provide either subcondition or fulfillment.'
          end
        end
      end

      def validate(message: nil, **_kwargs)
        fulfillments = subconditions.select { |c| c['type'] == FULFILLMENT }

        min_weight = Float::INFINITY
        total_weight = 0
        fulfillments.each do |fulfillment|
          min_weight = [min_weight, fulfillment['weight'].abs].max
          total_weight += min_weight
        end

        # Total weight must meet the threshold
        return if total_weight < threshold

        valid_decisions = fulfillments.map do |fulfillment|
          if fulfillment['body'].validate(message: message, **_kwargs)
            [true] * fulfillment['weight']
          end
        end.compact.flatten
        valid_decisions.count >= threshold
      end
    end
  end
end
