# frozen_string_literal: true

require "paper_trail/attribute_serializers/object_changes_attribute"
require "paper_trail/queries/versions/where_object"
require "paper_trail/queries/versions/where_object_changes"

module PaperTrail
  # Originally, PaperTrail did not provide this module, and all of this
  # functionality was in `PaperTrail::Version`. That model still exists (and is
  # used by most apps) but by moving the functionality to this module, people
  # can include this concern instead of sub-classing the `Version` model.
  module VersionConcern
    extend ::ActiveSupport::Concern

    included do
      if ::ActiveRecord.gem_version >= Gem::Version.new("5.0")
        belongs_to :item, polymorphic: true, optional: true
      else
        belongs_to :item, polymorphic: true
      end

      validates_presence_of :event
      after_create :enforce_version_limit!
    end

    # :nodoc:
    module ClassMethods
      def item_subtype_column_present?
        column_names.include?("item_subtype")
      end

      def with_item_keys(item_type, item_id)
        where item_type: item_type, item_id: item_id
      end

      def creates
        where event: "create"
      end

      def updates
        where event: "update"
      end

      def destroys
        where event: "destroy"
      end

      def not_creates
        where "event <> ?", "create"
      end

      def between(start_time, end_time)
        where(
          arel_table[:created_at].gt(start_time).
          and(arel_table[:created_at].lt(end_time))
        ).order(timestamp_sort_order)
      end

      # Defaults to using the primary key as the secondary sort order if
      # possible.
      def timestamp_sort_order(direction = "asc")
        [arel_table[:created_at].send(direction.downcase)].tap do |array|
          array << arel_table[primary_key].send(direction.downcase) if primary_key_is_int?
        end
      end

      # Given a hash of attributes like `name: 'Joan'`, query the
      # `versions.objects` column.
      #
      # ```
      # SELECT "versions".*
      # FROM "versions"
      # WHERE ("versions"."object" LIKE '%
      # name: Joan
      # %')
      # ```
      #
      # This is useful for finding versions where a given attribute had a given
      # value. Imagine, in the example above, that Joan had changed her name
      # and we wanted to find the versions before that change.
      #
      # Based on the data type of the `object` column, the appropriate SQL
      # operator is used. For example, a text column will use `like`, and a
      # jsonb column will use `@>`.
      #
      # @api public
      def where_object(args = {})
        raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
        Queries::Versions::WhereObject.new(self, args).execute
      end

      # Given a hash of attributes like `name: 'Joan'`, query the
      # `versions.objects_changes` column.
      #
      # ```
      # SELECT "versions".*
      # FROM "versions"
      # WHERE .. ("versions"."object_changes" LIKE '%
      # name:
      # - Joan
      # %' OR "versions"."object_changes" LIKE '%
      # name:
      # -%
      # - Joan
      # %')
      # ```
      #
      # This is useful for finding versions immediately before and after a given
      # attribute had a given value. Imagine, in the example above, that someone
      # changed their name to Joan and we wanted to find the versions
      # immediately before and after that change.
      #
      # Based on the data type of the `object` column, the appropriate SQL
      # operator is used. For example, a text column will use `like`, and a
      # jsonb column will use `@>`.
      #
      # @api public
      def where_object_changes(args = {})
        raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
        Queries::Versions::WhereObjectChanges.new(self, args).execute
      end

      def primary_key_is_int?
        @primary_key_is_int ||= columns_hash[primary_key].type == :integer
      rescue StandardError # TODO: Rescue something more specific
        true
      end

      # Returns whether the `object` column is using the `json` type supported
      # by PostgreSQL.
      def object_col_is_json?
        %i[json jsonb].include?(columns_hash["object"].type)
      end

      # Returns whether the `object_changes` column is using the `json` type
      # supported by PostgreSQL.
      def object_changes_col_is_json?
        %i[json jsonb].include?(columns_hash["object_changes"].try(:type))
      end

      # Returns versions before `obj`.
      #
      # @param obj - a `Version` or a timestamp
      # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
      #   Default: false.
      # @return `ActiveRecord::Relation`
      # @api public
      def preceding(obj, timestamp_arg = false)
        if timestamp_arg != true && primary_key_is_int?
          preceding_by_id(obj)
        else
          preceding_by_timestamp(obj)
        end
      end

      # Returns versions after `obj`.
      #
      # @param obj - a `Version` or a timestamp
      # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
      #   Default: false.
      # @return `ActiveRecord::Relation`
      # @api public
      def subsequent(obj, timestamp_arg = false)
        if timestamp_arg != true && primary_key_is_int?
          subsequent_by_id(obj)
        else
          subsequent_by_timestamp(obj)
        end
      end

      private

      # @api private
      def preceding_by_id(obj)
        where(arel_table[primary_key].lt(obj.id)).order(arel_table[primary_key].desc)
      end

      # @api private
      def preceding_by_timestamp(obj)
        obj = obj.send(:created_at) if obj.is_a?(self)
        where(arel_table[:created_at].lt(obj)).
          order(timestamp_sort_order("desc"))
      end

      # @api private
      def subsequent_by_id(version)
        where(arel_table[primary_key].gt(version.id)).order(arel_table[primary_key].asc)
      end

      # @api private
      def subsequent_by_timestamp(obj)
        obj = obj.send(:created_at) if obj.is_a?(self)
        where(arel_table[:created_at].gt(obj)).order(timestamp_sort_order)
      end
    end

    # @api private
    def object_deserialized
      if self.class.object_col_is_json?
        object
      else
        PaperTrail.serializer.load(object)
      end
    end

    # Restore the item from this version.
    #
    # Optionally this can also restore all :has_one and :has_many (including
    # has_many :through) associations as they were "at the time", if they are
    # also being versioned by PaperTrail.
    #
    # Options:
    #
    # - :has_one
    #   - `true` - Also reify has_one associations.
    #   - `false - Default.
    # - :has_many
    #   - `true` - Also reify has_many and has_many :through associations.
    #   - `false` - Default.
    # - :mark_for_destruction
    #   - `true` - Mark the has_one/has_many associations that did not exist in
    #     the reified version for destruction, instead of removing them.
    #   - `false` - Default. Useful for persisting the reified version.
    # - :dup
    #   - `false` - Default.
    #   - `true` - Always create a new object instance. Useful for
    #     comparing two versions of the same object.
    # - :unversioned_attributes
    #   - `:nil` - Default. Attributes undefined in version record are set to
    #     nil in reified record.
    #   - `:preserve` - Attributes undefined in version record are not modified.
    #
    def reify(options = {})
      unless self.class.column_names.include? "object"
        raise "reify can't be called without an object column"
      end
      return nil if object.nil?
      ::PaperTrail::Reifier.reify(self, options)
    end

    # Returns what changed in this version of the item.
    # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
    # not have an `object_changes` text column.
    def changeset
      return nil unless self.class.column_names.include? "object_changes"
      @changeset ||= load_changeset
    end

    # Returns who put the item into the state stored in this version.
    def paper_trail_originator
      @paper_trail_originator ||= previous.try(:whodunnit)
    end

    # Returns who changed the item from the state it had in this version. This
    # is an alias for `whodunnit`.
    def terminator
      @terminator ||= whodunnit
    end
    alias version_author terminator

    def sibling_versions(reload = false)
      if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
        @sibling_versions = self.class.with_item_keys(item_type, item_id)
      end
      @sibling_versions
    end

    def next
      @next ||= sibling_versions.subsequent(self).first
    end

    def previous
      @previous ||= sibling_versions.preceding(self).first
    end

    # Returns an integer representing the chronological position of the
    # version among its siblings (see `sibling_versions`). The "create" event,
    # for example, has an index of 0.
    # @api public
    def index
      @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
    end

    private

    # @api private
    def load_changeset
      if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset)
        return PaperTrail.config.object_changes_adapter.load_changeset(self)
      end

      # First, deserialize the `object_changes` column.
      changes = HashWithIndifferentAccess.new(object_changes_deserialized)

      # The next step is, perhaps unfortunately, called "de-serialization",
      # and appears to be responsible for custom attribute serializers. For an
      # example of a custom attribute serializer, see
      # `Person::TimeZoneSerializer` in the test suite.
      #
      # Is `item.class` good enough? Does it handle `inheritance_column`
      # as well as `Reifier#version_reification_class`? We were using
      # `item_type.constantize`, but that is problematic when the STI parent
      # is not versioned. (See `Vehicle` and `Car` in the test suite).
      #
      # Note: `item` returns nil if `event` is "destroy".
      unless item.nil?
        AttributeSerializers::ObjectChangesAttribute.
          new(item.class).
          deserialize(changes)
      end

      # Finally, return a Hash mapping each attribute name to
      # a two-element array representing before and after.
      changes
    end

    # If the `object_changes` column is a Postgres JSON column, then
    # ActiveRecord will deserialize it for us. Otherwise, it's a string column
    # and we must deserialize it ourselves.
    # @api private
    def object_changes_deserialized
      if self.class.object_changes_col_is_json?
        object_changes
      else
        begin
          PaperTrail.serializer.load(object_changes)
        rescue StandardError # TODO: Rescue something more specific
          {}
        end
      end
    end

    # Enforces the `version_limit`, if set. Default: no limit.
    # @api private
    def enforce_version_limit!
      limit = version_limit
      return unless limit.is_a? Numeric
      previous_versions = sibling_versions.not_creates.
        order(self.class.timestamp_sort_order("asc"))
      return unless previous_versions.size > limit
      excess_versions = previous_versions - previous_versions.last(limit)
      excess_versions.map(&:destroy)
    end

    # See docs section 2.e. Limiting the Number of Versions Created.
    # The version limit can be global or per-model.
    #
    # @api private
    #
    # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
    def version_limit
      if self.class.item_subtype_column_present?
        klass = (item_subtype || item_type).constantize
        if klass&.paper_trail_options&.key?(:limit)
          return klass.paper_trail_options[:limit]
        end
      end
      PaperTrail.config.version_limit
    end
  end
end


# frozen_string_literal: true

