module ActiveFedora
  module Datastreams
    extend ActiveSupport::Concern

    autoload :NokogiriDatastreams, 'active_fedora/datastreams/nokogiri_datastreams'

    included do
      class_attribute :ds_specs
      self.ds_specs = {'RELS-EXT'=> {:type=> ActiveFedora::RelsExtDatastream, :label=>"Fedora Object-to-Object Relationship Metadata", :block=>nil}}
      class << self
        def inherited_with_datastreams(kls) #:nodoc:
          ## Do some inheritance logic that doesn't override Base.inherited
          inherited_without_datastreams kls
          # each subclass should get a copy of the parent's datastream definitions, it should not add to the parent's definition table.
          kls.ds_specs = kls.ds_specs.dup
        end
        alias_method_chain :inherited, :datastreams
      end

      before_save :add_disseminator_location_to_datastreams
      #before_save :serialize_datastreams
    end

    def ds_specs
      self.class.ds_specs
    end

    def serialize_datastreams
      datastreams.each {|k, ds| ds.serialize! }
    end

    # Adds the disseminator location to the datastream after the pid has been established
    def add_disseminator_location_to_datastreams
      self.ds_specs.each do |name,ds_config|
        ds = datastreams[name]
        if ds && ds.controlGroup == 'E' && ds_config[:disseminator].present?
          ds.dsLocation= "#{ActiveFedora.config_for_environment[:url]}/objects/#{pid}/methods/#{ds_config[:disseminator]}"
        end
      end
      true
    end

    #
    # Datastream Management
    #
    
    # Returns all known datastreams for the object.  If the object has been 
    # saved to fedora, the persisted datastreams will be included.
    # Datastreams that have been modified in memory are given preference over 
    # the copy in Fedora.
    def datastreams
      @datastreams ||= DatastreamHash.new(self)
    end
  
    def configure_datastream(ds, ds_spec=nil)
      ds_spec ||= self.ds_specs[ds.dsid]
      if ds_spec
        ds.model = self if ds_spec[:type] == RelsExtDatastream
        # If you called has_metadata with a block, pass the block into the Datastream class
        if ds_spec[:block].class == Proc
          ds_spec[:block].call(ds)
        end
      end
    end

    def datastream_from_spec(ds_spec, name)
      inner_object.datastream_object_for name, {}, ds_spec
    end

    def load_datastreams
      local_ds_specs = self.ds_specs.dup
      inner_object.datastreams.each do |dsid, ds|
        self.add_datastream(ds)
        configure_datastream(datastreams[dsid])
        local_ds_specs.delete(dsid)
      end
      local_ds_specs.each do |name,ds_spec|
        ds = datastream_from_spec(ds_spec, name)
        self.add_datastream(ds)
        configure_datastream(ds, ds_spec)
      end
    end      

    # Adds datastream to the object.
    # @return [String] dsid of the added datastream
    def add_datastream(datastream, opts={})
      datastreams[datastream.dsid] = datastream
      datastream.dsid
    end

    # @return [Array] all datastreams that return true for `metadata?` and are not Rels-ext
    def metadata_streams
      datastreams.select { |k, ds| ds.metadata? }.reject { |k, ds| ds.kind_of?(ActiveFedora::RelsExtDatastream) }.values
    end
    

    # Returns the RELS-EXT Datastream
    # Tries to grab from in-memory datastreams first
    # Failing that, attempts to load from Fedora and addst to in-memory datastreams
    # Failing that, creates a new RelsExtDatastream and adds it to the object
    def rels_ext
      if !datastreams.has_key?("RELS-EXT") 
        ds = ActiveFedora::RelsExtDatastream.new(@inner_object,'RELS-EXT')
        ds.model = self
        add_datastream(ds)
      end
      return datastreams["RELS-EXT"]
    end

    #
    # File Management
    #
    
    # Add the given file as a datastream in the object
    #
    # @param [File] file the file to add
    # @param [Hash] opts options: :dsid, :label, :mimeType, :prefix, :checksumType
    def add_file_datastream(file, opts={})
      label = opts.has_key?(:label) ? opts[:label] : ""
      attrs = {:dsLabel => label, :controlGroup => 'M', :blob => file, :prefix=>opts[:prefix]}
      if opts.has_key?(:mime_type)
        attrs.merge!({:mimeType=>opts[:mime_type]})
      elsif opts.has_key?(:mimeType)
        attrs.merge!({:mimeType=>opts[:mimeType]})
      elsif opts.has_key?(:content_type)
        attrs.merge!({:mimeType=>opts[:content_type]})
      end
      attrs[:checksumType] = opts[:checksumType] if opts[:checksumType]
      attrs[:versionable] = opts[:versionable] unless opts[:versionable].nil?
      ds = create_datastream(self.class.datastream_class_for_name(opts[:dsid]), opts[:dsid], attrs)
      add_datastream(ds).tap do |dsid|
        self.class.build_datastream_accessor(dsid) unless respond_to? dsid
      end
    end
    
    
    def create_datastream(type, dsid, opts={})
      klass = type.kind_of?(Class) ? type : type.constantize
      raise ArgumentError, "Argument dsid must be of type string" if dsid && !dsid.kind_of?(String)
      ds = klass.new(inner_object, dsid, prefix: opts[:prefix])
      [:mimeType, :controlGroup, :dsLabel, :dsLocation, :checksumType, :versionable].each do |key|
        ds.send("#{key}=".to_sym, opts[key]) unless opts[key].nil?
      end
      if ds.managed? || ds.inline?
        blob = opts[:blob] 
        if blob 
          if !ds.mimeType.present? 
            ##TODO, this is all done by rubydora -- remove
            ds.mimeType = blob.respond_to?(:content_type) ? blob.content_type : "application/octet-stream"
          end
          if !ds.dsLabel.present? && blob.respond_to?(:path)
            ds.dsLabel = File.basename(blob.path)
          end
        end
        ds.content = blob || "" 
      end
      ds
    end

    module ClassMethods
      # @param [String] dsid the datastream id
      # @return [Class] the class of the datastream
      def datastream_class_for_name(dsid)
        ds_specs[dsid] ? ds_specs[dsid].fetch(:type, ActiveFedora::Datastream) : ActiveFedora::Datastream
      end

      # This method is used to specify the details of a datastream. 
      # You can pass the name as the first argument and a hash of options as the second argument
      # or you can pass the :name as a value in the args hash. Either way, name is required.
      # Note that this method doesn't actually execute the block, but stores it, to be executed
      # by any the implementation of the datastream(specified as :type)
      #
      # @param [Hash] args 
      # @option args [Class] :type The class that will represent this datastream, should extend ``Datastream''
      # @option args [String] :name the handle to refer to this datastream as
      # @option args [String] :label sets the fedora label
      # @option args [String] :control_group must be one of 'E', 'X', 'M', 'R'
      # @option args [String] :disseminator Sets the disseminator location see {#add_disseminator_location_to_datastreams}
      # @option args [String] :url 
      # @option args [Boolean] :autocreate Always create this datastream on new objects
      # @option args [Boolean] :versionable Should versioned datastreams be stored
      # @yield block executed by some kinds of datastreams
      def has_metadata(*args, &block)
        @metadata_ds_defaults ||= {
          :autocreate => false,
          :type=>nil,
          :label=>"",
          :control_group=>nil,
          :disseminator=>"",
          :url=>"",
          :name=>nil
        }
        spec_datastream(args, @metadata_ds_defaults, &block)
      end

      
      # @overload has_file_datastream(name, args)
      #   Declares a file datastream exists for objects of this type
      #   @param [String] name 
      #   @param [Hash] args 
      #     @option args :type (ActiveFedora::Datastream) The class the datastream should have
      #     @option args :label ("File Datastream") The default value to put in the dsLabel field
      #     @option args :control_group ("M") The type of controlGroup to store the datastream as. Defaults to M
      #     @option args [Boolean] :autocreate Always create this datastream on new objects
      #     @option args [Boolean] :versionable Should versioned datastreams be stored
      # @overload has_file_datastream(args)
      #   Declares a file datastream exists for objects of this type
      #   @param [Hash] args 
      #     @option args :name ("content") The dsid of the datastream
      #     @option args :type (ActiveFedora::Datastream) The class the datastream should have
      #     @option args :label ("File Datastream") The default value to put in the dsLabel field
      #     @option args :control_group ("M") The type of controlGroup to store the datastream as. Defaults to M
      #     @option args [Boolean] :autocreate Always create this datastream on new objects
      #     @option args [Boolean] :versionable Should versioned datastreams be stored
      def has_file_datastream(*args)
        @file_ds_defaults ||= {
          :autocreate => false,
          :type=>ActiveFedora::Datastream,
          :label=>"File Datastream",
          :control_group=>"M",
          :name=>"content"
        }
        spec_datastream(args, @file_ds_defaults)
      end

      def build_datastream_accessor(dsid)
        name = name_for_dsid(dsid)
        define_method name do
          datastreams[dsid]
        end
        end


      private

      # Creates a datastream spec combining the given args and default values
      # @param args [Array] either [String, Hash] or [Hash]; the latter must .has_key? :name
      # @param defaults [Hash] the default values for the datastream spec
      # @yield block executed by some kinds of datastreams
      def spec_datastream(args, defaults, &block)
        if args.first.is_a? String 
          name = args.first
          args = args[1] || {}
          args[:name] = name
        else
          args = args.first || {}
        end
        spec = defaults.merge(args.select {|key, value| defaults.has_key? key})
        name = spec.delete(:name)
        raise ArgumentError, "You must provide a name (dsid) for the datastream" unless name
        raise ArgumentError, "You must provide a :type property for the datastream '#{name}'" unless spec[:type]
        spec[:versionable] = args[:versionable] if args.has_key? :versionable
        spec[:block] = block if block
        build_datastream_accessor(name)
        ds_specs[name]= spec
      end

        ## Given a dsid return a standard name
        def name_for_dsid(dsid)
          dsid.gsub('-', '_')
        end

    end

  end
end
