require 'stringio'
require 'tempfile'
require 'pathname'
require 'fileutils'

module Dragonfly

  # A TempObject is used for HOLDING DATA.
  # It's the thing that is passed between the datastore and the processor, and is useful
  # for separating how the data was created and how it is later accessed.
  #
  # You can initialize it various ways:
  #
  #   temp_object = Dragonfly::TempObject.new('this is the content')           # with a String
  #   temp_object = Dragonfly::TempObject.new(Pathname.new('path/to/content')) # with a Pathname
  #   temp_object = Dragonfly::TempObject.new(File.new('path/to/content'))     # with a File
  #   temp_object = Dragonfly::TempObject.new(some_tempfile)                   # with a Tempfile
  #   temp_object = Dragonfly::TempObject.new(some_other_temp_object)          # with another TempObject
  #
  # However, no matter how it was initialized, you can always access the data a number of ways:
  #
  #   temp_object.data      # returns a data string
  #   temp_object.file      # returns a file object holding the data
  #   temp_object.path      # returns a path for the file
  #
  # The data/file are created lazily, something which you may wish to take advantage of.
  #
  # For example, if a TempObject is initialized with a file, and temp_object.data is never called, then
  # the data string will never be loaded into memory.
  #
  # Conversely, if the TempObject is initialized with a data string, and neither temp_object.file nor temp_object.path
  # are ever called, then the filesystem will never be hit.
  #
  class TempObject

    # Exceptions
    class Closed < RuntimeError; end

    # Instance Methods

    def initialize(obj, name=nil)
      if obj.is_a? TempObject
        @data = obj.get_data
        @tempfile = obj.get_tempfile
        @pathname = obj.get_pathname
      elsif obj.is_a? String
        @data = obj
      elsif obj.is_a? Tempfile
        @tempfile = obj
      elsif obj.is_a? File
        @pathname = Pathname.new(obj.path)
      elsif obj.is_a? Pathname
        @pathname = obj
      elsif obj.respond_to?(:tempfile)
        @tempfile = obj.tempfile
      elsif obj.respond_to?(:path) # e.g. Rack::Test::UploadedFile
        @pathname = Pathname.new(obj.path)
      else
        raise ArgumentError, "#{self.class.name} must be initialized with a String, a Pathname, a File, a Tempfile, another TempObject, something that responds to .tempfile, or something that responds to .path - you gave #{obj.inspect}"
      end

      @tempfile.close if @tempfile

      # Name
      @name = if name
        name
      elsif obj.respond_to?(:original_filename)
        obj.original_filename
      elsif @pathname
        @pathname.basename.to_s
      end
    end

    attr_reader :name

    def ext
      if n = name
        n.split('.').last
      end
    end

    def data
      raise Closed, "can't read data as TempObject has been closed" if closed?
      @data ||= file{|f| f.read }
    end

    def file(&block)
      f = tempfile.open
      tempfile.binmode
      if block_given?
        ret = yield f
        tempfile.close unless tempfile.closed?
      else
        ret = f
      end
      ret
    end

    def path
      @pathname ? @pathname.expand_path.to_s : tempfile.path
    end

    def size
      @tempfile && @tempfile.size ||
      @data && @data.bytesize ||
      File.size(path)
    end

    def each(&block)
      to_io do |io|
        while part = io.read(block_size)
          yield part
        end
      end
    end

    def to_file(path, opts={})
      mode = opts[:mode] || 0644
      prepare_path(path) unless opts[:mkdirs] == false
      if @data
        File.open(path, 'wb', mode){|f| f.write(@data) }
      else
        FileUtils.cp(self.path, path)
        File.chmod(mode, path)
      end
      File.new(path, 'rb')
    end

    def to_tempfile
      tempfile = copy_to_tempfile(path)
      tempfile.open
      tempfile
    end

    def to_io(&block)
      @data ? StringIO.open(@data, 'rb', &block) : file(&block)
    end

    def close
      @tempfile.close! if @tempfile
      @data = nil
      @closed = true
    end

    def closed?
      !!@closed
    end

    def inspect
      content_string = case
      when @data
        data_string = size > 20 ? "#{@data[0..20]}..." : @data
        "data=#{data_string.inspect}"
      when @pathname then "pathname=#{@pathname.inspect}"
      when @tempfile then "tempfile=#{@tempfile.inspect}"
      end
      "<#{self.class.name} #{content_string} >"
    end

    protected

    # We don't use normal accessors here because #data etc. do more than just return the instance var
    def get_data
      @data
    end

    def get_pathname
      @pathname
    end

    def get_tempfile
      @tempfile
    end

    private

    def tempfile
      raise Closed, "can't read from tempfile as TempObject has been closed" if closed?
      @tempfile ||= begin
        case
        when @data
          @tempfile = Utils.new_tempfile(ext, @data)
        when @pathname
          @tempfile = copy_to_tempfile(@pathname.expand_path)
        end
        @tempfile
      end
    end

    def block_size
      8192
    end

    def copy_to_tempfile(path)
      tempfile = Utils.new_tempfile(ext)
      FileUtils.cp path, tempfile.path
      tempfile
    end

    def prepare_path(path)
      dir = File.dirname(path)
      FileUtils.mkdir_p(dir) unless File.exist?(dir)
    end

  end
end