module PaperTrail
  # Represents the history of a single record.
  # @api private
  class RecordHistory
    # @param versions - ActiveRecord::Relation - All versions of the record.
    # @param version_class - Class - Usually PaperTrail::Version,
    #   but it could also be a custom version class.
    # @api private
    def initialize(versions, version_class)
      @versions = versions
      @version_class = version_class
    end

    # Returns ordinal position of `version` in `sequence`.
    # @api private
    def index(version)
      sequence.to_a.index(version)
    end

    private

    # Returns `@versions` in chronological order.
    # @api private
    def sequence
      if @version_class.primary_key_is_int?
        @versions.select(primary_key).order(primary_key.asc)
      else
        @versions.
          select([table[:created_at], primary_key]).
          order(@version_class.timestamp_sort_order)
      end
    end

    # @return - Arel::Attribute - Attribute representing the primary key
    #   of the version table. The column's data type is usually a serial
    #   integer (the rails convention) but not always.
    # @api private
    def primary_key
      table[@version_class.primary_key]
    end

    # @return - Arel::Table - The version table, usually named `versions`, but
    #   not always.
    # @api private
    def table
      @version_class.arel_table
    end
  end
end


# frozen_string_literal: true

module PaperTrail
  # Utilities for deleting version records.
  module Cleaner
    # Destroys all but the most recent version(s) for items on a given date
    # (or on all dates). Useful for deleting drafts.
    #
    # Options:
    #
    # - :keeping - An `integer` indicating the number of versions to be kept for
    #   each item per date. Defaults to `1`. The most recent matching versions
    #   are kept.
    # - :date - Should either be a `Date` object specifying which date to
    #   destroy versions for or `:all`, which will specify that all dates
    #   should be cleaned. Defaults to `:all`.
    # - :item_id - The `id` for the item to be cleaned on, or `nil`, which
    #   causes all items to be cleaned. Defaults to `nil`.
    #
    def clean_versions!(options = {})
      options = { keeping: 1, date: :all }.merge(options)
      gather_versions(options[:item_id], options[:date]).each do |_item_id, item_versions|
        group_versions_by_date(item_versions).each do |_date, date_versions|
          # Remove the number of versions we wish to keep from the collection
          # of versions prior to destruction.
          date_versions.pop(options[:keeping])
          date_versions.map(&:destroy)
        end
      end
    end

    private

    # Returns a hash of versions grouped by the `item_id` attribute formatted
    # like this: {:item_id => PaperTrail::Version}. If `item_id` or `date` is
    # set, versions will be narrowed to those pointing at items with those ids
    # that were created on specified date. Versions are returned in
    # chronological order.
    def gather_versions(item_id = nil, date = :all)
      unless date == :all || date.respond_to?(:to_date)
        raise ArgumentError, "Expected date to be a Timestamp or :all"
      end
      versions = item_id ? PaperTrail::Version.where(item_id: item_id) : PaperTrail::Version
      versions = versions.order(PaperTrail::Version.timestamp_sort_order)
      versions = versions.between(date.to_date, date.to_date + 1.day) unless date == :all

      # If `versions` has not been converted to an ActiveRecord::Relation yet,
      # do so now.
      versions = PaperTrail::Version.all if versions == PaperTrail::Version
      versions.group_by(&:item_id)
    end

    # Given an array of versions, returns a hash mapping dates to arrays of
    # versions.
    # @api private
    def group_versions_by_date(versions)
      versions.group_by { |v| v.created_at.to_date }
    end
  end
end


# frozen_string_literal: true

