module Hobo
  module Model
    module Lifecycles

      class Lifecycle

        def self.init(model, options)
          @model   = model
          @options = options
          reset
        end

        def self.reset
          @states        = {}
          @creators      = {}
          @transitions   = []
          @invariants    = []
        end

        class << self
          attr_accessor :model, :options, :states, :default_state,
                        :creators, :transitions, :invariants
        end

        def self.def_state(name, on_enter)
          name = name.to_sym
          class_eval "def #{name}_state?; state_name == :#{name} end"
          states[name] = Lifecycles::State.new(name, on_enter)
        end


        def self.def_creator(name, on_create, options)
          class_eval %{
                       def self.#{name}(user, attributes=nil)
                         create(:#{name}, user, attributes)
                       end
                       def self.can_#{name}?(user, attributes=nil)
                         can_create?(:#{name}, user)
                       end
                      }
          Creator.new(self, name.to_s, on_create, options)
        end

        def self.def_transition(name, start_state, end_states, on_transition, options)
          class_eval %{
                       def #{name}!(user, attributes=nil)
                         transition(:#{name}, user, attributes)
                       end
                       def can_#{name}?(user, attributes=nil)
                         can_transition?(:#{name}, user)
                       end
                      }
          Transition.new(self, name.to_s, start_state, end_states, on_transition, options)
        end

        def self.state_names
          states.keys
        end

        def self.publishable_creators
          creators.values.where.publishable?
        end

        def self.publishable_transitions
          transitions.where.publishable?
        end

        def self.step_names
          (creators.keys | transitions.*.name).uniq
        end


        def self.creator(name)
          creators[name.to_sym] or raise ArgumentError, "No such creator in lifecycle: #{name}"
        end


        def self.can_create?(name, user)
          creators[name.to_sym].allowed?(user)
        end


        def self.create(name, user, attributes=nil)
          creator = creators[name.to_sym] or raise LifecycleError, "No creator #{name} available"
          creator.run!(user, attributes)
        end


        def self.state_field
          options[:state_field]
        end

        def self.key_timeout
          options[:key_timeout]
        end


        # --- Instance Features --- #

        attr_reader :record

        attr_accessor :provided_key, :active_step


        def initialize(record)
          @record = record
        end


        def can_transition?(name, user)
          available_transitions_for(user, name).any?
        end


        def transition(name, user, attributes)
          transition = find_transition(name, user) or raise LifecycleError, "No transition #{name} available"
          transition.run!(record, user, attributes)
        end


        def find_transition(name, user)
          available_transitions_for(user, name).first
        end


        def state_name
          name = record.read_attribute(self.class.state_field)
          name.to_sym if name
        end


        def state
          self.class.states[state_name]
        end


        def available_transitions
          state ? state.transitions_out : []
        end

        # see also publishable_transitions_for
        def available_transitions_for(user, name=nil)
          name = name.to_sym if name
          matches = available_transitions
          matches = matches.select { |t| t.name == name } if name
          record.with_acting_user(user) do
            matches.select { |t| t.can_run?(record) }
          end
        end

        def publishable_transitions_for(user)
          available_transitions_for(user).select {|t| t.publishable_by(user, t.available_to, record)}
        end


        def become(state_name, validate=true)
          state_name = state_name.to_sym
          record.send :write_attribute, self.class.state_field, state_name.to_s

          if state_name == :destroy
            record.destroy
            true
          else
            s = self.class.states[state_name]
            raise ArgumentError, "No such state '#{state_name}' for #{record.class.name}" unless s

            if record.save(:validate => validate)
              s.activate! record
              self.active_step = nil # That's the end of this step
              true
            else
              false
            end
          end
        end


        def key_timestamp_field
          record.class::Lifecycle.options[:key_timestamp_field]
        end

        def key_timeout
          record.class::Lifecycle.options[:key_timeout]
        end

        def generate_key
          if Time.zone.nil?
            raise RuntimeError, "Cannot generate lifecycle key timestamp if the time-zone is not configured. Please add, e.g. config.time_zone = 'UTC' to environment.rb"
          end
          key_timestamp = Time.now.utc
          record.send :write_attribute, key_timestamp_field, key_timestamp
          key
        end


        def key
          require 'digest/sha1'
          timestamp = record.read_attribute(key_timestamp_field)
          if timestamp
            timestamp = timestamp.getutc
            Digest::SHA1.hexdigest("#{record.id}-#{state_name}-#{timestamp}-#{Rails.application.config.secret_token}")
          end
        end

        def key_expired?
          timestamp = record.read_attribute(key_timestamp_field)
          timestamp.nil? || (timestamp.getutc + key_timeout < Time.now.utc)
        end

        def valid_key?
          provided_key && provided_key == key && !key_expired?
        end

        def clear_key
          record.write_attribute key_timestamp_field, nil
        end

        def invariants_satisfied?
          self.class.invariants.all? { |i| record.instance_eval(&i) }
        end


        def active_step_is?(name)
          active_step && active_step.name == name.to_sym
        end

        def method_missing(name, *args)
          if name.to_s =~ /^(.*)_in_progress\?$/
            active_step_is?($1)
          else
            super
          end
        end

      end

    end
  end
end