#! /usr/bin/env crystal
#
# This script generates the `src/object/properties.cr` file with the whole set
# of `[class_](getter|setter|property)[?!]` macros to avoid duplicating the
# implementations. Having an external script avoids the runtime cost of having
# macros generating AST calls to other macros that must expanded again.

struct Generator
  def initialize(@file : File, @macro_prefix : String, @method_prefix : String, @var_prefix : String, @doc_prefix : String)
  end

  def puts
    @file.puts
  end

  def puts(message)
    @file.puts(message)
  end

  def def_vars
    def_vars do
      <<-TEXT
              {% if block %}
                #{@var_prefix}{{var_name}} : {{type}}? {% if name.value %} = {{name.value}} {% end %}
              {% else %}
                #{@var_prefix}{{name}}
              {% end %}
      TEXT
    end
  end

  def def_vars_no_macro_block
    def_vars { "#{@var_prefix}{{name}}" }
  end

  def def_vars(&)
    <<-TEXT
          {% if name.is_a?(TypeDeclaration) %}
            {% var_name = name.var.id %}
            {% type = name.type %}
            #{yield.lstrip}
          {% elsif name.is_a?(Assign) %}
            {% var_name = name.target %}
            {% type = nil %}
            #{@var_prefix}{{name}}
          {% else %}
            {% var_name = name.id %}
            {% type = nil %}
          {% end %}

    TEXT
  end

  def def_vars!
    <<-TEXT
          {% if name.is_a?(TypeDeclaration) %}
            {% var_name = name.var.id %}
            {% type = name.type %}
            #{@var_prefix}{{name}}?
          {% else %}
            {% var_name = name.id %}
            {% type = nil %}
          {% end %}

    TEXT
  end

  def def_getter(suffix = "")
    <<-TEXT
          def #{@method_prefix}{{var_name}}#{suffix} {% if type %} : {{type}} {% end %}
            {% if block %}
              if (%value = #{@var_prefix}{{var_name}}).nil?
                #{@var_prefix}{{var_name}} = {{yield}}
              else
                %value
              end
            {% else %}
              #{@var_prefix}{{var_name}}
            {% end %}
          end

    TEXT
  end

  def def_getter!
    <<-TEXT
          def #{@method_prefix}{{var_name}}? {% if type %} : {{type}}? {% end %}
            #{@var_prefix}{{var_name}}
          end

          def #{@method_prefix}{{var_name}} {% if type %} : {{type}} {% end %}
            if (%value = #{@var_prefix}{{var_name}}).nil?
              ::raise ::NilAssertionError.new("{{@type.id}}{{#{@doc_prefix.inspect}.id}}{{var_name}} cannot be nil")
            else
              %value
            end
          end

    TEXT
  end

  def def_setter
    <<-TEXT
          def #{@method_prefix}{{var_name}}=(#{@var_prefix}{{var_name}}{% if type %} : {{type}} {% end %})
          end

    TEXT
  end

  def gen_property_macros
    puts <<-TEXT
      # Generates both `#{@macro_prefix}getter` and `#{@macro_prefix}setter`
      # methods to access instance variables.
      #
      # Refer to the aforementioned macros for details.
      macro #{@macro_prefix}property(*names, &block)
        {% for name in names %}
    #{def_vars}
    #{def_getter}
    #{def_setter}
        {% end %}
      end

      # Generates both `#{@macro_prefix}getter?` and `#{@macro_prefix}setter`
      # methods to access instance variables.
      #
      # Refer to the aforementioned macros for details.
      macro #{@macro_prefix}property?(*names, &block)
        {% for name in names %}
    #{def_vars}
    #{def_getter "?"}
    #{def_setter}
        {% end %}
      end

      # Generates both `#{@macro_prefix}getter!` and `#{@macro_prefix}setter`
      # methods to access instance variables.
      #
      # Refer to the aforementioned macros for details.
      macro #{@macro_prefix}property!(*names)
        {% for name in names %}
    #{def_vars!}
    #{def_getter!}
    #{def_setter}
        {% end %}
      end
    TEXT
  end
end

directory = File.expand_path("../src/object", __DIR__)
Dir.mkdir(directory) unless Dir.exists?(directory)

output = File.join(directory, "properties.cr")
File.open(output, "w") do |f|
  f.puts "# This file was automatically generated by running:"
  f.puts "#"
  f.puts "#   scripts/generate_object_properties.cr"
  f.puts "#"
  f.puts "# DO NOT EDIT"
  f.puts
  f.puts "class Object"

  g = Generator.new(f, "", "", "@", "#")

  f.puts <<-TEXT
    # Defines getter methods to access instance variables.
    #
    # Refer to [Getters](#getters) for details.
    macro getter(*names, &block)
      {% for name in names %}
  #{g.def_vars}
  #{g.def_getter}
      {% end %}
    end

    # Identical to `getter` but defines query methods.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   getter? working
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   def working?
    #     @working
    #   end
    # end
    # ```
    #
    # Refer to [Getters](#getters) for general details.
    macro getter?(*names, &block)
      {% for name in names %}
  #{g.def_vars}
  #{g.def_getter "?"}
      {% end %}
    end

    # Similar to `getter` but defines both raise-on-nil methods as well as query
    # methods that return a nilable value.
    #
    # If a type is specified, then it will become a nilable type (union of the
    # type and `Nil`). Unlike the other `getter` methods the value is always
    # initialized to `nil`. There are no initial value or lazy initialization.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   getter! name : String
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   @name : String?
    #
    #   def name? : String?
    #     @name
    #   end
    #
    #   def name : String
    #     @name.not_nil!("Robot#name cannot be nil")
    #   end
    # end
    # ```
    #
    # Refer to [Getters](#getters) for general details.
    macro getter!(*names)
      {% for name in names %}
  #{g.def_vars!}
  #{g.def_getter!}
      {% end %}
    end

    # Generates setter methods to set instance variables.
    #
    # Refer to [Setters](#setters) for general details.
    macro setter(*names)
      {% for name in names %}
  #{g.def_vars_no_macro_block}
  #{g.def_setter}
      {% end %}
    end
  TEXT

  g.gen_property_macros

  g = Generator.new(f, "class_", "self.", "@@", ".")

  f.puts <<-TEXT
    # Defines getter methods to access class variables.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   class_getter backend
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   def self.backend
    #     @@backend
    #   end
    # end
    # ```
    #
    # Refer to [Getters](#getters) for details.
    macro class_getter(*names, &block)
      {% for name in names %}
  #{g.def_vars}
  #{g.def_getter}
      {% end %}
    end

    # Identical to `class_getter` but defines query methods.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   class_getter? backend
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   def self.backend?
    #     @@backend
    #   end
    # end
    # ```
    #
    # Refer to [Getters](#getters) for general details.
    macro class_getter?(*names, &block)
      {% for name in names %}
  #{g.def_vars}
  #{g.def_getter "?"}
      {% end %}
    end

    # Similar to `class_getter` but defines both raise-on-nil methods as well as
    # query methods that return a nilable value.
    #
    # If a type is specified, then it will become a nilable type (union of the
    # type and `Nil`). Unlike with `class_getter` the value is always initialized
    # to `nil`. There are no initial value or lazy initialization.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   class_getter! backend : String
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   @@backend : String?
    #
    #   def self.backend? : String?
    #     @@backend
    #   end
    #
    #   def backend : String
    #     @@backend.not_nil!("Robot.backend cannot be nil")
    #   end
    # end
    # ```
    #
    # Refer to [Getters](#getters) for general details.
    macro class_getter!(*names)
      {% for name in names %}
  #{g.def_vars!}
  #{g.def_getter!}
      {% end %}
    end

    # Generates setter methods to set class variables.
    #
    # For example, writing:
    #
    # ```
    # class Robot
    #   class_setter factories
    # end
    # ```
    #
    # Is equivalent to writing:
    #
    # ```
    # class Robot
    #   @@factories
    #
    #   def self.factories=(@@factories)
    #   end
    # end
    # ```
    #
    # Refer to [Setters](#setters) for general details.
    macro class_setter(*names)
      {% for name in names %}
  #{g.def_vars_no_macro_block}
  #{g.def_setter}
      {% end %}
    end
  TEXT

  g.gen_property_macros

  f.puts "end"
end
