require "inflector/core_ext"
require "pg"

# This module declare all the methods and macro related to columns in `Clear::Model`
module Clear::Model::HasColumns
  macro included # In Clear::Model
    macro included # In RealModel
      COLUMNS = {} of Nil => Nil
      # Attributes, used when fetch_columns is true
      getter attributes : Hash(String, ::Clear::SQL::Any) = {} of String => ::Clear::SQL::Any

      # Special reinitialization if we detect inheritance (meaning polymorphism)
      macro inherited
        # Reset COLUMNS constants
        COLUMNS = {} of Nil => Nil
        # Table is same than parent table
        self.table = \\{{@type.ancestors.first}}.table
      end
    end
  end

  # Reset one or multiple columns; Reseting set the current value of the column
  # to the given value, while the `changed?` flag remains false.
  # If you call save on a persisted model, the reset columns won't be
  # commited in the UPDATE query.
  def reset(**t : **T) forall T
    # Dev note:
    # ---------
    # The current implementation of reset is overriden on finalize (see below).
    # This method is a placeholder to ensure that we can call `super`
    # in case of inherited (polymorphic) models
  end

  # See `reset(**t : **T)`
  def reset(h : Hash(String, _))
  end

  # See `reset(**t : **T)`
  def reset(h : Hash(Symbol, _))
  end

  # Set one or multiple columns to a specific value
  # This two are equivalents:
  #
  # ```
  # model.set(a: 1)
  # model.a = 1
  # ```
  def set(**t : **T) forall T
    # Dev note:
    # ---------
    # The current implementation of set is overriden on finalize (see below).
    # This method is a placeholder to ensure that we can call `super`
    # in case of inherited (polymorphic) models
  end

  # See `set(**t : **T)`
  def set(h : Hash(String, _))
  end

  # See `set(**t : **T)`
  def set(h : Hash(Symbol, _))
  end

  # Access to direct SQL attributes given by the request used to build the model.
  # Access is read only and updating the model columns will not apply change to theses columns.
  #
  # ```
  # model = Model.query.select("MIN(id) as min_id").first(fetch_columns: true)
  # id = model["min_id"].to_i32
  # ```
  def [](x) : ::Clear::SQL::Any
    attributes[x]
  end

  # Access to direct SQL attributes given by the request and used to build the model
  # or Nil if not found.
  #
  # Access is read only and updating the model columns will not apply change to theses columns.
  # You must set `fetch_column: true` in your model to access the attributes.
  def []?(x) : ::Clear::SQL::Any
    attributes[x]?
  end

  # Returns the current hash of the modified values:
  #
  # ```
  # model = Model.query.first!
  # model.update_h # => {}
  # model.first_name = "hello"
  # model.update_h # => { "first_name" => "hello" }
  # model.save!
  # model.update_h # => {}
  # ```
  def update_h
    {} of String => ::Clear::SQL::Any
  end

  # Returns the model columns as Hash.
  # Calling `to_h` will returns only the defined columns, while settings the optional parameter `full` to `true`
  #   will return all the column and fill the undefined columns by `nil` values.
  # Example:
  #
  # ```
  # # Assuming our model has a primary key, a first name and last name and two timestamp columns:
  # model = Model.query.select("first_name, last_name").first!
  # model.to_h             # => { "first_name" => "Johnny", "last_name" => "Walker" }
  # model.to_h(full: true) # => {"id" => nil, "first_name" => "Johnny", "last_name" => "Walker", "created_at" => nil, "updated_at" => nil}
  # ```
  def to_h(full = false)
    {} of String => ::Clear::SQL::Any
  end

  # Bind a column to the model.
  #
  # Simple example:
  # ```
  # class MyModel
  #   include Clear::Model
  #
  #   column some_id : Int32, primary: true
  #   column nullable_column : String?
  # end
  # ```
  # options:
  #
  # * `primary : Bool`: Let Clear ORM know which column is the primary key.
  # Currently compound primary key are not compatible with Clear ORM.
  #
  # * `converter : Class | Module`: Use this class to convert the data from the
  # SQL. This class must possess the class methods
  # `to_column(::Clear::SQL::Any) : T` and `to_db(T) : ::Clear::SQL::Any`
  # with `T` the type of the column.
  #
  # * `column_name : String`: If the name of the column in the model doesn't fit the name of the
  #   column in the SQL, you can use the parameter `column_name` to tell Clear about
  #   which db column is linked to current field.
  #
  # * `presence : Bool (default = true)`: Use this option to let know Clear that
  #   your column is not nullable but with default value generated by the database
  #   on insert (e.g. serial)
  # During validation before saving, the presence will not be checked on this field
  #   and Clear will try to insert without the field value.
  #
  # * `mass_assign : Bool (default = true)`: Use this option to turn on/ off mass assignment
  #   when instantiating or updating a new model from json through `.from_json` methods from
  #   the `Clear::Model::JSONDeserialize` module.
  #
  macro column(name, primary = false, converter = nil, column_name = nil, presence = true, mass_assign = true)
    {% _type = name.type %}
    {%
      unless converter
        if _type.is_a?(Path)
          if _type.resolve.stringify =~ /\(/
            converter = _type.stringify
          else
            converter = _type.resolve.stringify
          end
        elsif _type.is_a?(Generic) # Union?
          if _type.name.stringify == "::Union"
            converter = (_type.type_vars.map(&.resolve).map(&.stringify).sort.reject { |x| x == "Nil" || x == "::Nil" }.join("")).id.stringify
          else
            converter = _type.resolve.stringify
          end
        elsif _type.is_a?(Union)
          converter = (_type.types.map(&.resolve).map(&.stringify).sort.reject { |x| x == "Nil" || x == "::Nil" }.join("")).id.stringify
        else
          raise "Unknown: #{_type}, #{_type.class}"
        end
      end
    %}

    {%
      db_column_name = column_name == nil ? name.var : column_name.id

      COLUMNS["#{db_column_name.id}"] = {
        type:                  _type,
        primary:               primary,
        converter:             converter,
        db_column_name:        "#{db_column_name.id}",
        crystal_variable_name: name.var,
        presence:              presence,
        mass_assign:           mass_assign,
      }
    %}
  end

  # :nodoc:
  # Used internally to gather the columns
  macro __generate_columns__
    {% for db_column_name, settings in COLUMNS %}
      {% type = settings[:type] %}

      {% var_name = settings[:crystal_variable_name] %}
      {% db_name = settings[:db_column_name] %}

      {% has_db_default = !settings[:presence] %}
      {% converter = Clear::Model::Converter::CONVERTERS[settings[:converter]] %}
      {% if converter == nil %}
        {% raise "No converter found for `#{settings[:converter].id}`.\n" +
                 "The type is probably not supported natively by Clear.\n" +
                 "Please refer to the manual to create a custom converter.\n" +
                 "If this errors appears when settings belongs_to relation, ensure than `foreign_key_type` is set to\n" +
                 "not nilable type (e.g. Int32 instead of Int32?). Clear will use the nilable parameter of the model instead." %}
      {% end %}

      @{{var_name}}_column : Clear::Model::Column({{type}}, {{converter}}) =
        Clear::Model::Column({{type}}, {{converter}}).new({{db_name}},
        has_db_default: {{has_db_default}} )

      # Returns the column object used to manage `{{var_name}}` field
      #
      # See `Clear::Model::Column`
      def {{var_name}}_column : Clear::Model::Column({{type}}, {{converter}})
        @{{var_name}}_column
      end

      # Returns the value of `{{var_name}}` column or throw an exception if the column is not defined.
      def {{var_name}} : {{type}}
        @{{var_name}}_column.value
      end

      # Setter for `{{var_name}}` column.
      def {{var_name}}=(x : {{type}})
        @{{var_name}}_column.value = x
      end

      {% if settings[:primary] %}
        # :nodoc:
        class_property __pkey__ : String = "{{var_name}}"

        # :nodoc:
        def __pkey__
          @{{var_name}}_column.value
        end

        # :nodoc:
        def __pkey_column__
          @{{var_name}}_column
        end
      {% end %}
    {% end %}

    # reset flavors
    def reset( **t : **T ) forall T
      super

      {% verbatim do %}

        {% for name, typ in T %}

          {% if settings = COLUMNS["#{name}"] %}
            @{{settings[:crystal_variable_name]}}_column.reset_convert(t[:{{name}}])
          {% else %}
            {% if !@type.has_method?("#{name}=") %}
              {% raise "Cannot find the column `#{name}` of the model `#{@type}`" %}
            {% end %}
            self.{{name}} = t[:{{name}}]
          {% end %}
        {% end %}
      {% end %}
      self
    end

    def reset( t : NamedTuple )
      reset(**t)
    end

    # Set the columns from hash
    def reset( h : Hash(Symbol, _) )
      super

      {% verbatim do %}
        {% for name, settings in COLUMNS %}
          v = h.fetch(:\{{settings[:db_column_name]}}){ Column::UNKNOWN }
          @{{settings[:crystal_variable_name]}}_column.reset_convert(v) unless v.is_a?(Column::UnknownClass)
        {% end %}
      {% end %}
      self
    end

    # Set the model fields from hash
    def reset( h : Hash(String, _) )
      super

      {% verbatim do %}
        {% for name, settings in COLUMNS %}
          v = h.fetch({{settings[:db_column_name]}}){ Column::UNKNOWN }
          @{{settings[:crystal_variable_name]}}_column.reset_convert(v) unless v.is_a?(Column::UnknownClass)
        {% end %}
      {% end %}

      self
    end

    def reset( from_json : JSON::Any )
      reset(from_json.as_h)
    end

    def set( **t : **T ) forall T
      super

      {% verbatim do %}
        {% for name, typ in T %}
          {% if settings = COLUMNS["#{name}".id] %}
            @{{settings[:crystal_variable_name]}}_column.set_convert(t[:{{name}}])
          {% else %}
            {% if !@type.has_method?("#{name}=") %}
              {% raise "No method #{@type}##{name}= while trying to set value of #{name}" %}
            {% end %}
            self.{{name}} = t[:{{name}}]
          {% end %}
        {% end %}
      {% end %}

      self
    end

    def set( t : NamedTuple )
      set(**t)
    end

    # Set the columns from hash
    def set( h : Hash(Symbol, _) )
      super

      {% verbatim do %}
        {% for name, settings in COLUMNS %}
          v = h.fetch(:{{settings[:db_column_name]}}){ Column::UNKNOWN }
          @{{settings[:crystal_variable_name]}}_column.set_convert(v) unless v.is_a?(Column::UnknownClass)
        {% end %}
      {% end %}

      self
    end

    # Set the model fields from hash
    def set( h : Hash(String, _) )
      super

      {% verbatim do %}
        {% for name, settings in COLUMNS %}
          v = h.fetch({{settings[:db_column_name]}}){ Column::UNKNOWN }
          @{{settings[:crystal_variable_name]}}_column.set_convert(v) unless v.is_a?(Column::UnknownClass)
        {% end %}
      {% end %}

      self
    end

    def set( from_json : JSON::Any )
      set(from_json.as_h)
    end


    # Generate the hash for update request (like during save)
    def update_h : Hash(String, ::Clear::SQL::Any)
      o = super

      {% for name, settings in COLUMNS %}
        if @{{settings[:crystal_variable_name]}}_column.defined? &&
           @{{settings[:crystal_variable_name]}}_column.changed?
          o[{{settings[:db_column_name]}}] = @{{settings[:crystal_variable_name]}}_column.to_sql_value
        end
      {% end %}

      o
    end

    # set flavors


    # For each column, ensure than when needed the column has present
    # information into it.
    #
    # This method is called on validation.
    def validate_fields_presence
      # It should have only zero (non-polymorphic) or
      # one (polymorphic) ancestor inheriting from Clear::Model
      {% for ancestors in @type.ancestors %}{% if ancestors < Clear::Model %}
        super
      {% end %}{% end %}

      {% for name, settings in COLUMNS %}
        unless persisted?
          if @{{settings[:crystal_variable_name]}}_column.failed_to_be_present?
            add_error({{settings[:crystal_variable_name].stringify}}, "must be present")
          end
        end
      {% end %}
    end

    # Reset the `changed?` flag on all columns
    #
    # The model behave like its not dirty anymore
    # and call to save would apply no changes.
    #
    # Returns `self`
    def clear_change_flags
      {% for name, settings in COLUMNS %}
        @{{settings[:crystal_variable_name]}}_column.clear_change_flag
      {% end %}

      self
    end

    # Return a hash version of the columns of this model.
    def to_h(full = false) : Hash(String, ::Clear::SQL::Any)
      out = super

      {% for name, settings in COLUMNS %}
        if full || @{{settings[:crystal_variable_name]}}_column.defined?
          out[{{settings[:db_column_name]}}] = @{{settings[:crystal_variable_name]}}_column.to_sql_value(nil)
        end
      {% end %}

      out
    end

    def to_json(emit_nulls : Bool = false)
      JSON.build{ |json| to_json(json, emit_nulls) }
    end

    def to_json(json, emit_nulls = false)
      json.object do
        {% for name, settings in COLUMNS %}
        if emit_nulls || @{{settings[:crystal_variable_name]}}_column.defined?
          json.field {{settings[:db_column_name]}} do
            @{{settings[:crystal_variable_name]}}_column.value(nil).to_json(json)
          end
        end
        {% end %}
      end
    end

    # Return `true` if the model is dirty (e.g. one or more fields
    #   have been changed.). Return `false` otherwise.
    def changed?
      {% for name, settings in COLUMNS %}
          return true if @{{settings[:crystal_variable_name]}}_column.changed?
      {% end %}

      return false
    end

  end
end
