module Padrino
  module Admin
    ##
    # Contains all admin related generator functionality.
    #
    module Generators
      # Defines a generic exception for the admin ORM handler.
      class OrmError < StandardError; end

      ##
      # Defines the generic ORM management functions used to manipulate data for admin.
      class Orm
        attr_reader :klass_name, :klass, :name_plural, :name_singular, :orm, :name_param

        def initialize(name, orm, columns=nil, column_fields=nil)
          name            = name.to_s
          @klass_name     = name.underscore.camelize
          @klass          = @klass_name.constantize rescue nil
          @name_param     = name.underscore.gsub(/\//, '_')
          @name_singular  = name.underscore.gsub(/^.*\//, '') # convert submodules i.e. FooBar::Jank.all # => jank
          @name_plural    = @name_singular.pluralize
          @orm            = orm.to_sym
          @columns        = columns
          @column_fields  = column_fields
          raise OrmError, "Model '#{klass_name}' could not be found!\nPerhaps you would like to run 'bundle exec padrino g model #{klass_name}' to create it first?" if @columns.nil? && @klass.nil?
        end

        def activerecord?
          case orm
          when :activerecord, :minirecord then true
          else false
          end
        end

        def field_type(type)
          type = :string if type.nil? # couchrest-Hack to avoid the next line to fail
          type = type.to_s.demodulize.downcase.to_sym unless type.is_a?(Symbol)
          case type
            when :integer, :float, :decimal   then :text_field
            when :string                      then :text_field
            when :text                        then :text_area
            when :boolean                     then :check_box
            else :text_field
          end
        end

        Column = Struct.new(:name, :type) # for compatibility

        def columns
          @columns ||= case orm
            when :activerecord then @klass.columns
            when :minirecord   then @klass.columns
            when :datamapper   then @klass.properties.map { |p| dm_column(p) }
            when :couchrest    then @klass.properties
            when :mongoid      then @klass.fields.values.reject { |col| %w[_id _type].include?(col.name) }
            when :mongomapper  then @klass.keys.values.reject { |key| key.name == "_id" } # On MongoMapper keys are an hash
            when :sequel       then @klass.db_schema.map { |k,v| v[:type] = :text if v[:db_type] =~ /^text/i; Column.new(k, v[:type]) }
            when :ohm          then @klass.attributes.map { |a| Column.new(a.to_s, :string) } # ohm has strings
            when :dynamoid     then @klass.attributes.map { |k,v| Column.new(k.to_s, v[:type]) }
            else raise OrmError, "Adapter #{orm} is not yet supported!"
          end
        end

        def dm_column(p)
          case p
          when DataMapper::Property::Text
            Column.new(p.name, :text)
          when DataMapper::Property::Boolean
            Column.new(p.name, :boolean)
          when DataMapper::Property::Integer
            Column.new(p.name, :integer)
          when DataMapper::Property::Decimal
            Column.new(p.name, :decimal)
          when DataMapper::Property::Float
            Column.new(p.name, :float)
          when DataMapper::Property::String
            Column.new(p.name, :string)
          else #if all fails, lets assume its stringish
            Column.new(p.name, :string)
          end
        end

        def column_fields
          excluded_columns = %w[created_at updated_at]
          case orm
            when :mongoid then excluded_columns << '_id'
            else excluded_columns << 'id'
          end

          column_fields    = columns.dup
          column_fields.reject! { |column| excluded_columns.include?(column.name.to_s) }
          @column_fields ||= column_fields.map do |column|
            { :name => column.name, :field_type => field_type(column.type) }
          end
        end

        def all
          "#{klass_name}.all"
        end

        def find(params=nil)
          case orm
            when :activerecord, :minirecord, :mongomapper, :mongoid, :dynamoid then "#{klass_name}.find(#{params})"
            when :datamapper, :couchrest then "#{klass_name}.get(#{params})"
            when :sequel, :ohm then "#{klass_name}[#{params}]"
            else raise OrmError, "Adapter #{orm} is not yet supported!"
          end
        end

        def build(params=nil)
          if params
            "#{klass_name}.new(#{params})"
          else
            "#{klass_name}.new"
          end
        end

        def save
          case orm
            when :sequel then "(@#{name_singular}.save rescue false)"
            else "@#{name_singular}.save"
          end
        end

        def update_attributes(params=nil)
          case orm
            when :mongomapper, :mongoid, :couchrest, :dynamoid then "@#{name_singular}.update_attributes(#{params})"
            when :activerecord, :minirecord, :datamapper, :ohm then "@#{name_singular}.update(#{params})"
            when :sequel then "@#{name_singular}.modified! && @#{name_singular}.update(#{params})"
            else raise OrmError, "Adapter #{orm} is not yet supported!"
          end
        end

        def destroy
          case orm
            when :ohm then "#{name_singular}.delete"
            else "#{name_singular}.destroy"
          end
        end

        def find_by_ids(params=nil)
          case orm
            when :ohm then "#{klass_name}.fetch(#{params})"
            when :datamapper then "#{klass_name}.all(:id => #{params})"
            when :sequel then "#{klass_name}.where(:id => #{params})"
            when :mongoid then "#{klass_name}.find(#{params})"
            when :couchrest then "#{klass_name}.all(:keys => #{params})"
            when :dynamoid then "#{klass_name}.find(#{params})"
            else find(params)
          end
        end

        def multiple_destroy(params=nil)
          case orm
            when :ohm then "#{params}.each(&:delete)"
            when :sequel then  "#{params}.destroy"
            when :datamapper then "#{params}.destroy"
            when :couchrest, :mongoid, :mongomapper, :dynamoid then "#{params}.each(&:destroy)"
            else "#{klass_name}.destroy #{params}"
          end
        end

        def has_error(field)
          case orm
            when :datamapper, :ohm, :sequel then "@#{name_singular}.errors.key?(:#{field}) && @#{name_singular}.errors[:#{field}].count > 0"
            else "@#{name_singular}.errors.include?(:#{field})"
          end
        end
      end
    end
  end
end
