require 'fileutils'
require 'rugged'

module ActsAsGit
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  def self.configure
    ClassMethods.module_eval do
      yield self
    end
  end

  # ToDo: rename if filename is changed
  module ClassMethods
    def self.email=(email)
      @@email = email
    end

    def self.username=(username)
      @@username = username
    end

    def get_option(index)
      option = {}
      option[:author] = { :email => @@email, :name => @@username, :time => Time.now }
      option[:committer] = { :email => @@email, :name => @@username, :time => Time.now }
      option[:parents] = @@repo.empty? ? [] : [ @@repo.head.target ].compact
      option[:tree] = index.write_tree(@@repo)
      option[:update_ref] = 'HEAD'
      option
    end

    # acts_as_git :field => self.instance_method(:filename)
    def acts_as_git(params = {})
      self.class_eval do
        unless method_defined?(:save_with_file)
          def self.path(filename)
            File.join(repodir, filename)
          end

          def commit
            if @commit
              @commit
            else
             (@@repo.empty?)? nil: @@repo.head.target
            end
          end

          def is_changed?
            (@is_changed)? true: false
          end

          def get_commit
            (commit)? commit.oid: nil
          end

          define_method :checkout do |commit|
            @commit = (commit)? @@repo.lookup(commit): nil
            @is_changed = false
            params.each do |field, filename_instance_method|
              field = nil
            end
            self
          end

          repodir = self.repodir
          FileUtils.mkdir_p(repodir)
          begin
            @@repo = Rugged::Repository.new(repodir)
          rescue
            Rugged::Repository.init_at(repodir)
            @@repo = Rugged::Repository.new(repodir)
          end

          define_method(:save_with_file) do |*args|
            params.each do |field, filename_instance_method|
              field_name = :"@#{field}"
              repodir = self.class.repodir
              filename = filename_instance_method.bind(self).call
              content  = instance_variable_get(field_name)
              if repodir and filename and content
                oid = @@repo.write(content, :blob)
                index = @@repo.index
                path = self.class.path(filename)
                action = File.exists?(path)? 'Update': 'Create'
                FileUtils.mkdir_p(File.dirname(path))
                File.open(path, 'w') do |f|
                  f.write(content)
                end
                index.add(path: filename, oid: oid, mode: 0100644)
                option = self.class.get_option(index)
                option[:message] = "#{action} #{filename} for field #{field_name} of #{self.class.name}"
                Rugged::Commit.create(@@repo, option)
                @commit = @@repo.head.target
                @is_changed = false
                index.read_tree(@commit.tree)
                index.write
                instance_variable_set(field_name, nil)
              end
            end
            save_without_file(*args)
          end

          define_method(:save) {|*args| } unless method_defined?(:save)
          alias_method :save_without_file, :save
          alias_method :save, :save_with_file

          params.each do |field, filename_instance_method|
            field_name = :"@#{field}"
            define_method("#{field}=") do |content|
              instance_variable_set(field_name, content)
              @is_changed = true
            end
          end

          params.each do |field, filename_instance_method|
            field_name = :"@#{field}"
            define_method(field) do |offset = nil, length = nil|
              if @is_changed
                return instance_variable_get(field_name)
              end
              filename = filename_instance_method.bind(self).call
              return nil unless repodir
              return nil unless filename
              return nil if @@repo.empty?
              return nil unless fileob = commit.tree.path(filename)
              oid = fileob[:oid]
              file = StringIO.new(@@repo.lookup(oid).content)
              file.seek(offset) if offset
              file.read(length)
            end
          end

          define_method(:destroy_with_file) do
            params.each do |field, filename_instance_method|
              field_name = :"@#{field}"
              filename = filename_instance_method.bind(self).call
              index = @@repo.index
              path = self.class.path(filename)
              File.unlink(path) if File.exists?(path)
              begin
                index.remove(filename)
                option = self.class.get_option(index)
                option[:message] = "Remove #{filename} for field #{field_name} of #{self.class.name}"
                Rugged::Commit.create(@@repo, option)
                @commit = @@repo.head.target
                @is_changed = false
                index.read_tree(@commit.tree)
                index.write
              rescue Rugged::IndexError => e
              end
            end
            destroy_without_file
          end
          define_method(:destroy) {} unless method_defined?(:destroy)
          alias_method :destroy_without_file, :destroy
          alias_method :destroy, :destroy_with_file
        end
      end
    end
  end
end