require 'ar_pg_array/parser'
module ActiveRecord
  module ConnectionAdapters
    class PostgreSQLColumn < Column #:nodoc:
      include PgArrayParser
      extend PgArrayParser

      BASE_TYPE_COLUMNS = Hash.new{|h, base_type| 
        base_column= new(nil, nil, base_type.to_s, true)
        h[base_type] = h[base_column.type]= base_column
      }
      attr_reader :base_column

      def initialize(name, default, sql_type = nil, null = true)
        if sql_type =~ /^(.+)\[\]$/
          @base_sql_type = $1
          @base_column = BASE_TYPE_COLUMNS[@base_sql_type]
        end
        super(name, self.class.extract_value_from_default(default), sql_type, null)
      end

      def simplified_type_with_postgresql_arrays(field_type)
        if field_type=~/^(.+)\[\]$/
          :"#{simplified_type_without_postgresql_arrays($1)}_array"
        else
          simplified_type_without_postgresql_arrays(field_type)
        end
      end
      alias_method_chain :simplified_type, :postgresql_arrays

      def klass
        if type.to_s =~ /_array$/
          Array
        else
          super
        end
      end

      def type_cast(value)
        return nil if value.nil?
        case type
          when :integer_array, :float_array 
            string_to_num_array(value)
          when :decimal_array, :date_array, :boolean_array
            safe_string_to_array(value)
          when :timestamp_array, :time_array, :datetime_array, :binary_array
            string_to_array(value)
          when :text_array, :string_array
            string_to_text_array(value)
          else super
        end
      end

      def type_cast_code(var_name)
        case type
          when :integer_array, :float_array
            "#{self.class.name}.string_to_num_array(#{var_name})"
          when :decimal_array, :date_array, :boolean_array
            "#{self.class.name}.safe_string_to_array(#{var_name}, #{@base_sql_type.inspect})"
          when :timestamp_array, :time_array, :datetime_array, :binary_array
            "#{self.class.name}.string_to_array(#{var_name}, #{@base_sql_type.inspect})"
          when :text_array, :string_array
            "#{self.class.name}.string_to_text_array(#{var_name})"
          else super
        end
      end

      def default
        res = super
        Array === res ? res.dup : res
      end

      def self._string_to_array(string)
        return string unless string.is_a? String
        return nil if string.empty?

        yield
      end

      def _string_to_array(string)
        return string unless string.is_a? String
        return nil if string.empty?

        yield
      end

      def safe_string_to_array(string)
        _string_to_array(string) do
          parse_safe_pgarray(string){|v| @base_column.type_cast(v)}
        end
      end

      def self.safe_string_to_array(string, sql_type)
        _string_to_array(string) do
          base_column = BASE_TYPE_COLUMNS[sql_type]
          parse_safe_pgarray(string){|v| base_column.type_cast(v)}
        end
      end

      def string_to_array(string)
        _string_to_array(string) do
          parse_pgarray(string){|v| @base_column.type_cast(v)}
        end
      end

      def self.string_to_array(string, sql_type)
        _string_to_array(string) do
          base_column = BASE_TYPE_COLUMNS[sql_type]
          parse_pgarray(string){|v| base_column.type_cast(v)}
        end
      end

      def string_to_num_array(string)
        _string_to_array(string) do
          parse_numeric_pgarray(string)
        end
      end

      def self.string_to_num_array(string)
        _string_to_array(string) do
          parse_numeric_pgarray(string)
        end
      end

      def string_to_text_array(string)
        _string_to_array(string) do
          parse_pgarray(string){|v| v}
        end
      end

      def self.string_to_text_array(string)
        _string_to_array(string) do
          parse_pgarray(string){|v| v}
        end
      end
    end

    class PostgreSQLAdapter #:nodoc:
      include PgArrayParser
      def quote_with_postgresql_arrays(value, column = nil)
        if Array === value && column && "#{column.type}" =~ /^(.+)_array$/
          quote_array_by_base_type(value, $1, column)
        else
          quote_without_postgresql_arrays(value, column)
        end
      end
      alias_method_chain :quote, :postgresql_arrays

      def quote_array_by_base_type(value, base_type, column = nil)
        case base_type.to_sym
        when :integer, :float, :decimal, :boolean, :date, :safe, :datetime, :timestamp, :time
          "'#{ prepare_array_for_arel_by_base_type(value, base_type) }'"
        when :string, :text, :other
          pa = prepare_array_for_arel_by_base_type(value, base_type)
          "'#{ quote_string( pa ) }'"
        else
          "'#{ prepare_pg_string_array(value, base_type, column) }'"
        end
      end

      def prepare_array_for_arel_by_base_type(value, base_type)
        case base_type.to_sym
          when :integer
            prepare_pg_integer_array(value)
          when :float
            prepare_pg_float_array(value)
          when :string, :text, :other
            prepare_pg_text_array(value)
          when :datetime, :timestamp, :time
            prepare_pg_string_array(value, base_type)
          when :decimal, :boolean, :date, :safe
            prepare_pg_safe_array(value)
          else
            raise "Unsupported array base type #{base_type} for arel"
        end
      end

      def prepare_pg_string_array(value, base_type, column=nil)
        base_column= if column
                       column.base_column
                     else
                       PostgreSQLColumn::BASE_TYPE_COLUMNS[base_type.to_sym]
                     end
        _prepare_pg_string_array(value){|v| quote_without_postgresql_arrays(v, base_column)}
      end

      NATIVE_DATABASE_TYPES.keys.each do |key|
        unless key==:primary_key
          base = NATIVE_DATABASE_TYPES[key].dup
          base[:name] = base[:name]+'[]'
          NATIVE_DATABASE_TYPES[:"#{key}_array"]= base
          TableDefinition.class_eval <<-EOV
            def #{key}_array(*args)                                             # def string_array(*args)
              options = args.extract_options!                                   #   options = args.extract_options!
              column_names = args                                               #   column_names = args
                                                                                #
              column_names.each { |name| column(name, :'#{key}_array', options) }#   column_names.each { |name| column(name, 'string_array', options) }
            end                                                                 # end
          EOV
          Table.class_eval <<-EOV
            def #{key}_array(*args)                                             # def string_array(*args)
              options = args.extract_options!                                   #   options = args.extract_options!
              column_names = args                                               #   column_names = args
                                                                                #
              column_names.each { |name|                                        #   column_names.each { |name|
                @base.add_column(@table_name, name, :'#{key}_array', options)    #     @base.add_column(@table_name, name, 'string_array', options) }
              }                                                                 #   }
            end                                                                 # end
          EOV
        end
      end

      def add_column_with_postgresql_arrays( table, column, type, options = {} )
        if type.to_s =~ /^(.+)_array$/ && options[:default].is_a?(Array)
          options = options.merge(:default => prepare_array_for_arel_by_base_type(options[:default], $1))
        end
        add_column_without_postgresql_arrays( table, column, type, options )
      end
      alias_method_chain :add_column, :postgresql_arrays

      def type_to_sql_with_postgresql_arrays(type, limit = nil, precision = nil, scale = nil)
        if type.to_s =~ /^(.+)_array$/
          type_to_sql_without_postgresql_arrays($1.to_sym, limit, precision, scale)+'[]'
        else
          type_to_sql_without_postgresql_arrays(type, limit, precision, scale)
        end
      end

      alias_method_chain :type_to_sql, :postgresql_arrays

      if method_defined?(:type_cast)
        def type_cast_with_postgresql_arrays(value, column)
          if Array === value && column && "#{column.type}" =~ /^(.+)_array$/
            prepare_array_for_arel_by_base_type(value, $1)
          else
            type_cast_without_postgresql_arrays(value, column)
          end
        end
        alias_method_chain :type_cast, :postgresql_arrays
      end
    end
  end
end

