require 'forwardable'
begin
  require 'json'
rescue LoadError
end

module Degu
  module Renum

    # This is the superclass of all enumeration classes.
    # An enumeration class is Enumerable over its values and exposes them by numeric index via [].
    # Values are also comparable, sorting into the order in which they're declared.
    class EnumeratedValue

      class << self
        include Enumerable
        extend Forwardable

        def_delegators :values, :first, :last, :each

        # Returns an array of values in the order they're declared.
        def values
          @values ||= []
        end

        alias all values

        # This class encapsulates an enum field (аctually a method with arity == 0).
        class Field < Struct.new('Field', :name, :options, :block)
          # Returns true if the :default option was given.
          def default?
            options.key?(:default)
          end

          # Returns the value of the :default option.
          def default
            options[:default]
          end

          # Returns true if a block was given.
          def block?
            !!block
          end

          # Determine the default value for the enum value +obj+ if +options+ is
          # the options hash given to the init method.
          def default_value(obj, options)
            field_value = options[name]
            if field_value.nil?
              if default?
                default_value = default
              elsif block?
                default_value = block[obj]
              end
            else
              field_value
            end
          end

          # Returns the name as a string.
          def to_s
            name.to_s
          end

          # Returns a detailed string representation of this field.
          def inspect
            "#<#{self.class}: #{self} #{options.inspect}>"
          end
        end

        # Returns an array of all fields defined on this enum.
        def fields
          @fields ||= []
        end

        # Defines a field with the name +name+, the options +options+ and the
        # block +block+. The only valid option at the moment is :default which is
        # the default value the field is initialized with.
        def field(name, options = {}, &block)
          name = name.to_sym
          fields.delete_if { |f| f.name == name }
          fields << field = Field.new(name, options, block)
          instance_eval { attr_reader field.name }
        end

        # Returns the value with the name +name+ and returns it.
        def with_name name
          values_by_name[name.to_s]
        end

        # Returns a hash that maps names to their respective values values.
        def values_by_name
          @values_by_name ||= values.inject({}) do |memo, value|
            memo[value.name] = value
            memo
          end.freeze
        end

        # Returns the enum value for +index+. If +index+ is an Integer the
        # index-th enum value is returned. Otherwise +index+ is converted into a
        # String. For strings that start with a capital letter the with_name
        # method is used to determine the enum value with the name +index+. If
        # the string starts with a lowercase letter it is converted into
        # camelcase first, that is foo_bar will be converted into FooBar, before
        # with_name is called with this new value.
        def [](index)
          case index
          when Integer
            values[index]
          when self
            values[index.index]
          else
            name = index.to_s
            case name
            when /\A(\d+)\Z/
              return values[$1.to_i]
            when /\A[a-z]/
              name = name.gsub(/(?:\A|_)(.)/) { $1.upcase }
            end
            with_name(name)
          end
        end

        # Returns the enum instance stored in the marshalled string +string+.
        def _load(string)
          with_name Marshal.load(string)
        end

        if defined?(::JSON)
          # Fetches the correct enum determined by the deserialized JSON
          # document.
          def json_create(data)
            JSON.deep_const_get(data[JSON.create_id])[data['name']]
          end
        end
      end

      include Comparable

      # Name of this enumerated value as a string.
      attr_reader :name

      # Index of this enumerated value as an integer.
      attr_reader :index

      alias_method :id, :index

      # Creates an enumerated value named +name+ with a unique autoincrementing
      # index number.
      def initialize name
        @name = name.to_s.freeze
        @index = self.class.values.size
        self.class.values << self
      end

      # This is the standard init method method which has an arbitrary number of
      # arguments. If the last argument is a Hash and its keys are defined fields
      # their respective values will be used to initialize the fields. If you
      # want to use this method from an enum and define your own custom init
      # method there, don't forget to call super from your method.
      def init(*args)
        if Hash === options = args.last
          for field in self.class.fields
            instance_variable_set "@#{field}", field.default_value(self, options)
          end
        end
      end

      # Returns the fully qualified name of the constant referring to this value.
      # Don't override this if you're using Renum with the constantize_attribute
      # plugin, which relies on this behavior.
      def to_s
        "#{self.class}::#{name}"
      end

      # Sorts enumerated values into the order in which they're declared.
      def <=> other
        index <=> other.index
      end

      # Returns a marshalled string for this enum instance.
      def _dump(limit = -1)
        Marshal.dump(name, limit)
      end

      if defined?(::JSON)
        # Set the given fields in the +obj+ hash
        def set_fields(obj, fields)
          fields.each do |f|
            name = f.name
            value = instance_variable_get("@#{name}")
            value.nil? and next
            obj[name] = value
          end
        end

        # Returns an enum (actually more a reference to an enum) serialized as a
        # JSON document.
        def as_json(opts = {}, *a)
          opts ||= {}
          obj = {
            JSON.create_id => self.class.name,
            :name          => name,
          }
          case fields_opt = opts[:fields]
          when nil, false
          when true
            set_fields obj, self.class.fields
          when Array
            fields_opt = fields_opt.map(&:to_sym)
            set_fields obj, self.class.fields.select { |field| fields_opt.include?(field.name) }
          else
            raise ArgumentError, "unexpected fields option #{fields_opt.inspect}"
          end
          obj.as_json(opts)
        end

        def to_json(opts, *a)
          obj = as_json(opts)
          opts.respond_to?(:fields) and opts.delete(:fields)
          obj.to_json(opts, *a)
        end
      end
    end
  end
end