require 'digest/sha1'

module ActiveRecord
  module ConnectionAdapters
    module OracleEnhancedSchemaStatements
      # SCHEMA STATEMENTS ========================================
      #
      # see: abstract/schema_statements.rb

      # Additional options for +create_table+ method in migration files.
      #
      # You can specify individual starting value in table creation migration file, e.g.:
      #
      #   create_table :users, :sequence_start_value => 100 do |t|
      #     # ...
      #   end
      #
      # You can also specify other sequence definition additional parameters, e.g.:
      #
      #   create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t|
      #     # ...
      #   end
      #
      # Create primary key trigger (so that you can skip primary key value in INSERT statement).
      # By default trigger name will be "table_name_pkt", you can override the name with
      # :trigger_name option (but it is not recommended to override it as then this trigger will
      # not be detected by ActiveRecord model and it will still do prefetching of sequence value).
      # Example:
      #
      #   create_table :users, :primary_key_trigger => true do |t|
      #     # ...
      #   end
      #
      # It is possible to add table and column comments in table creation migration files:
      #
      #   create_table :employees, :comment => “Employees and contractors” do |t|
      #     t.string      :first_name, :comment => “Given name”
      #     t.string      :last_name, :comment => “Surname”
      #   end

      def create_table(name, options = {}, &block)
        create_sequence = options[:id] != false
        column_comments = {}

        table_definition = TableDefinition.new(self)
        table_definition.primary_key(options[:primary_key] || Base.get_primary_key(name.to_s.singularize)) unless options[:id] == false

        # store that primary key was defined in create_table block
        unless create_sequence
          class << table_definition
            attr_accessor :create_sequence
            def primary_key(*args)
              self.create_sequence = true
              super(*args)
            end
          end
        end

        # store column comments
        class << table_definition
          attr_accessor :column_comments
          def column(name, type, options = {})
            if options[:comment]
              self.column_comments ||= {}
              self.column_comments[name] = options[:comment]
            end
            super(name, type, options)
          end
        end

        result = block.call(table_definition) if block
        create_sequence = create_sequence || table_definition.create_sequence
        column_comments = table_definition.column_comments if table_definition.column_comments
        tablespace = tablespace_for(:table, options[:tablespace])

        if options[:force] && table_exists?(name)
          drop_table(name, options)
        end

        create_sql = "CREATE#{' GLOBAL TEMPORARY' if options[:temporary]} TABLE "
        create_sql << quote_table_name(name)
        create_sql << " (#{table_definition.to_sql})"
        unless options[:temporary]
          create_sql << " ORGANIZATION #{options[:organization]}" if options[:organization]
          create_sql << tablespace
          table_definition.lob_columns.each{|cd| create_sql << tablespace_for(cd.sql_type.downcase.to_sym, nil, name, cd.name)}
        end
        create_sql << " #{options[:options]}"
        execute create_sql

        create_sequence_and_trigger(name, options) if create_sequence

        add_table_comment name, options[:comment]
        column_comments.each do |column_name, comment|
          add_comment name, column_name, comment
        end

      end

      def rename_table(name, new_name) #:nodoc:
        if new_name.to_s.length > table_name_length
          raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{table_name_length} characters"
        end
        if "#{new_name}_seq".to_s.length > sequence_name_length
          raise ArgumentError, "New sequence name '#{new_name}_seq' is too long; the limit is #{sequence_name_length} characters"
        end
        execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
        execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}"
      end

      def drop_table(name, options = {}) #:nodoc:
        super(name)
        seq_name = options[:sequence_name] || default_sequence_name(name)
        execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil
      ensure
        clear_table_columns_cache(name)
      end

      def initialize_schema_migrations_table
        sm_table = ActiveRecord::Migrator.schema_migrations_table_name

        unless table_exists?(sm_table)
          index_name = "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
          if index_name.length > index_name_length
            truncate_to    = index_name_length - index_name.to_s.length - 1
            truncated_name = "unique_schema_migrations"[0..truncate_to]
            index_name     = "#{Base.table_name_prefix}#{truncated_name}#{Base.table_name_suffix}"
          end

          create_table(sm_table, :id => false) do |schema_migrations_table|
            schema_migrations_table.column :version, :string, :null => false
          end
          add_index sm_table, :version, :unique => true, :name => index_name

          # Backwards-compatibility: if we find schema_info, assume we've
          # migrated up to that point:
          si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
          if table_exists?(si_table)
            ActiveSupport::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table"

            old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
            assume_migrated_upto_version(old_version)
            drop_table(si_table)
          end
        end
      end

      # clear cached indexes when adding new index
      def add_index(table_name, column_name, options = {}) #:nodoc:
        column_names = Array(column_name)
        index_name   = index_name(table_name, :column => column_names)

        if Hash === options # legacy support, since this param was a string
          index_type = options[:unique] ? "UNIQUE" : ""
          index_name = options[:name].to_s if options.key?(:name)
          tablespace = tablespace_for(:index, options[:tablespace])
        else
          index_type = options
        end

        if index_name.to_s.length > index_name_length
          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
        end
        if index_name_exists?(table_name, index_name, false)
          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
        end
        quoted_column_names = column_names.map { |e| quote_column_name_or_expression(e) }.join(", ")

        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{options[:options]}"
      ensure
        self.all_schema_indexes = nil
      end

      # Remove the given index from the table.
      # Gives warning if index does not exist
      def remove_index(table_name, options = {}) #:nodoc:
        index_name = index_name(table_name, options)
        unless index_name_exists?(table_name, index_name, true)
          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
        end
        remove_index!(table_name, index_name)
      end

      # clear cached indexes when removing index
      def remove_index!(table_name, index_name) #:nodoc:
        execute "DROP INDEX #{quote_column_name(index_name)}"
      ensure
        self.all_schema_indexes = nil
      end

      # returned shortened index name if default is too large
      def index_name(table_name, options) #:nodoc:
        default_name = super(table_name, options).to_s
        # sometimes options can be String or Array with column names
        options = {} unless options.is_a?(Hash)
        identifier_max_length = options[:identifier_max_length] || index_name_length
        return default_name if default_name.length <= identifier_max_length

        # remove 'index', 'on' and 'and' keywords
        shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}"

        # leave just first three letters from each word
        if shortened_name.length > identifier_max_length
          shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_')
        end
        # generate unique name using hash function
        if shortened_name.length > identifier_max_length
          shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,identifier_max_length-1]
        end
        @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger
        shortened_name
      end

      # Verify the existence of an index with a given name.
      #
      # The default argument is returned if the underlying implementation does not define the indexes method,
      # as there's no way to determine the correct answer in that case.
      #
      # Will always query database and not index cache.
      def index_name_exists?(table_name, index_name, default)
        (owner, table_name, db_link) = @connection.describe(table_name)
        result = select_value(<<-SQL)
          SELECT 1 FROM all_indexes#{db_link} i
          WHERE i.owner = '#{owner}'
             AND i.table_owner = '#{owner}'
             AND i.table_name = '#{table_name}'
             AND i.index_name = '#{index_name.to_s.upcase}'
        SQL
        result == 1
      end

      def rename_index(table_name, index_name, new_index_name) #:nodoc:
        unless index_name_exists?(table_name, index_name, true)
          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
        end
        execute "ALTER INDEX #{quote_column_name(index_name)} rename to #{quote_column_name(new_index_name)}"
      ensure
        self.all_schema_indexes = nil
      end

      def add_column(table_name, column_name, type, options = {}) #:nodoc:
        if type.to_sym == :virtual
          type = options[:type]
        end
        add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} "
        add_column_sql << type_to_sql(type, options[:limit], options[:precision], options[:scale]) if type

        add_column_options!(add_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name))

        add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) if type

        execute(add_column_sql)
      ensure
        clear_table_columns_cache(table_name)
      end

      def change_column_default(table_name, column_name, default) #:nodoc:
        execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
      ensure
        clear_table_columns_cache(table_name)
      end

      def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
        column = column_for(table_name, column_name)

        unless null || default.nil?
          execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
        end

        change_column table_name, column_name, column.sql_type, :null => null
      end

      def change_column(table_name, column_name, type, options = {}) #:nodoc:
        column = column_for(table_name, column_name)

        # remove :null option if its value is the same as current column definition
        # otherwise Oracle will raise error
        if options.has_key?(:null) && options[:null] == column.null
          options[:null] = nil
        end
        if type.to_sym == :virtual
          type = options[:type]
        end
        change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} "
        change_column_sql << "#{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" if type

        add_column_options!(change_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name))

        change_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, options[:table_name], options[:column_name]) if type

        execute(change_column_sql)
      ensure
        clear_table_columns_cache(table_name)
      end

      def rename_column(table_name, column_name, new_column_name) #:nodoc:
        execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
      ensure
        clear_table_columns_cache(table_name)
      end

      def remove_column(table_name, *column_names) #:nodoc:
        raise ArgumentError.new("You must specify at least one column name.  Example: remove_column(:people, :first_name)") if column_names.empty?

        major, minor = ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR
        is_deprecated = (major == 3 and minor >= 2) or major > 3

        if column_names.flatten! and is_deprecated
          message = 'Passing array to remove_columns is deprecated, please use ' +
            'multiple arguments, like: `remove_columns(:posts, :foo, :bar)`'
          ActiveSupport::Deprecation.warn message, caller
        end

        column_names.each do |column_name|
          execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
        end
      ensure
        clear_table_columns_cache(table_name)
      end

      def add_comment(table_name, column_name, comment) #:nodoc:
        return if comment.blank?
        execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'"
      end

      def add_table_comment(table_name, comment) #:nodoc:
        return if comment.blank?
        execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'"
      end

      def table_comment(table_name) #:nodoc:
        (owner, table_name, db_link) = @connection.describe(table_name)
        select_value <<-SQL
          SELECT comments FROM all_tab_comments#{db_link}
          WHERE owner = '#{owner}'
            AND table_name = '#{table_name}'
        SQL
      end

      def column_comment(table_name, column_name) #:nodoc:
        (owner, table_name, db_link) = @connection.describe(table_name)
        select_value <<-SQL
          SELECT comments FROM all_col_comments#{db_link}
          WHERE owner = '#{owner}'
            AND table_name = '#{table_name}'
            AND column_name = '#{column_name.upcase}'
        SQL
      end

      # Maps logical Rails types to Oracle-specific data types.
      def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
        # Ignore options for :text and :binary columns
        return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s)

        super
      end

      def tablespace(table_name)
        select_value <<-SQL
          SELECT tablespace_name
          FROM user_tables
          WHERE table_name='#{table_name.to_s.upcase}'
        SQL
      end

      private

      def tablespace_for(obj_type, tablespace_option, table_name=nil, column_name=nil)
        tablespace_sql = ''
        if tablespace = (tablespace_option || default_tablespace_for(obj_type))
          tablespace_sql << if [:blob, :clob].include?(obj_type.to_sym)
           " LOB (#{quote_column_name(column_name)}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})"
          else
           " TABLESPACE #{tablespace}"
          end
        end
        tablespace_sql
      end

      def default_tablespace_for(type)
        (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil
      end


      def column_for(table_name, column_name)
        unless column = columns(table_name).find { |c| c.name == column_name.to_s }
          raise "No such column: #{table_name}.#{column_name}"
        end
        column
      end

      def create_sequence_and_trigger(table_name, options)
        seq_name = options[:sequence_name] || default_sequence_name(table_name)
        seq_start_value = options[:sequence_start_value] || default_sequence_start_value
        execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}"

        create_primary_key_trigger(table_name, options) if options[:primary_key_trigger]
      end

      def create_primary_key_trigger(table_name, options)
        seq_name = options[:sequence_name] || default_sequence_name(table_name)
        trigger_name = options[:trigger_name] || default_trigger_name(table_name)
        primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)
        execute compress_lines(<<-SQL)
          CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)}
          BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW
          BEGIN
            IF inserting THEN
              IF :new.#{quote_column_name(primary_key)} IS NULL THEN
                SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual;
              END IF;
            END IF;
          END;
        SQL
      end

      def default_trigger_name(table_name)
        # truncate table name if necessary to fit in max length of identifier
        "#{table_name.to_s[0,table_name_length-4]}_pkt"
      end

    end
  end
end

ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
  include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatements
end