module DBF
  module Column
    # Raised if length is less than 1
    class LengthError < StandardError; end

    # Raised if name is empty
    class NameError < StandardError; end

    class Base
      attr_reader :name, :type, :length, :decimal

      # Initialize a new DBF::Column
      #
      # @param [String] name
      # @param [String] type
      # @param [Fixnum] length
      # @param [Fixnum] decimal
      def initialize(name, type, length, decimal, version, encoding=nil)
        @name, @type, @length, @decimal, @version, @encoding = clean(name), type, length, decimal, version, encoding

        raise LengthError, "field length must be greater than 0" unless length > 0
        raise NameError, "column name cannot be empty" if @name.length == 0
      end

      # Cast value to native type
      #
      # @param [String] value
      # @return [Fixnum, Float, Date, DateTime, Boolean, String]
      def type_cast(value)
        case type
          when 'N' then unpack_number(value)
          when 'I' then unpack_unsigned_long(value)
          when 'F' then value.to_f
          when 'Y' then unpack_unsigned_long(value) / 10000.0
          when 'D' then decode_date(value)
          when 'T' then decode_datetime(value)
          when 'L' then boolean(value)
          when 'M' then decode_memo(value)
          else          encode_string(value.to_s).strip
        end
      end

      def memo?
        @memo ||= type == 'M'
      end

      # Schema definition
      #
      # @return [String]
      def schema_definition
        "\"#{underscored_name}\", #{schema_data_type}\n"
      end

      def self.underscore_name(string)
        string.gsub(/::/, '/').
          gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
          gsub(/([a-z\d])([A-Z])/,'\1_\2').
          tr('-', '_').
          downcase
      end

      def underscored_name
        @underscored_name ||= self.class.underscore_name(name)
      end

      private

      def decode_date(value) #nodoc
        value.gsub!(' ', '0')
        value !~ /\S/ ? nil : Date.parse(value)
      rescue
        nil
      end

      def decode_datetime(value) #nodoc
        days, milliseconds = value.unpack('l2')
        seconds = (milliseconds / 1000).to_i
        DateTime.jd(days, (seconds/3600).to_i, (seconds/60).to_i % 60, seconds % 60) rescue nil
      end

      def decode_memo(value) #nodoc
        value && encode_string(value)
      end

      def unpack_number(value) #nodoc
        decimal.zero? ? value.to_i : value.to_f
      end

      def unpack_unsigned_long(value) #nodoc
        value.unpack('V')[0]
      end

      def boolean(value) #nodoc
        value.strip =~ /^(y|t)$/i ? true : false
      end

      def encode_string(value) #nodoc
        if @encoding
          if String.new.respond_to?(:encoding)
            value.force_encoding(@encoding).encode(Encoding.default_external, :undef => :replace, :invalid => :replace)
          else
            Iconv.conv('UTF-8', @encoding, value)
          end
        else
          value
        end
      end

      def schema_data_type #nodoc
        case type
        when "N", "F"
          decimal > 0 ? ":float" : ":integer"
        when "I"
          ":integer"
        when "Y"
          ":decimal, :precision => 15, :scale => 4"
        when "D"
          ":date"
        when "T"
          ":datetime"
        when "L"
          ":boolean"
        when "M"
          ":text"
        when "B"
          if DBF::Table::FOXPRO_VERSIONS.keys.include?(@version)
            decimal > 0 ? ":float" : ":integer"
          else
            ":text"
          end
        else
          ":string, :limit => #{length}"
        end
      end

      def clean(value) #nodoc
        first_null = value.index("\x00")
        value = value[0, first_null] if first_null
        value.gsub(/[^\x20-\x7E]/, "")
      end

    end
  end
end
