module HasTranslations
  module ModelAdditions
    extend ActiveSupport::Concern

    module ClassMethods
      def translated(locale)
        where("#{self.has_translations_options[:translation_class].table_name}.locale = ?", locale).joins(:translations)
      end

      def has_translations(*attrs)
        new_options = attrs.extract_options!
        options = {
          fallback: false,
          reader: true,
          writer: false,
          nil: '',
          autosave: new_options[:writer],
          translation_class: nil
        }.merge(new_options)

        translation_class_name = options[:translation_class].try(:name) || "#{self.model_name}Translation"
        options[:translation_class] ||= translation_class_name.constantize

        options.assert_valid_keys(
          [
            :fallback,
            :reader,
            :writer,
            :nil,
            :inverse_of,
            :autosave,
            :translation_class,
            :foreign_key
          ]
        )

        belongs_to = self.model_name.to_s.demodulize.underscore.to_sym

        class_attribute :has_translations_options
        self.has_translations_options = options

        # associations, validations and scope definitions
        options[:translation_class].belongs_to(belongs_to, foreign_key: options[:foreign_key])
        has_many(
          :translations,
          class_name: translation_class_name,
          dependent: :destroy,
          autosave: options[:autosave],
          inverse_of: options[:inverse_of],
          foreign_key: options[:foreign_key]
        )
        options[:translation_class].validates(
          :locale,
          presence: true,
          uniqueness: {scope: (options[:foreign_key] || "#{belongs_to}_id").to_sym}
        )

        # Optionals delegated readers
        if options[:reader]
          attrs.each do |name|
            send :define_method, name do |*args|
              locale = args.first || I18n.locale
              translation = self.translation(locale)
              translation.try(name) || has_translations_options[:nil]
            end
          end
        end

        # Optionals delegated writers
        if options[:writer]
          attrs.each do |name|
            send :define_method, "#{name}_before_type_cast" do
              translation = self.translation(I18n.locale, false)
              translation.try(name)
            end

            send :define_method, "#{name}=" do |value|
              translation = find_or_build_translation(I18n.locale)
              translation.send(:"#{name}=", value)
            end
          end
        end
      end
    end

    def find_or_create_translation(locale)
      locale = locale.to_s
      (find_translation(locale) || self.has_translations_options[:translation_class].new).tap do |t|
        t.locale = locale
        t.send(:"#{self.class.model_name.to_s.demodulize.underscore.to_sym}_id=", self.id)
      end
    end

    def find_or_build_translation(locale)
      locale = locale.to_s
      (find_translation(locale) || self.translations.build).tap do |t|
        t.locale = locale
      end
    end

    def translation(locale, fallback=has_translations_options[:fallback])
      find_translation(locale) || (fallback && !translations.empty? ? find_translation(I18n.default_locale) || translations.first : nil)
    end

    def all_translations
      t = I18n.available_locales.map do |locale|
        [locale, find_or_create_translation(locale)]
      end
      ActiveSupport::OrderedHash[t]
    end

    def has_translation?(locale)
      find_translation(locale).present?
    end

    def find_translation(locale)
      locale = locale.to_s
      translations.detect { |t| t.locale == locale }
    end
  end
end
