##
#
  class GridFS
    const_set :Version, '1.1.2'

    class << GridFS
      def version
        const_get :Version
      end

      def dependencies
        {
          'mongoid'         => [ 'mongoid'         , ' >= 3.0.1' ] ,
          'mime/types'      => [ 'mime-types'      , ' >= 1.19'  ] ,
        }
      end

      def libdir(*args, &block)
        @libdir ||= File.expand_path(__FILE__).sub(/\.rb$/,'')
        args.empty? ? @libdir : File.join(@libdir, *args)
      ensure
        if block
          begin
            $LOAD_PATH.unshift(@libdir)
            block.call()
          ensure
            $LOAD_PATH.shift()
          end
        end
      end

      def load(*libs)
        libs = libs.join(' ').scan(/[^\s+]+/)
        libdir{ libs.each{|lib| Kernel.load(lib) } }
      end
    end

    begin
      require 'rubygems'
    rescue LoadError
      nil
    end

    if defined?(gem)
      dependencies.each do |lib, dependency|
        gem(*dependency)
        require(lib)
      end
    end

    require "digest/md5"
    require "cgi"
  end

##
#
  class GridFS
    class << GridFS
      attr_accessor :namespace
      attr_accessor :file_model
      attr_accessor :chunk_model

      def init!
        GridFS.build_namespace_for(:Fs)

        GridFS.namespace = Fs
        GridFS.file_model = Fs.file_model
        GridFS.chunk_model = Fs.chunk_model

        const_set(:File, Fs.file_model)
        const_set(:Chunk, Fs.chunk_model)

        to_delegate = %w(
          put
          get
          delete
          find
          []
          []=
          clear
        )
        
        to_delegate.each do |method|
          class_eval <<-__
            def GridFS.#{ method }(*args, &block)
              ::GridFS::Fs::#{ method }(*args, &block)
            end
          __
        end
      end
    end

  ##
  #
    def GridFS.namespace_for(prefix)
      prefix = prefix.to_s.downcase
      const = "::GridFS::#{ prefix.to_s.camelize }"
      namespace = const.split(/::/).last
      const_defined?(namespace) ? const_get(namespace) : build_namespace_for(namespace)
    end

  ##
  #
    def GridFS.build_namespace_for(prefix)
      prefix = prefix.to_s.downcase
      const = prefix.camelize

      namespace =
        Module.new do
          module_eval(&NamespaceMixin)
          self
        end

      const_set(const, namespace)

      file_model = build_file_model_for(namespace)
      chunk_model = build_chunk_model_for(namespace)

      file_model.namespace = namespace
      chunk_model.namespace = namespace

      file_model.chunk_model = chunk_model
      chunk_model.file_model = file_model

      namespace.prefix = prefix
      namespace.file_model = file_model
      namespace.chunk_model = chunk_model

      namespace.send(:const_set, :File, file_model)
      namespace.send(:const_set, :Chunk, chunk_model)

      #at_exit{ file_model.create_indexes rescue nil }
      #at_exit{ chunk_model.create_indexes rescue nil }

      const_get(const)
    end

    NamespaceMixin = proc do
      class << self
        attr_accessor :prefix
        attr_accessor :file_model
        attr_accessor :chunk_model

        def to_s
          prefix
        end

        def namespace
          prefix
        end

        def put(readable, attributes = {})
          chunks = []
          file = file_model.new
          attributes.to_options!

          if attributes.has_key?(:id)
            file.id = attributes.delete(:id)
          end

          if attributes.has_key?(:_id)
            file.id = attributes.delete(:_id)
          end

          if attributes.has_key?(:content_type)
            attributes[:contentType] = attributes.delete(:content_type)
          end

          if attributes.has_key?(:upload_date)
            attributes[:uploadDate] = attributes.delete(:upload_date)
          end

          md5 = Digest::MD5.new
          length = 0
          chunkSize = file.chunkSize
          n = 0

          GridFS.reading(readable) do |io|

            filename =
              attributes[:filename] ||=
                [file.id.to_s, GridFS.extract_basename(io)].join('/').squeeze('/')

            content_type =
              attributes[:contentType] ||=
                GridFS.extract_content_type(filename) || file.contentType

            while((buf = io.read(chunkSize)))
              md5 << buf
              length += buf.size
              chunk = file.chunks.build
              chunk.data = binary_for(buf)
              chunk.n = n
              n += 1
              chunk.save!
              chunks.push(chunk)
            end

          end

          attributes[:length] ||= length
          attributes[:uploadDate] ||= Time.now.utc
          attributes[:md5] ||= md5.hexdigest

          file.update_attributes(attributes)

          file.save!
          file
        ensure
          chunks.each{|chunk| chunk.destroy rescue nil} if $!
        end

        if defined?(Moped)
          def binary_for(*buf)
            Moped::BSON::Binary.new(:generic, buf.join)
          end
        else
          def binary_for(buf)
            BSON::Binary.new(buf.bytes.to_a)
          end
        end

        def get(id)
          file_model.find(id)
        end

        def delete(id)
          file_model.find(id).destroy
        rescue
          nil
        end

        def where(conditions = {})
          case conditions
            when String
              file_model.where(:filename => conditions)
            else
              file_model.where(conditions)
          end
        end

        def find(*args)
          where(*args).first
        end

        def [](filename)
          file_model.where(:filename => filename.to_s).first
        end

        def []=(filename, readable)
          file = self[filename]
          file.destroy if file
          put(readable, :filename => filename.to_s)
        end

        def clear
          file_model.destroy_all
        end

      # TODO - opening with a mode = 'w' should return a GridIO::IOProxy
      # implementing a StringIO-like interface
      #
        def open(filename, mode = 'r', &block)
          raise NotImplementedError
        end
      end
    end

  ##
  #
    def GridFS.build_file_model_for(namespace)
      prefix = namespace.name.split(/::/).last.downcase
      file_model_name = "#{ namespace.name }::File"
      chunk_model_name = "#{ namespace.name }::Chunk"

      Class.new do
        include Mongoid::Document

        singleton_class = class << self; self; end

        singleton_class.instance_eval do
          define_method(:name){ file_model_name }
          attr_accessor :chunk_model
          attr_accessor :namespace
        end

        self.default_collection_name = "#{ prefix }.files"

        field(:filename, :type => String)
        field(:contentType, :type => String, :default => 'application/octet-stream')

        field(:length, :type => Integer, :default => 0)
        field(:chunkSize, :type => Integer, :default => (256 * (2 ** 20)))
        field(:uploadDate, :type => Date, :default => Time.now.utc)
        field(:md5, :type => String, :default => Digest::MD5.hexdigest(''))

        %w( filename contentType length chunkSize uploadDate md5 ).each do |f|
          validates_presence_of(f)
        end
        validates_uniqueness_of(:filename)

        has_many(:chunks, :class_name => chunk_model_name, :inverse_of => :files, :dependent => :destroy, :order => [:n, :asc])

        index({:filename => 1}, :unique => true) 

        def path
          filename
        end

        def basename
          ::File.basename(filename)
        end

        def prefix
          self.class.namespace.prefix
        end

        def each(&block)
          chunks.all.order_by([:n, :asc]).each do |chunk|
            block.call(chunk.to_s)
          end
        end

        def data
          data = ''
          each{|chunk| data << chunk}
          data
        end

        def base64
          Array(to_s).pack('m')
        end

        def data_uri(options = {})
          data = base64.chomp
          "data:#{ content_type };base64,".concat(data)
        end

        def bytes(&block)
          if block
            each{|data| block.call(data)}
            length
          else
            bytes = []
            each{|data| bytes.push(*data)}
            bytes
          end
        end

        def close
          self
        end

        def content_type
          contentType
        end

        def update_date 
          updateDate
        end

        def created_at
          updateDate
        end

        def namespace
          self.class.namespace
        end
      end
    end

  ##
  #
    def GridFS.build_chunk_model_for(namespace)
      prefix = namespace.name.split(/::/).last.downcase
      file_model_name = "#{ namespace.name }::File"
      chunk_model_name = "#{ namespace.name }::Chunk"

      Class.new do
        include Mongoid::Document

        singleton_class = class << self; self; end

        singleton_class.instance_eval do
          define_method(:name){ chunk_model_name }
          attr_accessor :file_model
          attr_accessor :namespace
        end

        self.default_collection_name = "#{ prefix }.chunks"

        field(:n, :type => Integer, :default => 0)
        field(:data, :type => (defined?(Moped) ? Moped::BSON::Binary : BSON::Binary))

        belongs_to(:file, :foreign_key => :files_id, :class_name => file_model_name)

        index({:files_id => 1, :n => -1}, :unique => true) 

        def namespace
          self.class.namespace
        end

        def to_s
          data.data
        end

        alias_method 'to_str', 'to_s'
      end
    end

  ##
  #
    def GridFS.reading(arg, &block)
      if arg.respond_to?(:read)
        rewind(arg) do |io|
          block.call(io)
        end
      else
        open(arg.to_s) do |io|
          block.call(io)
        end
      end
    end

    def GridFS.rewind(io, &block)
      begin
        pos = io.pos
        io.flush
        io.rewind
      rescue
        nil
      end

      begin
        block.call(io)
      ensure
        begin
          io.pos = pos
        rescue
          nil
        end
      end
    end

    def GridFS.extract_basename(object)
      filename = nil
      [:original_path, :original_filename, :path, :filename, :pathname].each do |msg|
        if object.respond_to?(msg)
          filename = object.send(msg)
          break
        end
      end
      filename ? cleanname(filename) : nil
    end

    def GridFS.extract_content_type(filename)
      content_type = MIME::Types.type_for(::File.basename(filename.to_s)).first
      content_type.to_s if content_type
    end

    def GridFS.cleanname(pathname)
      basename = ::File.basename(pathname.to_s)
      CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_')
    end
  end

##
#
  GridFs = GridFS

  GridFS.init!