module PaperTrail
  # Configures an ActiveRecord model, mostly at application boot time, but also
  # sometimes mid-request, with methods like enable/disable.
  class ModelConfig
    E_CANNOT_RECORD_AFTER_DESTROY = <<-STR.strip_heredoc.freeze
      paper_trail.on_destroy(:after) is incompatible with ActiveRecord's
      belongs_to_required_by_default. Use on_destroy(:before)
      or disable belongs_to_required_by_default.
    STR
    E_HPT_ABSTRACT_CLASS = <<~STR.squish.freeze
      An application model (%s) has been configured to use PaperTrail (via
      `has_paper_trail`), but the version model it has been told to use (%s) is
      an `abstract_class`. This could happen when an advanced feature called
      Custom Version Classes (http://bit.ly/2G4ch0G) is misconfigured. When all
      version classes are custom, PaperTrail::Version is configured to be an
      `abstract_class`. This is fine, but all application models must be
      configured to use concrete (not abstract) version models.
    STR
    E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
      To use PaperTrail's per-model limit in your %s model, you must have an
      item_subtype column in your versions table. See documentation sections
      2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
    STR
    DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
      Passing versions association name as `has_paper_trail versions: %{versions_name}`
      is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
      The hash you pass to `versions:` is now passed directly to `has_many`.
    STR
    DPR_CLASS_NAME_OPTION = <<~STR.squish
      Passing Version class name as `has_paper_trail class_name: %{class_name}`
      is deprecated. Use `has_paper_trail versions: {class_name: %{class_name}}`
      instead. The hash you pass to `versions:` is now passed directly to `has_many`.
    STR

    def initialize(model_class)
      @model_class = model_class
    end

    # Adds a callback that records a version after a "create" event.
    #
    # @api public
    def on_create
      @model_class.after_create { |r|
        r.paper_trail.record_create if r.paper_trail.save_version?
      }
      return if @model_class.paper_trail_options[:on].include?(:create)
      @model_class.paper_trail_options[:on] << :create
    end

    # Adds a callback that records a version before or after a "destroy" event.
    #
    # @api public
    def on_destroy(recording_order = "before")
      unless %w[after before].include?(recording_order.to_s)
        raise ArgumentError, 'recording order can only be "after" or "before"'
      end

      if recording_order.to_s == "after" && cannot_record_after_destroy?
        raise E_CANNOT_RECORD_AFTER_DESTROY
      end

      @model_class.send(
        "#{recording_order}_destroy",
        lambda do |r|
          return unless r.paper_trail.save_version?
          r.paper_trail.record_destroy(recording_order)
        end
      )

      return if @model_class.paper_trail_options[:on].include?(:destroy)
      @model_class.paper_trail_options[:on] << :destroy
    end

    # Adds a callback that records a version after an "update" event.
    #
    # @api public
    def on_update
      @model_class.before_save { |r|
        r.paper_trail.reset_timestamp_attrs_for_update_if_needed
      }
      @model_class.after_update { |r|
        if r.paper_trail.save_version?
          r.paper_trail.record_update(
            force: false,
            in_after_callback: true,
            is_touch: false
          )
        end
      }
      @model_class.after_update { |r|
        r.paper_trail.clear_version_instance
      }
      return if @model_class.paper_trail_options[:on].include?(:update)
      @model_class.paper_trail_options[:on] << :update
    end

    # Adds a callback that records a version after a "touch" event.
    # @api public
    def on_touch
      @model_class.after_touch { |r|
        r.paper_trail.record_update(
          force: true,
          in_after_callback: true,
          is_touch: true
        )
      }
    end

    # Set up `@model_class` for PaperTrail. Installs callbacks, associations,
    # "class attributes", instance methods, and more.
    # @api private
    def setup(options = {})
      options[:on] ||= %i[create update destroy touch]
      options[:on] = Array(options[:on]) # Support single symbol
      @model_class.send :include, ::PaperTrail::Model::InstanceMethods
      setup_options(options)
      setup_associations(options)
      check_presence_of_item_subtype_column(options)
      @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
      setup_callbacks_from_options options[:on]
    end

    def version_class
      @_version_class ||= @model_class.version_class_name.constantize
    end

    private

    def active_record_gem_version
      Gem::Version.new(ActiveRecord::VERSION::STRING)
    end

    # Raises an error if the provided class is an `abstract_class`.
    # @api private
    def assert_concrete_activerecord_class(class_name)
      if class_name.constantize.abstract_class?
        raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
      end
    end

    def cannot_record_after_destroy?
      Gem::Version.new(ActiveRecord::VERSION::STRING).release >= Gem::Version.new("5") &&
        ::ActiveRecord::Base.belongs_to_required_by_default
    end

    # Some options require the presence of the `item_subtype` column. Currently
    # only `limit`, but in the future there may be others.
    #
    # @api private
    def check_presence_of_item_subtype_column(options)
      return unless options.key?(:limit)
      return if version_class.item_subtype_column_present?
      raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
    end

    def check_version_class_name(options)
      # @api private - `version_class_name`
      @model_class.class_attribute :version_class_name
      if options[:class_name]
        ::ActiveSupport::Deprecation.warn(
          format(
            DPR_CLASS_NAME_OPTION,
            class_name: options[:class_name].inspect
          ),
          caller(1)
        )
        options[:versions][:class_name] = options[:class_name]
      end
      @model_class.version_class_name = options[:versions][:class_name] || "PaperTrail::Version"
      assert_concrete_activerecord_class(@model_class.version_class_name)
    end

    def check_versions_association_name(options)
      # @api private - versions_association_name
      @model_class.class_attribute :versions_association_name
      @model_class.versions_association_name = options[:versions][:name] || :versions
    end

    def define_has_many_versions(options)
      options = ensure_versions_option_is_hash(options)
      check_version_class_name(options)
      check_versions_association_name(options)
      scope = get_versions_scope(options)
      @model_class.has_many(
        @model_class.versions_association_name,
        scope,
        class_name: @model_class.version_class_name,
        as: :item,
        **options[:versions].except(:name, :scope)
      )
    end

    def ensure_versions_option_is_hash(options)
      unless options[:versions].is_a?(Hash)
        if options[:versions]
          ::ActiveSupport::Deprecation.warn(
            format(
              DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION,
              versions_name: options[:versions].inspect
            ),
            caller(1)
          )
        end
        options[:versions] = {
          name: options[:versions]
        }
      end
      options
    end

    def get_versions_scope(options)
      options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
    end

    def setup_associations(options)
      # @api private - version_association_name
      @model_class.class_attribute :version_association_name
      @model_class.version_association_name = options[:version] || :version

      # The version this instance was reified from.
      # @api public
      @model_class.send :attr_accessor, @model_class.version_association_name

      # @api public - paper_trail_event
      @model_class.send :attr_accessor, :paper_trail_event

      define_has_many_versions(options)
    end

    def setup_callbacks_from_options(options_on = [])
      options_on.each do |event|
        public_send("on_#{event}")
      end
    end

    def setup_options(options)
      # @api public - paper_trail_options - Let's encourage plugins to use eg.
      # `paper_trail_options[:versions][:class_name]` rather than
      # `version_class_name` because the former is documented and the latter is
      # not.
      @model_class.class_attribute :paper_trail_options
      @model_class.paper_trail_options = options.dup

      %i[ignore skip only].each do |k|
        @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
          flatten.
          compact.
          map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
      end

      @model_class.paper_trail_options[:meta] ||= {}
    end
  end
end


# frozen_string_literal: true

require "paper_trail/events/create"
require "paper_trail/events/destroy"
require "paper_trail/events/update"

module PaperTrail
  # Represents the "paper trail" for a single record.
  class RecordTrail
    RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")

    def initialize(record)
      @record = record
    end

    # Invoked after rollbacks to ensure versions records are not created for
    # changes that never actually took place. Optimization: Use lazy `reset`
    # instead of eager `reload` because, in many use cases, the association will
    # not be used.
    def clear_rolled_back_versions
      versions.reset
    end

    # Invoked via`after_update` callback for when a previous version is
    # reified and then saved.
    def clear_version_instance
      @record.send("#{@record.class.version_association_name}=", nil)
    end

    # Is PT enabled for this particular record?
    # @api private
    def enabled?
      PaperTrail.enabled? &&
        PaperTrail.request.enabled? &&
        PaperTrail.request.enabled_for_model?(@record.class)
    end

    # Returns true if this instance is the current, live one;
    # returns false if this instance came from a previous version.
    def live?
      source_version.nil?
    end

    # Returns the object (not a Version) as it became next.
    # NOTE: if self (the item) was not reified from a version, i.e. it is the
    #  "live" item, we return nil.  Perhaps we should return self instead?
    def next_version
      subsequent_version = source_version.next
      subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
    rescue StandardError # TODO: Rescue something more specific
      nil
    end

    # Returns who put `@record` into its current state.
    #
    # @api public
    def originator
      (source_version || versions.last).try(:whodunnit)
    end

    # Returns the object (not a Version) as it was most recently.
    #
    # @api public
    def previous_version
      (source_version ? source_version.previous : versions.last).try(:reify)
    end

    def record_create
      return unless enabled?

      build_version_on_create(in_after_callback: true).tap do |version|
        version.save!
        # Because the version object was created using version_class.new instead
        # of versions_assoc.build?, the association cache is unaware. So, we
        # invalidate the `versions` association cache with `reset`.
        versions.reset
      end
    end

    # PT-AT extends this method to add its transaction id.
    #
    # @api private
    def data_for_create
      {}
    end

    # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
    #
    # @api private
    # @return - The created version object, so that plugins can use it, e.g.
    # paper_trail-association_tracking
    def record_destroy(recording_order)
      return unless enabled? && !@record.new_record?
      in_after_callback = recording_order == "after"
      event = Events::Destroy.new(@record, in_after_callback)

      # Merge data from `Event` with data from PT-AT. We no longer use
      # `data_for_destroy` but PT-AT still does.
      data = event.data.merge(data_for_destroy)

      version = @record.class.paper_trail.version_class.create(data)
      if version.errors.any?
        log_version_errors(version, :destroy)
      else
        assign_and_reset_version_association(version)
        version
      end
    end

    # PT-AT extends this method to add its transaction id.
    #
    # @api private
    def data_for_destroy
      {}
    end

    # @api private
    # @return - The created version object, so that plugins can use it, e.g.
    # paper_trail-association_tracking
    def record_update(force:, in_after_callback:, is_touch:)
      return unless enabled?

      version = build_version_on_update(
        force: force,
        in_after_callback: in_after_callback,
        is_touch: is_touch
      )
      return unless version

      if version.save
        # Because the version object was created using version_class.new instead
        # of versions_assoc.build?, the association cache is unaware. So, we
        # invalidate the `versions` association cache with `reset`.
        versions.reset
        version
      else
        log_version_errors(version, :update)
      end
    end

    # PT-AT extends this method to add its transaction id.
    #
    # @api private
    def data_for_update
      {}
    end

    # @api private
    # @return - The created version object, so that plugins can use it, e.g.
    # paper_trail-association_tracking
    def record_update_columns(changes)
      return unless enabled?
      event = Events::Update.new(@record, false, false, changes)

      # Merge data from `Event` with data from PT-AT. We no longer use
      # `data_for_update_columns` but PT-AT still does.
      data = event.data.merge(data_for_update_columns)

      versions_assoc = @record.send(@record.class.versions_association_name)
      version = versions_assoc.create(data)
      if version.errors.any?
        log_version_errors(version, :update)
      else
        version
      end
    end

    # PT-AT extends this method to add its transaction id.
    #
    # @api private
    def data_for_update_columns
      {}
    end

    # Invoked via callback when a user attempts to persist a reified
    # `Version`.
    def reset_timestamp_attrs_for_update_if_needed
      return if live?
      @record.send(:timestamp_attributes_for_update_in_model).each do |column|
        @record.send("restore_#{column}!")
      end
    end

    # AR callback.
    # @api private
    def save_version?
      if_condition = @record.paper_trail_options[:if]
      unless_condition = @record.paper_trail_options[:unless]
      (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
    end

    def source_version
      version
    end

    # Save, and create a version record regardless of options such as `:on`,
    # `:if`, or `:unless`.
    #
    # Arguments are passed to `save`.
    #
    # This is an "update" event. That is, we record the same data we would in
    # the case of a normal AR `update`.
    def save_with_version(*args)
      ::PaperTrail.request(enabled: false) do
        @record.save(*args)
      end
      record_update(force: true, in_after_callback: false, is_touch: false)
    end

    # Like the `update_column` method from `ActiveRecord::Persistence`, but also
    # creates a version to record those changes.
    # @api public
    def update_column(name, value)
      update_columns(name => value)
    end

    # Like the `update_columns` method from `ActiveRecord::Persistence`, but also
    # creates a version to record those changes.
    # @api public
    def update_columns(attributes)
      # `@record.update_columns` skips dirty-tracking, so we can't just use
      # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
      # We need to build our own hash with the changes that will be made
      # directly to the database.
      changes = {}
      attributes.each do |k, v|
        changes[k] = [@record[k], v]
      end
      @record.update_columns(attributes)
      record_update_columns(changes)
    end

    # Returns the object (not a Version) as it was at the given timestamp.
    def version_at(timestamp, reify_options = {})
      # Because a version stores how its object looked *before* the change,
      # we need to look for the first version created *after* the timestamp.
      v = versions.subsequent(timestamp, true).first
      return v.reify(reify_options) if v
      @record unless @record.destroyed?
    end

    # Returns the objects (not Versions) as they were between the given times.
    def versions_between(start_time, end_time)
      versions = send(@record.class.versions_association_name).between(start_time, end_time)
      versions.collect { |version| version_at(version.created_at) }
    end

    private

    # @api private
    def assign_and_reset_version_association(version)
      @record.send("#{@record.class.version_association_name}=", version)
      @record.send(@record.class.versions_association_name).reset
    end

    # @api private
    def build_version_on_create(in_after_callback:)
      event = Events::Create.new(@record, in_after_callback)

      # Merge data from `Event` with data from PT-AT. We no longer use
      # `data_for_create` but PT-AT still does.
      data = event.data.merge!(data_for_create)

      # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
      @record.class.paper_trail.version_class.new(data)
    end

    # @api private
    def build_version_on_update(force:, in_after_callback:, is_touch:)
      event = Events::Update.new(@record, in_after_callback, is_touch, nil)
      return unless force || event.changed_notably?

      # Merge data from `Event` with data from PT-AT. We no longer use
      # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
      # instead of `merge`.
      data = event.data.merge!(data_for_update)

      # Using `version_class.new` reduces memory usage compared to
      # `versions_assoc.build`. It's a trade-off though. We have to clear
      # the association cache (see `versions.reset`) and that could cause an
      # additional query in certain applications.
      @record.class.paper_trail.version_class.new(data)
    end

    def log_version_errors(version, action)
      version.logger&.warn(
        "Unable to create version for #{action} of #{@record.class.name}" \
          "##{@record.id}: " + version.errors.full_messages.join(", ")
      )
    end

    def version
      @record.public_send(@record.class.version_association_name)
    end

    def versions
      @record.public_send(@record.class.versions_association_name)
    end
  end
end


require 'listen/file'
require 'listen/directory'

module Listen
  # TODO: rename to Snapshot
  class Change
    # TODO: test this class for coverage
    class Config
      def initialize(queue, silencer)
        @queue = queue
        @silencer = silencer
      end

      def silenced?(path, type)
        @silencer.silenced?(Pathname(path), type)
      end

      def queue(*args)
        @queue << args
      end
    end

    attr_reader :record

    def initialize(config, record)
      @config = config
      @record = record
    end

    # Invalidate some part of the snapshot/record (dir, file, subtree, etc.)
    def invalidate(type, rel_path, options)
      watched_dir = Pathname.new(record.root)

      change = options[:change]
      cookie = options[:cookie]

      if !cookie && config.silenced?(rel_path, type)
        Listen::Logger.debug { "(silenced): #{rel_path.inspect}" }
        return
      end

      path = watched_dir + rel_path

      Listen::Logger.debug do
        log_details = options[:silence] && 'recording' || change || 'unknown'
        "#{log_details}: #{type}:#{path} (#{options.inspect})"
      end

      if change
        options = cookie ? { cookie: cookie } : {}
        config.queue(type, change, watched_dir, rel_path, options)
      elsif type == :dir
        # NOTE: POSSIBLE RECURSION
        # TODO: fix - use a queue instead
        Directory.scan(self, rel_path, options)
      else
        change = File.change(record, rel_path)
        return if !change || options[:silence]
        config.queue(:file, change, watched_dir, rel_path)
      end
    rescue RuntimeError => ex
      msg = format(
        '%s#%s crashed %s:%s',
        self.class,
        __method__,
        exinspect,
        ex.backtrace * "\n")
      Listen::Logger.error(msg)
      raise
    end

    private

    attr_reader :config
  end
end


module Listen
  class Silencer
    # The default list of directories that get ignored.
    DEFAULT_IGNORED_DIRECTORIES = %r{^(?:
      \.git
      | \.svn
      | \.hg
      | \.rbx
      | \.bundle
      | bundle
      | vendor/bundle
      | log
      | tmp
      |vendor/ruby
    )(/|$)}x

    # The default list of files that get ignored.
    DEFAULT_IGNORED_EXTENSIONS = %r{(?:
      # Kate's tmp\/swp files
      \..*\d+\.new
      | \.kate-swp

      # Gedit tmp files
      | \.goutputstream-.{6}

      # Intellij files
      | ___jb_bak___
      | ___jb_old___

      # Vim swap files and write test
      | \.sw[px]
      | \.swpx
      | ^4913

      # Sed temporary files - but without actual words, like 'sedatives'
      | (?:^
         sed

         (?:
          [a-zA-Z0-9]{0}[A-Z]{1}[a-zA-Z0-9]{5} |
          [a-zA-Z0-9]{1}[A-Z]{1}[a-zA-Z0-9]{4} |
          [a-zA-Z0-9]{2}[A-Z]{1}[a-zA-Z0-9]{3} |
          [a-zA-Z0-9]{3}[A-Z]{1}[a-zA-Z0-9]{2} |
          [a-zA-Z0-9]{4}[A-Z]{1}[a-zA-Z0-9]{1} |
          [a-zA-Z0-9]{5}[A-Z]{1}[a-zA-Z0-9]{0}
         )
        )

      # other files
      | \.DS_Store
      | \.tmp
      | ~
    )$}x

    attr_accessor :only_patterns, :ignore_patterns

    def initialize
      configure({})
    end

    def configure(options)
      @only_patterns = options[:only] ? Array(options[:only]) : nil
      @ignore_patterns = _init_ignores(options[:ignore], options[:ignore!])
    end

    # Note: relative_path is temporarily expected to be a relative Pathname to
    # make refactoring easier (ideally, it would take a string)

    # TODO: switch type and path places - and verify
    def silenced?(relative_path, type)
      path = relative_path.to_s

      if only_patterns && type == :file
        return true unless only_patterns.any? { |pattern| path =~ pattern }
      end

      ignore_patterns.any? { |pattern| path =~ pattern }
    end

    private

    attr_reader :options

    def _init_ignores(ignores, overrides)
      patterns = []
      unless overrides
        patterns << DEFAULT_IGNORED_DIRECTORIES
        patterns << DEFAULT_IGNORED_EXTENSIONS
      end

      patterns << ignores
      patterns << overrides

      patterns.compact.flatten
    end
  end
end


module Listen
  class QueueOptimizer
    class Config
      def initialize(adapter_class, silencer)
        @adapter_class = adapter_class
        @silencer = silencer
      end

      def exist?(path)
        Pathname(path).exist?
      end

      def silenced?(path, type)
        @silencer.silenced?(path, type)
      end

      def debug(*args, &block)
        Listen.logger.debug(*args, &block)
      end
    end

    def smoosh_changes(changes)
      # TODO: adapter could be nil at this point (shutdown)
      cookies = changes.group_by do |_, _, _, _, options|
        (options || {})[:cookie]
      end
      _squash_changes(_reinterpret_related_changes(cookies))
    end

    def initialize(config)
      @config = config
    end

    private

    attr_reader :config

    # groups changes into the expected structure expected by
    # clients
    def _squash_changes(changes)
      # We combine here for backward compatibility
      # Newer clients should receive dir and path separately
      changes = changes.map { |change, dir, path| [change, dir + path] }

      actions = changes.group_by(&:last).map do |path, action_list|
        [_logical_action_for(path, action_list.map(&:first)), path.to_s]
      end

      config.debug("listen: raw changes: #{actions.inspect}")

      { modified: [], added: [], removed: [] }.tap do |squashed|
        actions.each do |type, path|
          squashed[type] << path unless type.nil?
        end
        config.debug("listen: final changes: #{squashed.inspect}")
      end
    end

    def _logical_action_for(path, actions)
      actions << :added if actions.delete(:moved_to)
      actions << :removed if actions.delete(:moved_from)

      modified = actions.detect { |x| x == :modified }
      _calculate_add_remove_difference(actions, path, modified)
    end

    def _calculate_add_remove_difference(actions, path, default_if_exists)
      added = actions.count { |x| x == :added }
      removed = actions.count { |x| x == :removed }
      diff = added - removed

      # TODO: avoid checking if path exists and instead assume the events are
      # in order (if last is :removed, it doesn't exist, etc.)
      if config.exist?(path)
        if diff > 0
          :added
        elsif diff.zero? && added > 0
          :modified
        else
          default_if_exists
        end
      else
        diff < 0 ? :removed : nil
      end
    end

    # remove extraneous rb-inotify events, keeping them only if it's a possible
    # editor rename() call (e.g. Kate and Sublime)
    def _reinterpret_related_changes(cookies)
      table = { moved_to: :added, moved_from: :removed }
      cookies.flat_map do |_, changes|
        data = _detect_possible_editor_save(changes)
        if data
          to_dir, to_file = data
          [[:modified, to_dir, to_file]]
        else
          not_silenced = changes.reject do |type, _, _, path, _|
            config.silenced?(Pathname(path), type)
          end
          not_silenced.map do |_, change, dir, path, _|
            [table.fetch(change, change), dir, path]
          end
        end
      end
    end

    def _detect_possible_editor_save(changes)
      return unless changes.size == 2

      from_type = from = nil
      to_type = to_dir = to = nil

      changes.each do |data|
        case data[1]
        when :moved_from
          from_type, _from_change, _, from, = data
        when :moved_to
          to_type, _to_change, to_dir, to, = data
        else
          return nil
        end
      end

      return unless from && to

      # Expect an ignored moved_from and non-ignored moved_to
      # to qualify as an "editor modify"
      return unless config.silenced?(Pathname(from), from_type)
      config.silenced?(Pathname(to), to_type) ? nil : [to_dir, to]
    end
  end
end


module Ancestry
  module ClassMethods
    # Fetch tree node if necessary
    def to_node object
      if object.is_a?(self.ancestry_base_class) then object else unscoped_where{|scope| scope.find object} end
    end

    # Scope on relative depth options
    def scope_depth depth_options, depth
      depth_options.inject(self.ancestry_base_class) do |scope, option|
        scope_name, relative_depth = option
        if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
          scope.send scope_name, depth + relative_depth
        else
          raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
        end
      end
    end

    # Orphan strategy writer
    def orphan_strategy= orphan_strategy
      # Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed
      if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
        class_variable_set :@@orphan_strategy, orphan_strategy
      else
        raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
      end
    end

    # Get all nodes and sorting them into an empty hash
    def arrange options = {}
      if (order = options.delete(:order))
        arrange_nodes self.ancestry_base_class.order(order).where(options)
      else
        arrange_nodes self.ancestry_base_class.where(options)
      end
    end

    # Arrange array of nodes into a nested hash of the form
    # {node => children}, where children = {} if the node has no children
    # If a node's parent is not included, the node will be included as if it is a top level node
    def arrange_nodes(nodes)
      node_ids = Set.new(nodes.map(&:id))
      index = Hash.new { |h, k| h[k] = {} }

      nodes.each_with_object({}) do |node, arranged|
        children = index[node.id]
        index[node.parent_id][node] = children
        arranged[node] = children unless node_ids.include?(node.parent_id)
      end
    end

     # Arrangement to nested array
    def arrange_serializable options={}, nodes=nil, &block
      nodes = arrange(options) if nodes.nil?
      nodes.map do |parent, children|
        if block_given?
          yield parent, arrange_serializable(options, children, &block)
        else
          parent.serializable_hash.merge 'children' => arrange_serializable(options, children)
        end
      end
    end

    # Pseudo-preordered array of nodes.  Children will always follow parents,
    # for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
    def sort_by_ancestry(nodes, &block)
      arranged = nodes if nodes.is_a?(Hash)

      unless arranged
        presorted_nodes = nodes.sort do |a, b|
          a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'

          if block_given? && a_cestry == b_cestry
            yield a, b
          else
            a_cestry <=> b_cestry
          end
        end

        arranged = arrange_nodes(presorted_nodes)
      end

      arranged.inject([]) do |sorted_nodes, pair|
        node, children = pair
        sorted_nodes << node
        sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
        sorted_nodes
      end
    end

    # Integrity checking
    def check_ancestry_integrity! options = {}
      parents = {}
      exceptions = [] if options[:report] == :list

      unscoped_where do |scope|
        # For each node ...
        scope.find_each do |node|
          begin
            # ... check validity of ancestry column
            if !node.valid? and !node.errors[node.class.ancestry_column].blank?
              raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
            end
            # ... check that all ancestors exist
            node.ancestor_ids.each do |ancestor_id|
              unless exists? ancestor_id
                raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
              end
            end
            # ... check that all node parents are consistent with values observed earlier
            node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
              parents[node_id] = parent_id unless parents.has_key? node_id
              unless parents[node_id] == parent_id
                raise Ancestry::AncestryIntegrityException.new("Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}")
              end
            end
          rescue Ancestry::AncestryIntegrityException => integrity_exception
            case options[:report]
              when :list then exceptions << integrity_exception
              when :echo then puts integrity_exception
              else raise integrity_exception
            end
          end
        end
      end
      exceptions if options[:report] == :list
    end

    # Integrity restoration
    def restore_ancestry_integrity!
      parents = {}
      # Wrap the whole thing in a transaction ...
      self.ancestry_base_class.transaction do
        unscoped_where do |scope|
          # For each node ...
          scope.find_each do |node|
            # ... set its ancestry to nil if invalid
            if !node.valid? and !node.errors[node.class.ancestry_column].blank?
              node.without_ancestry_callbacks do
                node.update_attribute node.ancestry_column, nil
              end
            end
            # ... save parent of this node in parents array if it exists
            parents[node.id] = node.parent_id if exists? node.parent_id

            # Reset parent id in array to nil if it introduces a cycle
            parent = parents[node.id]
            until parent.nil? || parent == node.id
              parent = parents[parent]
            end
            parents[node.id] = nil if parent == node.id
          end

          # For each node ...
          scope.find_each do |node|
            # ... rebuild ancestry from parents array
            ancestry, parent = nil, parents[node.id]
            until parent.nil?
              ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
            end
            node.without_ancestry_callbacks do
              node.update_attribute node.ancestry_column, ancestry
            end
          end
        end
      end
    end

    # Build ancestry from parent id's for migration purposes
    def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
      unscoped_where do |scope|
        scope.where(:parent_id => parent_id).find_each do |node|
          node.without_ancestry_callbacks do
            node.update_attribute ancestry_column, ancestry
          end
          build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
        end
      end
    end

    # Rebuild depth cache if it got corrupted or if depth caching was just turned on
    def rebuild_depth_cache!
      raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column

      self.ancestry_base_class.transaction do
        unscoped_where do |scope|
          scope.find_each do |node|
            node.update_attribute depth_cache_column, node.depth
          end
        end
      end
    end

    def unscoped_where
      if ActiveRecord::VERSION::MAJOR < 4
        self.ancestry_base_class.unscoped do
          yield self.ancestry_base_class
        end
      else
        yield self.ancestry_base_class.unscope(:where)
      end
    end

    ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
    if ActiveSupport::VERSION::STRING < "4.2"
      def primary_key_is_an_integer?
        if defined?(@primary_key_is_an_integer)
          @primary_key_is_an_integer
        else
          @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(columns_hash[primary_key.to_s].type)
        end
      end
    else
      def primary_key_is_an_integer?
        if defined?(@primary_key_is_an_integer)
          @primary_key_is_an_integer
        else
          @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
        end
      end
    end
  end
end


module Ancestry
  module InstanceMethods
    BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save' : '_was'
    IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database' : '_was'

    # Validate that the ancestors don't include itself
    def ancestry_exclude_self
      errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
    end

    # Update descendants with new ancestry (before save)
    def update_descendants_with_new_ancestry
      # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
      if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestry?
        # ... for each descendant ...
        unscoped_descendants.each do |descendant|
          # ... replace old ancestry with new ancestry
          descendant.without_ancestry_callbacks do
            descendant.update_attribute(
              self.ancestry_base_class.ancestry_column,
              descendant.read_attribute(descendant.class.ancestry_column).gsub(
                # child_ancestry_was
                /^#{self.child_ancestry}/,
                # future child_ancestry
                if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
              )
            )
          end
        end
      end
    end

    # Apply orphan strategy (before destroy - no changes)
    def apply_orphan_strategy
      if !ancestry_callbacks_disabled? && !new_record?
        case self.ancestry_base_class.orphan_strategy
        when :rootify # make all children root if orphan strategy is rootify
          unscoped_descendants.each do |descendant|
            descendant.without_ancestry_callbacks do
              new_ancestry = if descendant.ancestry == child_ancestry
                nil
              else
                # child_ancestry did not change so child_ancestry_was will work here
                descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
              end
              descendant.update_attribute descendant.class.ancestry_column, new_ancestry
            end
          end
        when :destroy # destroy all descendants if orphan strategy is destroy
          unscoped_descendants.each do |descendant|
            descendant.without_ancestry_callbacks do
              descendant.destroy
            end
          end
        when :adopt # make child elements of this node, child of its parent
          descendants.each do |descendant|
            descendant.without_ancestry_callbacks do
              new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
              # check for empty string if it's then set to nil
              new_ancestry = nil if new_ancestry.empty?
              descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
            end
          end
        when :restrict # throw an exception if it has children
          raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
        end
      end
    end

    # Touch each of this record's ancestors (after save)
    def touch_ancestors_callback
      if !ancestry_callbacks_disabled? && self.ancestry_base_class.touch_ancestors
        # Touch each of the old *and* new ancestors
        unscoped_current_and_previous_ancestors.each do |ancestor|
          ancestor.without_ancestry_callbacks do
            ancestor.touch
          end
        end
      end
    end

    # The ancestry value for this record's children (before save)
    # This is technically child_ancestry_was
    def child_ancestry
      # New records cannot have children
      raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?

      if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
        id.to_s
      else
        "#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
      end
    end

    # Counter Cache
    def increase_parent_counter_cache
      self.class.increment_counter _counter_cache_column, parent_id
    end

    def decrease_parent_counter_cache
      # @_trigger_destroy_callback comes from activerecord, which makes sure only once decrement when concurrent deletion.
      # but @_trigger_destroy_callback began after rails@5.1.0.alpha.
      # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L340
      # https://github.com/rails/rails/pull/14735
      # https://github.com/rails/rails/pull/27248
      return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
      return if ancestry_callbacks_disabled?

      self.class.decrement_counter _counter_cache_column, parent_id
    end

    def update_parent_counter_cache
      changed =
        if ActiveRecord::VERSION::STRING >= '5.1.0'
          saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
        else
          ancestry_changed?
        end

      return unless changed

      if parent_id_was = parent_id_before_last_save
        self.class.decrement_counter _counter_cache_column, parent_id_was
      end

      parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
    end

    def _counter_cache_column
      self.ancestry_base_class.counter_cache_column.to_s
    end

    # Ancestors

    def ancestors?
      # ancestor_ids.present?
      read_attribute(self.ancestry_base_class.ancestry_column).present?
    end
    alias :has_parent? :ancestors?

    def ancestry_changed?
      changed.include?(self.ancestry_base_class.ancestry_column.to_s)
    end

    def ancestor_ids
      parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
    end

    def ancestor_conditions
      self.ancestry_base_class.ancestor_conditions(self)
    end

    def ancestors depth_options = {}
      return self.ancestry_base_class.none unless ancestors?
      self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
    end

    # deprecate
    def ancestor_was_conditions
      {primary_key_with_table => ancestor_ids_before_last_save}
    end

    # deprecated - probably don't want to use anymore
    def ancestor_ids_was
      parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
    end

    def ancestor_ids_before_last_save
      parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
    end

    def parent_id_before_last_save
      ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
      return unless ancestry_was.present?

      ancestry_was.split(ANCESTRY_DELIMITER).last.to_i
    end

    def path_ids
      ancestor_ids + [id]
    end

    def path_conditions
      self.ancestry_base_class.path_conditions(self)
    end

    def path depth_options = {}
      self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
    end

    def depth
      ancestor_ids.size
    end

    def cache_depth
      write_attribute self.ancestry_base_class.depth_cache_column, depth
    end

    def ancestor_of?(node)
      node.ancestor_ids.include?(self.id)
    end

    # Parent

    # currently parent= does not work in after save callbacks
    # assuming that parent hasn't changed
    def parent= parent
      write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
    end

    def parent_id= new_parent_id
      self.parent = new_parent_id.present? ? unscoped_find(new_parent_id) : nil
    end

    def parent_id
      ancestor_ids.last if ancestors?
    end

    def parent
      unscoped_find(parent_id) if ancestors?
    end

    def parent_id?
      ancestors?
    end

    def parent_of?(node)
      self.id == node.parent_id
    end

    # Root

    def root_id
      ancestors? ? ancestor_ids.first : id
    end

    def root
      ancestors? ? unscoped_find(root_id) : self
    end

    def is_root?
      read_attribute(self.ancestry_base_class.ancestry_column).blank?
    end
    alias :root? :is_root?

    def root_of?(node)
      self.id == node.root_id
    end

    # Children

    def child_conditions
      self.ancestry_base_class.child_conditions(self)
    end

    def children
      self.ancestry_base_class.where child_conditions
    end

    def child_ids
      children.pluck(self.ancestry_base_class.primary_key)
    end

    def has_children?
      self.children.exists?
    end
    alias_method :children?, :has_children?

    def is_childless?
      !has_children?
    end
    alias_method :childless?, :is_childless?

    def child_of?(node)
      self.parent_id == node.id
    end

    # Siblings

    def sibling_conditions
      self.ancestry_base_class.sibling_conditions(self)
    end

    def siblings
      self.ancestry_base_class.where sibling_conditions
    end

    def sibling_ids
      siblings.pluck(self.ancestry_base_class.primary_key)
    end

    def has_siblings?
      self.siblings.count > 1
    end
    alias_method :siblings?, :has_siblings?

    def is_only_child?
      !has_siblings?
    end
    alias_method :only_child?, :is_only_child?

    def sibling_of?(node)
      self.ancestry == node.ancestry
    end

    # Descendants

    def descendant_conditions
      self.ancestry_base_class.descendant_conditions(self)
    end

    def descendants depth_options = {}
      self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
    end

    def descendant_ids depth_options = {}
      descendants(depth_options).pluck(self.ancestry_base_class.primary_key)
    end

    def descendant_of?(node)
      ancestor_ids.include?(node.id)
    end

    # Indirects

    def indirect_conditions
      self.ancestry_base_class.indirect_conditions(self)
    end

    def indirects depth_options = {}
      self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where indirect_conditions
    end

    def indirect_ids depth_options = {}
      indirects(depth_options).pluck(self.ancestry_base_class.primary_key)
    end

    def indirect_of?(node)
      ancestor_ids[0..-2].include?(node.id)
    end

    # Subtree

    def subtree_conditions
      self.ancestry_base_class.subtree_conditions(self)
    end

    def subtree depth_options = {}
      self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
    end

    def subtree_ids depth_options = {}
      subtree(depth_options).pluck(self.ancestry_base_class.primary_key)
    end

    # Callback disabling

    def without_ancestry_callbacks
      @disable_ancestry_callbacks = true
      yield
      @disable_ancestry_callbacks = false
    end

    def ancestry_callbacks_disabled?
      defined?(@disable_ancestry_callbacks) && @disable_ancestry_callbacks
    end

  private
    ANCESTRY_DELIMITER = '/'.freeze

    def parse_ancestry_column obj
      return [] unless obj
      obj_ids = obj.split(ANCESTRY_DELIMITER)
      self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
    end

    def unscoped_descendants
      unscoped_where do |scope|
        scope.where descendant_conditions
      end
    end

    # works with after save context (hence before_last_save)
    def unscoped_current_and_previous_ancestors
      unscoped_where do |scope|
        scope.where id: (ancestor_ids + ancestor_ids_before_last_save).uniq
      end
    end

    def unscoped_find id
      unscoped_where do |scope|
        scope.find id
      end
    end

    def unscoped_where
      self.ancestry_base_class.unscoped_where do |scope|
        yield scope
      end
    end
  end
end


module Ancestry
  module MaterializedPath
    def self.extended(base)
      base.validates_format_of base.ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
      base.send(:include, InstanceMethods)
    end

    def root_conditions
      arel_table[ancestry_column].eq(nil)
    end

    def ancestor_conditions(object)
      t = arel_table
      node = to_node(object)
      t[primary_key].in(node.ancestor_ids)
    end

    def path_conditions(object)
      t = arel_table
      node = to_node(object)
      t[primary_key].in(node.path_ids)
    end

    def child_conditions(object)
      t = arel_table
      node = to_node(object)
      t[ancestry_column].eq(node.child_ancestry)
    end

    # indirect = anyone who is a descendant, but not a child
    def indirect_conditions(object)
      t = arel_table
      node = to_node(object)
      # rails has case sensitive matching.
      if ActiveRecord::VERSION::MAJOR >= 5
        t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true)
      else
        t[ancestry_column].matches("#{node.child_ancestry}/%")
      end
    end

    def descendant_conditions(object)
      t = arel_table
      node = to_node(object)
      # rails has case sensitive matching.
      if ActiveRecord::VERSION::MAJOR >= 5
        t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
      else
        t[ancestry_column].matches("#{node.child_ancestry}/%").or(t[ancestry_column].eq(node.child_ancestry))
      end
    end

    def subtree_conditions(object)
      t = arel_table
      node = to_node(object)
      descendant_conditions(node).or(t[primary_key].eq(node.id))
    end

    def sibling_conditions(object)
      t = arel_table
      node = to_node(object)
      t[ancestry_column].eq(node[ancestry_column])
    end

    module InstanceMethods
      # Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
      def sane_ancestry?
        ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
        ancestry_value.nil? || (ancestry_value.to_s =~ Ancestry::ANCESTRY_PATTERN && !ancestor_ids.include?(self.id))
      end
    end
  end
end


# frozen_string_literal: true

module Sail
  # Instrumenter
  #
  # Class containing methods to instrument
  # setting usage and provide insights to
  # dashboard users.
  class Instrumenter
    USAGES_UNTIL_CACHE_EXPIRE = 500

    # initialize
    #
    # Declare basic hash containing setting
    # statistics
    def initialize
      @statistics = {}.with_indifferent_access
    end

    # []
    #
    # Accessor method for the statistics to guarantee
    # proper initialization of hashes.
    def [](name)
      @statistics[name] = { usages: 0, failures: 0 }.with_indifferent_access if @statistics[name].blank?
      @statistics[name]
    end

    # increment_usage
    #
    # Simply increments the number of
    # times a setting has been called
    def increment_usage_of(setting_name)
      self[setting_name][:usages] += 1
      expire_cache_fragment(setting_name) if (self[setting_name][:usages] % USAGES_UNTIL_CACHE_EXPIRE).zero?
    end

    # relative_usage_of
    #
    # Calculates the relative usage of
    # a setting compared to all others
    # in percentage
    def relative_usage_of(setting_name)
      return 0.0 if @statistics.empty?

      (100.0 * self[setting_name][:usages]) / @statistics.map { |_, entry| entry[:usages] }.reduce(:+)
    end

    # increment_failure_of
    #
    # Counts the number of failed code block executions
    # enveloped by a given setting. If the number of failures
    # exceeds the amount configured, resets the setting value
    def increment_failure_of(setting_name)
      self[setting_name][:failures] += 1
      Sail.reset(setting_name) if self[setting_name][:failures] > Sail.configuration.failures_until_reset
    end

    private

    def expire_cache_fragment(setting_name)
      ActionController::Base.new.expire_fragment(/name: "#{setting_name}"/)
    end
  end
end


# frozen_string_literal: true

require_dependency "sail/application_controller"

module Sail
  # SettingsController
  # This is the main controller for settings
  # Implements all actions for the dashboard
  # and for the JSON API
  class SettingsController < ApplicationController
    after_action :log_update, only: %i[update reset], if: -> { Sail.configuration.enable_logging && @successful_update }

    # rubocop:disable AbcSize
    def index
      @settings = Setting.by_query(s_params[:query]).ordered_by(s_params[:order_field])
      @number_of_pages = (@settings.count.to_f / settings_per_page).ceil
      @settings = @settings.paginated(s_params[:page], settings_per_page)
      fresh_when(@settings)
    end
    # rubocop:enable AbcSize

    def update
      respond_to do |format|
        @setting, @successful_update = Setting.set(s_params[:name], s_params[:value])
        format.js {}
        format.json { @successful_update ? head(:ok) : head(:conflict) }
      end
    end

    def show
      respond_to do |format|
        format.json do
          render json: {
            value: Sail::Setting.get(s_params[:name])
          }
        end
      end
    end

    def switcher
      respond_to do |format|
        format.json do
          begin
            render json: {
              value: Sail::Setting.switcher(positive: s_params[:positive],
                                            negative: s_params[:negative],
                                            throttled_by: s_params[:throttled_by])
            }
          rescue Sail::Setting::UnexpectedCastType
            head(:bad_request)
          rescue ActiveRecord::RecordNotFound
            head(:not_found)
          end
        end
      end
    end

    def reset
      respond_to do |format|
        @setting, @successful_update = Setting.reset(s_params[:name])
        format.js { render :update }
      end
    end

    private

    def settings_per_page
      if params[:monitor_mode] == Sail::ConstantCollection::TRUE
        Sail::ConstantCollection::MINIMAL_SETTINGS_PER_PAGE
      else
        Sail::ConstantCollection::SETTINGS_PER_PAGE
      end
    end

    def s_params
      params.permit(:page, :query, :name,
                    :value, :positive, :negative,
                    :throttled_by, :order_field)
    end

    def log_update
      message = +"#{DateTime.now.utc.strftime("%Y/%m/%d %H:%M")} [Sail] #{action_name.capitalize} setting='#{@setting.name}' " \
                 "value='#{@setting.value}'"

      message << " author_user_id=#{current_user.id}" if defined?(current_user)
      Rails.logger.info(message)
    end
  end
end


# frozen_string_literal: true

require 'httparty'
require 'json'

module Gitlab
  # @private
  class Request
    include HTTParty
    format :json
    headers 'Accept' => 'application/json', 'Content-Type' => 'application/x-www-form-urlencoded'
    parser(proc { |body, _| parse(body) })

    attr_accessor :private_token, :endpoint

    # Converts the response body to an ObjectifiedHash.
    def self.parse(body)
      body = decode(body)

      if body.is_a? Hash
        ObjectifiedHash.new body
      elsif body.is_a? Array
        PaginatedResponse.new(body.collect! { |e| ObjectifiedHash.new(e) })
      elsif body
        true
      elsif !body
        false
      elsif body.nil?
        false
      else
        raise Error::Parsing, "Couldn't parse a response body"
      end
    end

    # Decodes a JSON response into Ruby object.
    def self.decode(response)
      response ? JSON.load(response) : {}
    rescue JSON::ParserError
      raise Error::Parsing, 'The response is not a valid JSON'
    end

    %w[get post put delete].each do |method|
      define_method method do |path, options = {}|
        httparty_config(options)
        authorization_header(options)
        validate self.class.send(method, @endpoint + path, options)
      end
    end

    # Checks the response code for common errors.
    # Returns parsed response for successful requests.
    def validate(response)
      error_klass = Error::STATUS_MAPPINGS[response.code]
      raise error_klass, response if error_klass

      parsed = response.parsed_response
      parsed.client = self if parsed.respond_to?(:client=)
      parsed.parse_headers!(response.headers) if parsed.respond_to?(:parse_headers!)
      parsed
    end

    # Sets a base_uri and default_params for requests.
    # @raise [Error::MissingCredentials] if endpoint not set.
    def request_defaults(sudo = nil)
      self.class.default_params sudo: sudo
      raise Error::MissingCredentials, 'Please set an endpoint to API' unless @endpoint

      self.class.default_params.delete(:sudo) if sudo.nil?
    end

    private

    # Sets a PRIVATE-TOKEN or Authorization header for requests.
    #
    # @param [Hash] options A customizable set of options.
    # @option options [Boolean] :unauthenticated true if the API call does not require user authentication.
    # @raise [Error::MissingCredentials] if private_token and auth_token are not set.
    def authorization_header(options)
      return if options[:unauthenticated]
      raise Error::MissingCredentials, 'Please provide a private_token or auth_token for user' unless @private_token

      options[:headers] = if @private_token.size < 21
                            { 'PRIVATE-TOKEN' => @private_token }
                          else
                            { 'Authorization' => "Bearer #{@private_token}" }
                          end
    end

    # Set HTTParty configuration
    # @see https://github.com/jnunemaker/httparty
    def httparty_config(options)
      options.merge!(httparty) if httparty
    end
  end
end


# frozen_string_literal: true

require 'gitlab/cli_helpers'
module Gitlab
  # Defines constants and methods related to configuration.
  module Configuration
    # An array of valid keys in the options hash when configuring a Gitlab::API.
    VALID_OPTIONS_KEYS = %i[endpoint private_token user_agent sudo httparty].freeze

    # The user agent that will be sent to the API endpoint if none is set.
    DEFAULT_USER_AGENT = "Gitlab Ruby Gem #{Gitlab::VERSION}"

    # @private
    attr_accessor(*VALID_OPTIONS_KEYS)
    # @private
    alias auth_token= private_token=

    # Sets all configuration options to their default values
    # when this module is extended.
    def self.extended(base)
      base.reset
    end

    # Convenience method to allow configuration options to be set in a block.
    def configure
      yield self
    end

    # Creates a hash of options and their values.
    def options
      VALID_OPTIONS_KEYS.inject({}) do |option, key|
        option.merge!(key => send(key))
      end
    end

    # Resets all configuration options to the defaults.
    def reset
      self.endpoint       = ENV['GITLAB_API_ENDPOINT']
      self.private_token  = ENV['GITLAB_API_PRIVATE_TOKEN'] || ENV['GITLAB_API_AUTH_TOKEN']
      self.httparty       = get_httparty_config(ENV['GITLAB_API_HTTPARTY_OPTIONS'])
      self.sudo           = nil
      self.user_agent     = DEFAULT_USER_AGENT
    end

    private

    # Allows HTTParty config to be specified in ENV using YAML hash.
    def get_httparty_config(options)
      return if options.nil?

      httparty = Gitlab::CLI::Helpers.yaml_load(options)
      raise ArgumentError, 'HTTParty config should be a Hash.' unless httparty.is_a? Hash

      Gitlab::CLI::Helpers.symbolize_keys httparty
    end
  end
end


# frozen_string_literal: true

module Socketry
  # Timeout subsystem
  module Timeout
    DEFAULT_TIMER = Hitimes::Interval

    # Default timeouts (in seconds)
    DEFAULT_TIMEOUTS = {
      read: 5,
      write: 5,
      connect: 5
    }.freeze

    # Start a timer in the included object
    #
    # @param timer [#start, #to_f] a timer object (ideally monotonic)
    # @return [true] timer started successfully
    # @raise [Socketry::InternalError] if timer is already started
    def start_timer(timer = DEFAULT_TIMER.new)
      raise Socketry::InternalError, "timer already started" if defined?(@timer)
      raise Socketry::InternalError, "deadline already set"  if defined?(@deadline)

      @deadline = nil
      @timer = timer
      @timer.start
      true
    end

    # Return how long since the timer has been started
    #
    # @return [Float] number of seconds since the timer has been started
    # @raise [Socketry::InternalError] if timer has not been started
    def lifetime
      raise Socketry::InternalError, "timer not started" unless @timer

      @timer.to_f
    end

    # Set a timeout. Only one timeout may be active at a given time for a given object.
    #
    # @param timeout [Numeric] number of seconds until the timeout is reached
    # @return [Float] deadline (relative to #lifetime) at which the timeout is reached
    # @raise [Socketry::InternalError] if timeout is already set
    def set_timeout(timeout)
      raise Socketry::InternalError, "deadline already set" if @deadline
      return unless timeout
      raise Socketry::TimeoutError, "time expired" if timeout < 0

      @deadline = lifetime + timeout
    end

    # Clear an already-set timeout
    #
    # @param timeout [Numeric] to gauge whether the timeout actually needs to be cleared
    # @raise [Socketry::InternalError] if timeout has not been set
    def clear_timeout(timeout)
      return unless timeout
      raise Socketry::InternalError, "no deadline set" unless @deadline

      @deadline = nil
    end

    # Calculate number of seconds remaining until we hit the timeout
    #
    # @param timeout [Numeric] to gauge whether a timeout needs to be calculated
    # @return [Float] number of seconds remaining until we hit the timeout
    # @raise [Socketry::TimeoutError] if we've already hit the timeout
    # @raise [Socketry::InternalError] if timeout has not been set
    def time_remaining(timeout)
      return unless timeout
      raise Socketry::InternalError, "no deadline set" unless @deadline

      remaining = @deadline - lifetime
      raise Socketry::TimeoutError, "time expired" if remaining <= 0

      remaining
    end
  end
end


require 'open-uri'
require 'pathname'
require 'bigdecimal'
require 'digest/sha1'
require 'date'
require 'thread'
require 'yaml'

require 'json-schema/schema/reader'
require 'json-schema/errors/schema_error'
require 'json-schema/errors/schema_parse_error'
require 'json-schema/errors/json_load_error'
require 'json-schema/errors/json_parse_error'
require 'json-schema/util/uri'

module JSON

  class Validator

    @@schemas = {}
    @@cache_schemas = true
    @@default_opts = {
      :list => false,
      :version => nil,
      :validate_schema => false,
      :record_errors => false,
      :errors_as_objects => false,
      :insert_defaults => false,
      :clear_cache => false,
      :strict => false,
      :parse_data => true
    }
    @@validators = {}
    @@default_validator = nil
    @@available_json_backends = []
    @@json_backend = nil
    @@serializer = nil
    @@mutex = Mutex.new

    def initialize(schema_data, data, opts={})
      @options = @@default_opts.clone.merge(opts)
      @errors = []

      validator = self.class.validator_for_name(@options[:version])
      @options[:version] = validator
      @options[:schema_reader] ||= self.class.schema_reader

      @validation_options = @options[:record_errors] ? {:record_errors => true} : {}
      @validation_options[:insert_defaults] = true if @options[:insert_defaults]
      @validation_options[:strict] = true if @options[:strict] == true
      @validation_options[:clear_cache] = true if !@@cache_schemas || @options[:clear_cache]

      @@mutex.synchronize { @base_schema = initialize_schema(schema_data) }
      @original_data = data
      @data = initialize_data(data)
      @@mutex.synchronize { build_schemas(@base_schema) }

      # validate the schema, if requested
      if @options[:validate_schema]
        if @base_schema.schema["$schema"]
          base_validator = self.class.validator_for_name(@base_schema.schema["$schema"])
        end
        metaschema = base_validator ? base_validator.metaschema : validator.metaschema
        # Don't clear the cache during metaschema validation!
        self.class.validate!(metaschema, @base_schema.schema, {:clear_cache => false})
      end

      # If the :fragment option is set, try and validate against the fragment
      if opts[:fragment]
        @base_schema = schema_from_fragment(@base_schema, opts[:fragment])
      end
    end

    def schema_from_fragment(base_schema, fragment)
      schema_uri = base_schema.uri
      fragments = fragment.split("/")

      # ensure the first element was a hash, per the fragment spec
      if fragments.shift != "#"
        raise JSON::Schema::SchemaError.new("Invalid fragment syntax in :fragment option")
      end

      fragments.each do |f|
        if base_schema.is_a?(JSON::Schema) #test if fragment is a JSON:Schema instance
          if !base_schema.schema.has_key?(f)
            raise JSON::Schema::SchemaError.new("Invalid fragment resolution for :fragment option")
          end
          base_schema = base_schema.schema[f]
        elsif base_schema.is_a?(Hash)
          if !base_schema.has_key?(f)
            raise JSON::Schema::SchemaError.new("Invalid fragment resolution for :fragment option")
          end
          base_schema = JSON::Schema.new(base_schema[f],schema_uri,@options[:version])
        elsif base_schema.is_a?(Array)
          if base_schema[f.to_i].nil?
            raise JSON::Schema::SchemaError.new("Invalid fragment resolution for :fragment option")
          end
          base_schema = JSON::Schema.new(base_schema[f.to_i],schema_uri,@options[:version])
        else
          raise JSON::Schema::SchemaError.new("Invalid schema encountered when resolving :fragment option")
        end
      end

      if @options[:list]
        base_schema.to_array_schema
      elsif base_schema.is_a?(Hash)
        JSON::Schema.new(base_schema, schema_uri, @options[:version])
      else
        base_schema
      end
    end

    # Run a simple true/false validation of data against a schema
    def validate
      @base_schema.validate(@data,[],self,@validation_options)

      if @options[:record_errors]
        if @options[:errors_as_objects]
          @errors.map{|e| e.to_hash}
        else
          @errors.map{|e| e.to_string}
        end
      else
        true
      end
    ensure
      if @validation_options[:clear_cache] == true
        self.class.clear_cache
      end
      if @validation_options[:insert_defaults]
        self.class.merge_missing_values(@data, @original_data)
      end
    end

    def load_ref_schema(parent_schema, ref)
      schema_uri = JSON::Util::URI.absolutize_ref(ref, parent_schema.uri)
      return true if self.class.schema_loaded?(schema_uri)

      validator = self.class.validator_for_uri(schema_uri, false)
      schema_uri = JSON::Util::URI.file_uri(validator.metaschema) if validator

      schema = @options[:schema_reader].read(schema_uri)
      self.class.add_schema(schema)
      build_schemas(schema)
    end

    # Build all schemas with IDs, mapping out the namespace
    def build_schemas(parent_schema)
      schema = parent_schema.schema

      # Build ref schemas if they exist
      if schema["$ref"]
        load_ref_schema(parent_schema, schema["$ref"])
      end

      case schema["extends"]
      when String
        load_ref_schema(parent_schema, schema["extends"])
      when Array
        schema['extends'].each do |type|
          handle_schema(parent_schema, type)
        end
      end

      # Check for schemas in union types
      ["type", "disallow"].each do |key|
        if schema[key].is_a?(Array)
          schema[key].each do |type|
            if type.is_a?(Hash)
              handle_schema(parent_schema, type)
            end
          end
        end
      end

      # Schema properties whose values are objects, the values of which
      # are themselves schemas.
      %w[definitions properties patternProperties].each do |key|
        next unless value = schema[key]
        value.each do |k, inner_schema|
          handle_schema(parent_schema, inner_schema)
        end
      end

      # Schema properties whose values are themselves schemas.
      %w[additionalProperties additionalItems dependencies extends].each do |key|
        next unless schema[key].is_a?(Hash)
        handle_schema(parent_schema, schema[key])
      end

      # Schema properties whose values may be an array of schemas.
      %w[allOf anyOf oneOf not].each do |key|
        next unless value = schema[key]
        Array(value).each do |inner_schema|
          handle_schema(parent_schema, inner_schema)
        end
      end

      # Items are always schemas
      if schema["items"]
        items = schema["items"].clone
        items = [items] unless items.is_a?(Array)

        items.each do |item|
          handle_schema(parent_schema, item)
        end
      end

      # Convert enum to a ArraySet
      if schema["enum"].is_a?(Array)
        schema["enum"] = ArraySet.new(schema["enum"])
      end

    end

    # Either load a reference schema or create a new schema
    def handle_schema(parent_schema, obj)
      if obj.is_a?(Hash)
        schema_uri = parent_schema.uri.dup
        schema = JSON::Schema.new(obj, schema_uri, parent_schema.validator)
        if obj['id']
          self.class.add_schema(schema)
        end
        build_schemas(schema)
      end
    end

    def validation_error(error)
      @errors.push(error)
    end

    def validation_errors
      @errors
    end

    class << self
      def validate(schema, data,opts={})
        begin
          validate!(schema, data, opts)
        rescue JSON::Schema::ValidationError, JSON::Schema::SchemaError
          return false
        end
      end

      def validate_json(schema, data, opts={})
        validate(schema, data, opts.merge(:json => true))
      end

      def validate_uri(schema, data, opts={})
        validate(schema, data, opts.merge(:uri => true))
      end

      def validate!(schema, data,opts={})
        validator = new(schema, data, opts)
        validator.validate
      end

      def validate2(schema, data, opts={})
        warn "[DEPRECATION NOTICE] JSON::Validator#validate2 has been replaced by JSON::Validator#validate! and will be removed in version >= 3. Please use the #validate! method instead."
        validate!(schema, data, opts)
      end

      def validate_json!(schema, data, opts={})
        validate!(schema, data, opts.merge(:json => true))
      end

      def validate_uri!(schema, data, opts={})
        validate!(schema, data, opts.merge(:uri => true))
      end

      def fully_validate(schema, data, opts={})
        validate!(schema, data, opts.merge(:record_errors => true))
      end

      def fully_validate_schema(schema, opts={})
        data = schema
        schema = validator_for_name(opts[:version]).metaschema
        fully_validate(schema, data, opts)
      end

      def fully_validate_json(schema, data, opts={})
        fully_validate(schema, data, opts.merge(:json => true))
      end

      def fully_validate_uri(schema, data, opts={})
        fully_validate(schema, data, opts.merge(:uri => true))
      end

      def schema_reader
        @@schema_reader ||= JSON::Schema::Reader.new
      end

      def schema_reader=(reader)
        @@schema_reader = reader
      end

      def clear_cache
        @@schemas = {}
        JSON::Util::URI.clear_cache
      end

      def schemas
        @@schemas
      end

      def add_schema(schema)
        @@schemas[schema_key_for(schema.uri)] ||= schema
      end

      def schema_for_uri(uri)
        # We only store normalized uris terminated with fragment #, so we can try whether
        # normalization can be skipped
        @@schemas[uri] || @@schemas[schema_key_for(uri)]
      end

      def schema_loaded?(schema_uri)
        !schema_for_uri(schema_uri).nil?
      end

      def schema_key_for(uri)
        key = Util::URI.normalized_uri(uri).to_s
        key.end_with?('#') ? key : "#{key}#"
      end

      def cache_schemas=(val)
        warn "[DEPRECATION NOTICE] Schema caching is now a validation option. Schemas will still be cached if this is set to true, but this method will be removed in version >= 3. Please use the :clear_cache validation option instead."
        @@cache_schemas = val == true ? true : false
      end

      def validators
        @@validators
      end

      def default_validator
        @@default_validator
      end

      def validator_for_uri(schema_uri, raise_not_found=true)
        return default_validator unless schema_uri
        u = JSON::Util::URI.parse(schema_uri)
        validator = validators["#{u.scheme}://#{u.host}#{u.path}"]
        if validator.nil? && raise_not_found
          raise JSON::Schema::SchemaError.new("Schema not found: #{schema_uri}")
        else
          validator
        end
      end

      def validator_for_name(schema_name, raise_not_found=true)
        return default_validator unless schema_name
        schema_name = schema_name.to_s
        validator = validators.values.detect do |v|
          Array(v.names).include?(schema_name)
        end
        if validator.nil? && raise_not_found
          raise JSON::Schema::SchemaError.new("The requested JSON schema version is not supported")
        else
          validator
        end
      end

      def validator_for(schema_uri)
        warn "[DEPRECATION NOTICE] JSON::Validator#validator_for has been replaced by JSON::Validator#validator_for_uri and will be removed in version >= 3. Please use the #validator_for_uri method instead."
        validator_for_uri(schema_uri)
      end

      def register_validator(v)
        @@validators["#{v.uri.scheme}://#{v.uri.host}#{v.uri.path}"] = v
      end

      def register_default_validator(v)
        @@default_validator = v
      end

      def register_format_validator(format, validation_proc, versions = (@@validators.flat_map{ |k, v| v.names.first } + [nil]))
        custom_format_validator = JSON::Schema::CustomFormat.new(validation_proc)
        versions.each do |version|
          validator = validator_for_name(version)
          validator.formats[format.to_s] = custom_format_validator
        end
      end

      def deregister_format_validator(format, versions = (@@validators.flat_map{ |k, v| v.names.first } + [nil]))
        versions.each do |version|
          validator = validator_for_name(version)
          validator.formats[format.to_s] = validator.default_formats[format.to_s]
        end
      end

      def restore_default_formats(versions = (@@validators.flat_map{ |k, v| v.names.first } + [nil]))
        versions.each do |version|
          validator = validator_for_name(version)
          validator.formats = validator.default_formats.clone
        end
      end

      def json_backend
        if defined?(MultiJson)
          MultiJson.respond_to?(:adapter) ? MultiJson.adapter : MultiJson.engine
        else
          @@json_backend
        end
      end

      def json_backend=(backend)
        if defined?(MultiJson)
          backend = backend == 'json' ? 'json_gem' : backend
          MultiJson.respond_to?(:use) ? MultiJson.use(backend) : MultiJson.engine = backend
        else
          backend = backend.to_s
          if @@available_json_backends.include?(backend)
            @@json_backend = backend
          else
            raise JSON::Schema::JsonParseError.new("The JSON backend '#{backend}' could not be found.")
          end
        end
      end

      def parse(s)
        if defined?(MultiJson)
          begin
            MultiJson.respond_to?(:adapter) ? MultiJson.load(s) : MultiJson.decode(s)
          rescue MultiJson::ParseError => e
            raise JSON::Schema::JsonParseError.new(e.message)
          end
        else
          case @@json_backend.to_s
          when 'json'
            begin
              JSON.parse(s, :quirks_mode => true)
            rescue JSON::ParserError => e
              raise JSON::Schema::JsonParseError.new(e.message)
            end
          when 'yajl'
            begin
              json = StringIO.new(s)
              parser = Yajl::Parser.new
              parser.parse(json) or raise JSON::Schema::JsonParseError.new("The JSON could not be parsed by yajl")
            rescue Yajl::ParseError => e
              raise JSON::Schema::JsonParseError.new(e.message)
            end
          else
            raise JSON::Schema::JsonParseError.new("No supported JSON parsers found. The following parsers are suported:\n * yajl-ruby\n * json")
          end
        end
      end

      def merge_missing_values(source, destination)
        case destination
        when Hash
          source.each do |key, source_value|
            destination_value = destination[key] || destination[key.to_sym]
            if destination_value.nil?
              destination[key] = source_value
            else
              merge_missing_values(source_value, destination_value)
            end
          end
        when Array
          source.each_with_index do |source_value, i|
            destination_value = destination[i]
            merge_missing_values(source_value, destination_value)
          end
        end
      end

      if !defined?(MultiJson)
        if Gem::Specification::find_all_by_name('json').any?
          require 'json'
          @@available_json_backends << 'json'
          @@json_backend = 'json'
        else
          # Try force-loading json for rubies > 1.9.2
          begin
            require 'json'
            @@available_json_backends << 'json'
            @@json_backend = 'json'
          rescue LoadError
          end
        end

        if Gem::Specification::find_all_by_name('yajl-ruby').any?
          require 'yajl'
          @@available_json_backends << 'yajl'
          @@json_backend = 'yajl'
        end

        if @@json_backend == 'yajl'
          @@serializer = lambda{|o| Yajl::Encoder.encode(o) }
        elsif @@json_backend == 'json'
          @@serializer = lambda{|o| JSON.dump(o) }
        else
          @@serializer = lambda{|o| YAML.dump(o) }
        end
      end
    end

    private

    if Gem::Specification::find_all_by_name('uuidtools').any?
      require 'uuidtools'
      @@fake_uuid_generator = lambda{|s| UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, s).to_s }
    else
      require 'json-schema/util/uuid'
      @@fake_uuid_generator = lambda{|s| JSON::Util::UUID.create_v5(s,JSON::Util::UUID::Nil).to_s }
    end

    def serialize schema
      if defined?(MultiJson)
        MultiJson.respond_to?(:dump) ? MultiJson.dump(schema) : MultiJson.encode(schema)
      else
        @@serializer.call(schema)
      end
    end

    def fake_uuid schema
      @@fake_uuid_generator.call(schema)
    end

    def initialize_schema(schema)
      if schema.is_a?(String)
        begin
          # Build a fake URI for this
          schema_uri = JSON::Util::URI.parse(fake_uuid(schema))
          schema = JSON::Schema.new(self.class.parse(schema), schema_uri, @options[:version])
          if @options[:list] && @options[:fragment].nil?
            schema = schema.to_array_schema
          end
          self.class.add_schema(schema)
        rescue JSON::Schema::JsonParseError
          # Build a uri for it
          schema_uri = Util::URI.normalized_uri(schema)
          if !self.class.schema_loaded?(schema_uri)
            schema = @options[:schema_reader].read(schema_uri)
            schema = JSON::Schema.stringify(schema)

            if @options[:list] && @options[:fragment].nil?
              schema = schema.to_array_schema
            end

            self.class.add_schema(schema)
          else
            schema = self.class.schema_for_uri(schema_uri)
            if @options[:list] && @options[:fragment].nil?
              schema = schema.to_array_schema
              schema.uri = JSON::Util::URI.parse(fake_uuid(serialize(schema.schema)))
              self.class.add_schema(schema)
            end
            schema
          end
        end
      elsif schema.is_a?(Hash)
        schema_uri = JSON::Util::URI.parse(fake_uuid(serialize(schema)))
        schema = JSON::Schema.stringify(schema)
        schema = JSON::Schema.new(schema, schema_uri, @options[:version])
        if @options[:list] && @options[:fragment].nil?
          schema = schema.to_array_schema
        end
        self.class.add_schema(schema)
      else
        raise JSON::Schema::SchemaParseError, "Invalid schema - must be either a string or a hash"
      end

      schema
    end

    def initialize_data(data)
      if @options[:parse_data]
        if @options[:json]
          data = self.class.parse(data)
        elsif @options[:uri]
          json_uri = Util::URI.normalized_uri(data)
          data = self.class.parse(custom_open(json_uri))
        elsif data.is_a?(String)
          begin
            data = self.class.parse(data)
          rescue JSON::Schema::JsonParseError
            begin
              json_uri = Util::URI.normalized_uri(data)
              data = self.class.parse(custom_open(json_uri))
            rescue JSON::Schema::JsonLoadError, JSON::Schema::UriError
              # Silently discard the error - use the data as-is
            end
          end
        end
      end
      JSON::Schema.stringify(data)
    end

    def custom_open(uri)
      uri = Util::URI.normalized_uri(uri) if uri.is_a?(String)
      if uri.absolute? && Util::URI::SUPPORTED_PROTOCOLS.include?(uri.scheme)
        begin
          open(uri.to_s).read
        rescue OpenURI::HTTPError, Timeout::Error => e
          raise JSON::Schema::JsonLoadError, e.message
        end
      else
        begin
          File.read(JSON::Util::URI.unescaped_path(uri))
        rescue SystemCallError => e
          raise JSON::Schema::JsonLoadError, e.message
        end
      end
    end
  end
end