class Permalink
  include Mongoid::Document
  include Mongoid::Timestamps

  belongs_to :linkable, polymorphic: true

  field :value
  field :scope, type: Array
  field :_current, type: Boolean, default: true

  before_validation :sanitize_value!
  after_save :unset_other_current, if: :current?
  after_destroy :set_last_current, if: :current?

  validates :value, :linkable, presence: true

  index({value: 1}, {sparse: true})

  # Sanitizes and increments string, if necessary.
  def sanitize_value!
    return true unless value_changed? || new_record?
    string = sanitize(value)
    if string != value_was
      string = increment(string)
    end
    self.value = string
    true
  end

  def scope=(scope)
    if array = scope
      array = self.class.scope_list(scope)
      self.write_attribute(:scope, array)
    end
    array
  end

  # Returns true if this permalink is the current one
  # of the assigned linkable.
  def current?
    !!_current
  end

  # Returns the current permalink of the assigned linkable.
  def current
    @current ||= begin
      if current?
        self
      else
        Permalink.where(linkable_id: linkable.id, _current: true).first
      end
    end
  end

  # Sets this permalink as the current one.
  # All other permalinks of this linkable will be updated after saving.
  def current!
    self._current = true
  end

  class << self
    # Scope method for finding Permalinks for given object.
    def for_linkable(object)
      where(linkable_id: object.id)
    end

    # Scope method for finding Permalinks for given value.
    # The value will be sanitized.
    def for_value(value)
      where(value: sanitize(value))
    end

    def for_scope(scope)
      return all unless scope
      all_in(scope: scope_list(scope))
    end

    # Returns a dispatcher object for given path.
    def dispatch(path, options = {})
      Vidibus::Permalink::Dispatcher.new(path, options)
    end

    # Sanitizes string: Remove stopwords and format as permalink.
    # See Vidibus::CoreExtensions::String for details.
    def sanitize(string)
      return if string.blank?
      remove_stopwords(string).permalink
    end

    def scope_list(scope)
      return [] unless scope
      return scope if scope.kind_of?(Array)
      scope.map { |key, value| "#{key}:#{value}" }
    end
  end

  protected

  # Removes stopwords and turns string into a permalink-formatted one.
  # However, if the stopwords-free value is blank or it already exists
  # in the database, the full value will be used.
  def sanitize(string)
    return if string.blank?
    clean = Permalink.remove_stopwords(string)
    unless clean.blank? or clean == string
      clean = clean.permalink
      sanitized = clean unless existing(clean).any?
    end
    sanitized || string.permalink
  end

  # Tries to remove stopwords from string.
  # If the resulting string is blank, the original one will be returned.
  # See Vidibus::Words for details.
  def self.remove_stopwords(string)
    words = Vidibus::Words.new(string)
    clean = words.keywords(10).join(" ")
    clean.blank? ? string : clean
  end

  # Appends next available number to string if it's already in use.
  def increment(string)
    _existing = existing(string)
    return string unless _existing.any?
    return string unless _existing.detect {|e| e.value == string}
    number = 1
    while true
      number += 1
      desired = "#{string}-#{number}"
      unless _existing.detect {|e| e.value == desired}
        return desired
      end
    end
  end

  # Finds existing permalinks with current value.
  def existing(string)
    @existing ||= {}
    @existing[string] ||= Permalink
      .for_scope(scope)
      .where(value: /^#{string}(-\d+)?$/)
      .excludes(:_id => id)
      .to_a
  end

  # Sets _current to false on all permalinks of the assigned linkable.
  def unset_other_current
    return unless linkable
    conditions = {linkable_id: linkable.id, _id: {'$ne' => id}}
    conditions[:scope] = Permalink.scope_list(scope) if scope.present?
    Permalink.where(conditions).each do |obj|
      obj.set(_current: false)
    end
  end

  # Sets the lastly updated permalink of the assigned linkable as current one.
  def set_last_current
    last = Permalink
      .where(linkable_id: linkable.id)
      .order_by(:updated_at.desc)
      .limit(1)
      .first
    if last
      last.update_attributes!(_current: true)
    end
  end
end
