require 'contracts'
require 'aruba/basic_configuration/option'
require 'aruba/in_config_wrapper'

# Aruba
module Aruba
  # Basic configuration for Aruba
  #
  # @private
  class BasicConfiguration
    include Contracts

    class << self
      def known_options
        @known_options ||= {}
      end

      # Define an option reader
      #
      # @param [Symbol] name
      #   The name of the reader
      #
      # @option [Class, Module] type
      #   The type contract for the option
      #
      # @option [Object] default
      #   The default value
      def option_reader(name, type:, default: nil)
        raise ArgumentError, 'Either use block or default value' if block_given? && default

        add_option(name, block_given? ? yield(InConfigWrapper.new(known_options)) : default)

        Contract None => type
        define_method(name) { find_option(name).value }
      end

      # Define an option reader and writer
      #
      # @param [Symbol] name
      #   The name of the reader
      #
      # @option [Class, Module] type
      #   The type contract for the option
      #
      # @option [Object] default
      #   The default value
      #
      def option_accessor(name, type:, default: nil)
        raise ArgumentError, 'Either use block or default value' if block_given? && default

        # Add writer
        add_option(name, block_given? ? yield(InConfigWrapper.new(known_options)) : default)

        Contract type => type
        define_method("#{name}=") { |v| find_option(name).value = v }

        # Add reader
        option_reader name, type: type
      end

      private

      def add_option(name, value = nil)
        return if known_options.key?(name)

        known_options[name] = Option.new(name: name, value: value)

        self
      end
    end

    protected

    attr_accessor :local_options
    attr_writer :hooks

    public

    # Create configuration
    def initialize
      initialize_configuration
    end

    # @yield [Configuration]
    #
    #   Yields self
    def configure
      yield self if block_given?
    end

    # Reset configuration
    def reset
      initialize_configuration
    end

    # Make deep dup copy of configuration
    def make_copy
      obj = dup
      obj.local_options = Marshal.load(Marshal.dump(local_options))
      obj.hooks         = @hooks

      obj
    end

    # Define before-hook
    #
    # @param [Symbol, String] name
    #   The name of the hook
    #
    # @yield
    #   The code block which should be run. This is a configure time only option
    def before(name, &block)
      name = format('%s_%s', 'before_', name.to_s).to_sym
      raise ArgumentError, 'A block is required' unless block

      @hooks.append(name, block)

      self
    end

    # Run before-hook
    #
    # @param [Symbol, String] name
    #   The name of the hook
    #
    # @param [Proc] context
    #   The context a hook should run in
    #
    # @param [Array] args
    #   Arguments for the run of hook
    def run_before_hook(name, context, *args)
      name = format('%s_%s', 'before_', name.to_s).to_sym

      @hooks.execute(name, context, *args)
    end

    # Define after-hook
    #
    # @param [Symbol, String] name
    #   The name of the hook
    #
    # @yield
    #   The code block which should be run. This is a configure time only option
    def after(name, &block)
      name = format('%s_%s', 'after_', name.to_s).to_sym
      raise ArgumentError, 'A block is required' unless block

      @hooks.append(name, block)

      self
    end

    # Run after-hook
    #
    # @param [Symbol, String] name
    #   The name of the hook
    #
    # @param [Proc] context
    #   The context a hook should run in
    #
    # @param [Array] args
    #   Arguments for the run of hook
    def run_after_hook(name, context, *args)
      name = format('%s_%s', 'after_', name.to_s).to_sym

      @hooks.execute(name, context, *args)
    end

    # Check if <name> is option
    #
    # @param [String, Symbol] name
    #   The name of the option
    def option?(name)
      local_options.any? { |_, v| v.name == name.to_sym }
    end

    def ==(other)
      local_options.values.map(&:value) == other.local_options.values.map(&:value)
    end

    # Set if name is option
    def set_if_option(name, *args)
      public_send("#{name}=".to_sym, *args) if option? name
    end

    private

    def initialize_configuration
      @local_options = Marshal.load(Marshal.dump(self.class.known_options))
      @hooks         = Hooks.new
    end

    def find_option(name)
      raise NotImplementedError, %(Unknown option "#{name}") unless option? name

      local_options[name]
    end
  end
end
