#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++

# Provides some convenience for copying an ActiveRecord model with associations.
# The actual copying methods need to be provided, though.
# Including this Module will include Redmine::SafeAttributes as well.
module CopyModel
  module InstanceMethods
    # Copies all attributes from +from_model+
    # except those specified in self.class#not_to_copy.
    # Does NOT save self.
    def copy_attributes(from_model)
      with_model(from_model) do |model|
        # clear unique attributes
        self.attributes = model.attributes.dup.except(*Array(self.class.not_to_copy).map(&:to_s))
        return self
      end
    end

    # Copies the instance's associations based on the +from_model+.
    # The associations CAN be copied when the instance responds to
    # something called 'copy_association_name'.
    #
    # For example: If we have a method called #copy_work_packages,
    #              the WorkPackages from the work_packages association can be copied.
    #
    # Accepts an +options+ argument to specify what to copy
    #
    # Examples:
    #   model.copy_associations(1)                                    # => copies everything
    #   model.copy_associations(1, only: 'members')                # => copies members only
    #   model.copy_associations(1, only: ['members', 'versions'])  # => copies members and versions
    def copy_associations(from_model, options = {})
      to_be_copied = self.class.reflect_on_all_associations.map(&:name)
      to_be_copied = Array(options[:only]) unless options[:only].nil?

      to_be_copied = to_be_copied.map(&:to_s).sort { |a, b|
        (copy_precedence.map(&:to_s).index(a) || -1) <=> (copy_precedence.map(&:to_s).index(b) || -1)
      }.map(&:to_sym)

      with_model(from_model) do |model|
        self.class.transaction do
          to_be_copied.each do |name|
            if respond_to?(:"copy_#{name}") || private_methods.include?(:"copy_#{name}")
              reload
              begin
                send(:"copy_#{name}", model, to_be_copied)
                # Array(nil) => [], works around nil values of has_one associations
                (Array(send(name)).map do |instance|
                  compiled_errors << instance.errors unless instance.valid?
                end)
              rescue => e
                Rails.logger.error "Failed to copy association #{name}: #{e}"
                errors.add(name, :could_not_be_copied)
              end
            end
          end
          self
        end
      end
    end

    # copies everything (associations and attributes) based on
    # +from_model+ and saves the instance.
    def copy(from_model, options = {})
      save if copy_attributes(from_model) && copy_associations(from_model, options)
      self
    end

    # resolves +model+ and returns it,
    # or yields it if a block was passed
    def with_model(model)
      model = model.is_a?(self.class) ? model : self.class.find(model)
      if model
        if block_given?
          yield model
        else
          return model
        end
      end
    end

    def copy_precedence
      self.class.copy_precedence
    end

    def compiled_errors
      @compiled_errors ||= []
    end

    def compiled_errors=(errors)
      @compiled_errors = errors
    end
  end

  module ClassMethods
    # Overwrite or set CLASS::NOT_TO_COPY to specify
    # which attributes are not safe to copy.
    def not_to_copy(should_not_be_copied = nil)
      @not_to_copy ||= (should_not_be_copied || begin self::NOT_TO_COPY
            rescue NameError
              []
            end)
      @not_to_copy
    end

    def copy_precedence(precedence = nil)
      @copy_precedence ||= (precedence || begin self::COPY_PRECEDENCE
            rescue NameError
              []
            end)
      @copy_precedence
    end

    # Copies +from_model+ and returns the new instance. This will not save
    # the copy
    def copy_attributes(from_model)
      new.copy_attributes(from_model)
    end

    # Creates a new instance and
    # copies everything (associations and attributes) based on
    # +from_model+.
    def copy(from_model, options = {})
      new.copy(from_model, options)
    end
  end

  def self.included(base)
    base.send :extend,  self::ClassMethods
    base.send :include, self::InstanceMethods
  end

  def self.extended(base)
    base.send :extend,  self::ClassMethods
    base.send :include, self::InstanceMethods
  end
end
