# frozen-string-literal: true

module Sequel
  class Dataset
    # ---------------------
    # :section: 6 - Miscellaneous methods
    # These methods don't fit cleanly into another section.
    # ---------------------
    
    # The database related to this dataset.  This is the Database instance that
    # will execute all of this dataset's queries.
    attr_reader :db

    # The hash of options for this dataset, keys are symbols.
    attr_reader :opts

    # Constructs a new Dataset instance with an associated database and 
    # options. Datasets are usually constructed by invoking the Database#[] method:
    #
    #   DB[:posts]
    #
    # Sequel::Dataset is an abstract class that is not useful by itself. Each
    # database adapter provides a subclass of Sequel::Dataset, and has
    # the Database#dataset method return an instance of that subclass.
    def initialize(db)
      @db = db
      @opts = OPTS
      @cache = {}
      freeze
    end

    # Define a hash value such that datasets with the same class, DB, and opts
    # will be considered equal.
    def ==(o)
      o.is_a?(self.class) && db == o.db && opts == o.opts
    end

    # An object representing the current date or time, should be an instance
    # of Sequel.datetime_class.
    def current_datetime
      Sequel.datetime_class.now
    end

    # Alias for ==
    def eql?(o)
      self == o
    end

    # Return self, as datasets are always frozen.
    def dup
      self
    end
    
    # Yield a dataset for each server in the connection pool that is tied to that server.
    # Intended for use in sharded environments where all servers need to be modified
    # with the same data:
    #
    #   DB[:configs].where(key: 'setting').each_server{|ds| ds.update(value: 'new_value')}
    def each_server
      db.servers.each{|s| yield server(s)}
    end

    # Returns the string with the LIKE metacharacters (% and _) escaped.
    # Useful for when the LIKE term is a user-provided string where metacharacters should not
    # be recognized. Example:
    #
    #   ds.escape_like("foo\\%_") # 'foo\\\%\_'
    def escape_like(string)
      string.gsub(/[\\%_]/){|m| "\\#{m}"}
    end

    if TRUE_FREEZE
      # Freeze the opts when freezing the dataset.
      def freeze
        @opts.freeze
        super
      end
    else
      # :nocov:
      def freeze # :nodoc:
        self
      end

      def frozen?  # :nodoc:
        true
      end
      # :nocov:
    end
   
    # Alias of +first_source_alias+
    def first_source
      first_source_alias
    end

    # The first source (primary table) for this dataset.  If the dataset doesn't
    # have a table, raises an +Error+.  If the table is aliased, returns the aliased name.
    #
    #   DB[:table].first_source_alias
    #   # => :table
    #
    #   DB[Sequel[:table].as(:t)].first_source_alias
    #   # => :t
    def first_source_alias
      source = @opts[:from]
      if source.nil? || source.empty?
        raise Error, 'No source specified for query'
      end
      case s = source.first
      when SQL::AliasedExpression
        s.alias
      when Symbol
        _, _, aliaz = split_symbol(s)
        aliaz ? aliaz.to_sym : s
      else
        s
      end
    end
    
    # The first source (primary table) for this dataset.  If the dataset doesn't
    # have a table, raises an error.  If the table is aliased, returns the original
    # table, not the alias
    #
    #   DB[:table].first_source_table
    #   # => :table
    #
    #   DB[Sequel[:table].as(:t)].first_source_table
    #   # => :table
    def first_source_table
      source = @opts[:from]
      if source.nil? || source.empty?
        raise Error, 'No source specified for query'
      end
      case s = source.first
      when SQL::AliasedExpression
        s.expression
      when Symbol
        sch, table, aliaz = split_symbol(s)
        aliaz ? (sch ? SQL::QualifiedIdentifier.new(sch, table) : table.to_sym) : s
      else
        s
      end
    end

    # Define a hash value such that datasets with the same class, DB, and opts,
    # will have the same hash value.
    def hash
      [self.class, db, opts].hash
    end
    
    # Returns a string representation of the dataset including the class name 
    # and the corresponding SQL select statement.
    def inspect
      "#<#{visible_class_name}: #{sql.inspect}>"
    end
    
    # Whether this dataset is a joined dataset (multiple FROM tables or any JOINs).
    def joined_dataset?
     !!((opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join])
    end

    # The alias to use for the row_number column, used when emulating OFFSET
    # support and for eager limit strategies
    def row_number_column
      :x_sequel_row_number_x
    end

    # The row_proc for this database, should be any object that responds to +call+ with
    # a single hash argument and returns the object you want #each to return.
    def row_proc
      @opts[:row_proc]
    end
    
    # Splits a possible implicit alias in +c+, handling both SQL::AliasedExpressions
    # and Symbols.  Returns an array of two elements, with the first being the
    # main expression, and the second being the alias.
    def split_alias(c)
      case c
      when Symbol
        c_table, column, aliaz = split_symbol(c)
        [c_table ? SQL::QualifiedIdentifier.new(c_table, column.to_sym) : column.to_sym, aliaz]
      when SQL::AliasedExpression
        [c.expression, c.alias]
      when SQL::JoinClause
        [c.table, c.table_alias]
      else
        [c, nil]
      end
    end

    # This returns an SQL::Identifier or SQL::AliasedExpression containing an
    # SQL identifier that represents the unqualified column for the given value.
    # The given value should be a Symbol, SQL::Identifier, SQL::QualifiedIdentifier,
    # or SQL::AliasedExpression containing one of those.  In other cases, this
    # returns nil.
    def unqualified_column_for(v)
      unless v.is_a?(String)
        _unqualified_column_for(v)
      end
    end

    # Creates a unique table alias that hasn't already been used in the dataset.
    # table_alias can be any type of object accepted by alias_symbol.
    # The symbol returned will be the implicit alias in the argument,
    # possibly appended with "_N" if the implicit alias has already been
    # used, where N is an integer starting at 0 and increasing until an
    # unused one is found.
    #
    # You can provide a second addition array argument containing symbols
    # that should not be considered valid table aliases.  The current aliases
    # for the FROM and JOIN tables are automatically included in this array.
    #
    #   DB[:table].unused_table_alias(:t)
    #   # => :t
    #
    #   DB[:table].unused_table_alias(:table)
    #   # => :table_0
    #
    #   DB[:table, :table_0].unused_table_alias(:table)
    #   # => :table_1
    #
    #   DB[:table, :table_0].unused_table_alias(:table, [:table_1, :table_2])
    #   # => :table_3
    def unused_table_alias(table_alias, used_aliases = [])
      table_alias = alias_symbol(table_alias)
      used_aliases += opts[:from].map{|t| alias_symbol(t)} if opts[:from]
      used_aliases += opts[:join].map{|j| j.table_alias ? alias_alias_symbol(j.table_alias) : alias_symbol(j.table)} if opts[:join]
      if used_aliases.include?(table_alias)
        i = 0
        while true
          ta = :"#{table_alias}_#{i}"
          return ta unless used_aliases.include?(ta)
          i += 1 
        end
      else
        table_alias
      end
    end

    # Return a modified dataset with quote_identifiers set.
    def with_quote_identifiers(v)
      clone(:quote_identifiers=>v, :skip_symbol_cache=>true)
    end
      
    protected

    # Access the cache for the current dataset.  Should be used with caution,
    # as access to the cache is not thread safe without a mutex if other
    # threads can reference the dataset.  Symbol keys prefixed with an
    # underscore are reserved for internal use.
    attr_reader :cache

    # Retreive a value from the dataset's cache in a thread safe manner.
    def cache_get(k)
      Sequel.synchronize{@cache[k]}
    end

    # Set a value in the dataset's cache in a thread safe manner.
    def cache_set(k, v)
      Sequel.synchronize{@cache[k] = v}
    end

    # Clear the columns hash for the current dataset.  This is not a
    # thread safe operation, so it should only be used if the dataset
    # could not be used by another thread (such as one that was just
    # created via clone).
    def clear_columns_cache
      @cache.delete(:_columns)
    end

    # The cached columns for the current dataset.
    def _columns
      cache_get(:_columns)
    end

    private

    # Check the cache for the given key, returning the value.
    # Otherwise, yield to get the dataset and cache the dataset under the given key.
    def cached_dataset(key)
      unless ds = cache_get(key)
        ds = yield
        cache_set(key, ds)
      end

      ds
    end

    # Return a cached placeholder literalizer for the given key if there
    # is one for this dataset.  If there isn't one, increment the counter
    # for the number of calls for the key, and if the counter is at least
    # three, then create a placeholder literalizer by yielding to the block,
    # and cache it.
    def cached_placeholder_literalizer(key)
      if loader = cache_get(key)
        return loader unless loader.is_a?(Integer)
        loader += 1

        if loader >= 3
          loader = Sequel::Dataset::PlaceholderLiteralizer.loader(self){|pl, _| yield pl}
          cache_set(key, loader)
        else
          cache_set(key, loader + 1)
          loader = nil
        end
      elsif cache_sql?
        cache_set(key, 1)
      end

      loader
    end

    # Return a cached placeholder literalizer for the key, unless where_block is
    # nil and where_args is an empty array or hash.  This is designed to guard
    # against placeholder literalizer use when passing arguments to where
    # in the uncached case and filter_expr if a cached placeholder literalizer
    # is used.
    def cached_where_placeholder_literalizer(where_args, where_block, key, &block)
      where_args = where_args[0] if where_args.length == 1
      unless where_block
        return if where_args == OPTS || where_args == EMPTY_ARRAY
      end

      cached_placeholder_literalizer(key, &block)
    end

    # Set the columns for the current dataset.
    def columns=(v)
      cache_set(:_columns, v)
    end

    # Set the db, opts, and cache for the copy of the dataset.
    def initialize_clone(c, _=nil)
      @db = c.db
      @opts = Hash[c.opts]
      if cols = c.cache_get(:_columns)
        @cache = {:_columns=>cols}
      else
        @cache = {}
      end
    end
    alias initialize_copy initialize_clone

    # Internal recursive version of unqualified_column_for, handling Strings inside
    # of other objects.
    def _unqualified_column_for(v)
      case v
      when Symbol
        _, c, a = Sequel.split_symbol(v)
        c = Sequel.identifier(c)
        a ? c.as(a) : c
      when String
        Sequel.identifier(v)
      when SQL::Identifier
        v
      when SQL::QualifiedIdentifier
        _unqualified_column_for(v.column)
      when SQL::AliasedExpression
        if expr = unqualified_column_for(v.expression)
          SQL::AliasedExpression.new(expr, v.alias)
        end
      end
    end

    # Return the class name for this dataset, but skip anonymous classes
    def visible_class_name
      c = self.class
      c = c.superclass while c.name.nil? || c.name == ''
      c.name
    end
  end
end
