require 'sober_swag/serializer'

module SoberSwag
  ##
  # Create a serializer that is heavily inspired by the "Blueprinter" library.
  # This allows you to make "views" and such inside.
  #
  # Under the hood, this is actually all based on {SoberSwag::Serialzier::Base}.
  class OutputObject < SoberSwag::Serializer::Base
    autoload(:Field, 'sober_swag/output_object/field')
    autoload(:Definition, 'sober_swag/output_object/definition')
    autoload(:FieldSyntax, 'sober_swag/output_object/field_syntax')
    autoload(:View, 'sober_swag/output_object/view')

    ##
    # Use a OutputObject to define a new serializer.
    # It will be based on {SoberSwag::Serializer::Base}.
    #
    # An example is illustrative:
    #
    #     PersonSerializer = SoberSwag::OutputObject.define do
    #       field :id, primitive(:Integer)
    #       field :name, primtive(:String).optional
    #
    #       view :complex do
    #         field :age, primitive(:Integer)
    #         field :title, primitive(:String)
    #       end
    #     end
    #
    # Note: This currently will generate a new *class* that does serialization.
    # However, this is only a hack to get rid of the weird naming issue when
    # generating swagger from dry structs: their section of the schema area
    # is defined by their *Ruby Class Name*. In the future, if we get rid of this,
    # we might be able to keep this on the value-level, in which case {#define}
    # can simply return an *instance* of SoberSwag::Serializer that does
    # the correct thing, with the name you give it. This works for now, though.
    def self.define(&block)
      d = Definition.new.tap do |o|
        o.instance_eval(&block)
      end
      new(d.fields, d.views, d.identifier)
    end

    def initialize(fields, views, identifier)
      @fields = fields
      @views = views
      @identifier = identifier
    end

    attr_reader :fields, :views, :identifier

    def serialize(obj, opts = {})
      serializer.serialize(obj, opts)
    end

    def type
      serializer.type
    end

    def view(name)
      return base_serializer if name == :base

      @views.find { |v| v.name == name }
    end

    def base
      base_serializer
    end

    ##
    # Compile down this to an appropriate serializer.
    # It uses {SoberSwag::Serializer::Conditional} to do view-parsing,
    # and {SoberSwag::Serializer::FieldList} to do the actual serialization.
    def serializer # rubocop:disable Metrics/MethodLength
      @serializer ||=
        begin
          views.reduce(base_serializer) do |base, view|
            view_serializer = view.serializer
            SoberSwag::Serializer::Conditional.new(
              proc do |object, options|
                if options[:view].to_s == view.name.to_s
                  [:left, object]
                else
                  [:right, object]
                end
              end,
              view_serializer,
              base
            )
          end
        end
    end

    def base_serializer
      @base_serializer ||= SoberSwag::Serializer::FieldList.new(fields).tap do |s|
        s.identifier(identifier)
      end
    end
  end
end