# frozen_string_literal: true

module PGTrunk
  # @private
  # The qualified name of an object consists from schema (namespace) and name.
  # The class contains several helper methods for ruby and sql snippets.
  QualifiedName = Struct.new(:schema, :name) do
    include Comparable

    # Build qualified name structure from a string
    #
    # @example
    #   QualifiedName["bar"].to_h
    #   # => { schema: "public", name: "bar" }
    #
    #   QualifiedName["foo.bar(a int, b int)"].to_h
    #   # => { schema: "foo", name: "bar(a int, b int)" }
    #
    def self.wrap(string)
      schema = /^([^.(]+)[.]/.match(string).to_a[1]
      name = string.sub(/^[^.(]*[.]/, "")
      new(schema, name)
    end

    # Get the current schema
    def current_schema
      @current_schema ||= PGTrunk.database.current_schema
    end

    private def initialize(schema, name)
      super(schema.to_s.presence || current_schema, name.to_s.presence)
    end

    # If the schema is current (which is usually 'public')
    # This schema would be ignored in +lean+
    def current_schema?
      schema == current_schema
    end

    # If the qualified name is blank (not specified)
    def blank?
      routine.blank?
    end

    # If the name matches the regex
    def match?(regex)
      routine.match?(regex)
    end

    # If the name is present but not matches the regexp
    # (used to check if it is not generated by some pattern)
    def differs_from?(regex)
      !routine.match?(regex) if present?
    end

    # Quoted representation of the name for SQL snippets
    #
    #   QualifiedName.wrap("foo.bar(a int, OUT b int) record").to_sql
    #   # => '"foo"."bar" (a int)'
    #
    #   qname = QualifiedName.wrap("public.foo")
    #   qname.to_sql
    #   # => '"foo"'
    #   qname.to_sql(true)
    #   # => '"foo"()'
    def to_sql(with_args = nil)
      @to_sql ||= begin
        str = [
          *(schema unless current_schema?),
          routine,
        ].map(&:inspect).join(".")
        str = "#{str} (#{args})" if args.present? || with_args
        str
      end
    end

    # The qualified name with unquoted parts
    # (to be used in ruby snippets)
    #
    #   QualifiedName.wrap("bar.foo").full
    #   # => "bar.foo"
    #
    #   QualifiedName.wrap("public.foo(int, int) int").full
    #   # => "foo(int, int) int"
    def full
      @full ||= [schema, name].join(".")
    end

    # Exclude current schema from a qualified name
    #
    #   QualifiedName.wrap("public.foo").lean
    #   # => "foo"
    #
    #   QualifiedName.wrap("bar.baz").lean
    #   # => "bar.baz"
    def lean
      @lean ||= [*(schema unless current_schema?), name].join(".")
    end

    # Name of the routine for function definition
    #   QualifiedName.wrap("foo.bar(a int, b int) int").routine
    #   # => "bar"
    def routine
      @routine ||= name[/^[^(]+/]&.strip
    end

    # Args from the function definition
    #   QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
    #   # => "a int, b int DEFAULT = 0"
    def args
      @args ||= name&.match(/\(([^)]+)/).to_a.last&.gsub(/\s+/, " ")&.strip
    end

    # Arg types from the function definition
    #   QualifiedName.wrap("foo.bar(a int, b int DEFAULT = 0) int").args
    #   # => %w[int int]
    def arg_types
      @arg_types ||= args.to_s.split(",").map { |a| a.strip.split(/\s+/).last }
    end

    # Type of returned value of a routine
    #   QualifiedName.wrap("foo.concat(a int, b int) text").returns
    #   # => "text"
    def returns
      @returns ||= name&.match(/\)(.+)/).to_a.last&.strip
    end

    # Quote the name (to compare to text fields in the database)
    #   QualifiedName.wrap("foo.concat(a int, b int) text").quoted
    #   # => "'concat'"
    def quoted
      @quoted ||= PGTrunk.database.quote(routine)
    end

    # Transform the namespace (to compare to oid fields in the database)
    #   QualifiedName.wrap("foo.bar").namespace
    #   # => "'foo'::regnamespace"
    def namespace
      @namespace ||= "#{PGTrunk.database.quote(schema)}::regnamespace"
    end

    # Make qualified names comparable by their full representation
    # @return [Integer, NilClass]
    def <=>(other)
      sort_order <=> other.sort_order if other.is_a?(self.class)
    end

    # Checks if partially qualified <function>
    # have the same schema/name as another function
    def maybe_eq?(other)
      sort_order.first(2) == other.sort_order.first(2)
    end

    # Build the name with a changed part (either a name or a schema)
    def merge(**args)
      self.class.new(args.fetch(:schema, schema), args.fetch(:name, name))
    end

    protected

    def sort_order
      [schema.to_s, routine.to_s, *arg_types]
    end
  end
end
