#--
# Copyright (c) 2005-2013, John Mettraux, jmettraux@gmail.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Made in Japan.
#++

require 'ruote/util/time'


module Ruote

  #
  # Base methods for storage implementations.
  #
  module StorageBase

    #--
    # misc
    #++

    def context

      @context ||= Ruote::Context.new(self)
    end

    def context=(c)

      @context = c
    end

    # Attempts to delete a document, returns true if the deletion
    # succeeded. This is used with msgs to reserve work on them.
    #
    def reserve(doc)

      delete(doc).nil?
    end

    # Warning, this is not equivalent to doing @context.worker, this method
    # fetches the worker from the local thread variables.
    #
    def worker

      Ruote.current_worker
    end

    #--
    # configurations
    #++

    def get_configuration(key)

      get('configurations', key)
    end

    def replace_engine_configuration(options)

      return if options.delete('preserve_configuration')

      conf = get('configurations', 'engine')

      doc = options.merge('type' => 'configurations', '_id' => 'engine')
      doc['_rev'] = conf['_rev'] if conf

      put(doc)
    end

    #--
    # messages
    #++

    def put_msg(action, options)

      put(prepare_msg_doc(action, options))
    end

    def get_msgs

      get_many('msgs', nil, :limit => 300).sort_by { |d| d['put_at'] }
    end

    def empty?(type)

      (get_many(type, nil, :count => true) == 0)
    end

    #--
    # expressions
    #++

    # Given a wfid, returns all the expressions of that process instance.
    #
    def find_expressions(wfid)

      get_many('expressions', wfid)
    end

    # For a given wfid, returns all the expressions (array of Hash instances)
    # that have a nil 'parent_id'.
    #
    def find_root_expressions(wfid)

      find_expressions(wfid).select { |hexp| hexp['parent_id'].nil? }
    end

    # For a given wfid, fetches all the root expressions, sort by expid and
    # return the first. Hopefully it's the right root_expression.
    #
    def find_root_expression(wfid)

      find_root_expressions(wfid).sort_by { |hexp| hexp['fei']['expid'] }.first
    end

    # Given all the expressions stored here, returns a sorted list of unique
    # wfids (this is used in Engine#processes(opts).
    #
    # Understands the :skip, :limit and :descending options.
    #
    # This is a base implementation, different storage implementations may
    # come up with different implementations (think CouchDB,  which could
    # provide a view for it).
    #
    def expression_wfids(opts)

      wfids = ids('expressions').collect { |fei| fei.split('!').last }.uniq.sort

      wfids = wfids.reverse if opts[:descending]

      skip = opts[:skip] || 0
      limit = opts[:limit] || wfids.length

      wfids[skip, limit]
    end

    #--
    # trackers
    #++

    # Some storage implementation might need the wfid information when
    # adding or removing trackers.
    #
    def get_trackers(wfid=nil)

      get('variables', 'trackers') ||
        { '_id' => 'trackers', 'type' => 'variables', 'trackers' => {} }
    end

    #--
    # ats and crons
    #++

    def get_schedules(delta, now)

      # TODO : bring that 'optimization' back in,
      #        maybe every minute, if min != last_min ...

      #if delta < 1.0
      #  at = now.strftime('%Y%m%d%H%M%S')
      #  get_many('schedules', /-#{at}$/)
      #elsif delta < 60.0
      #  at = now.strftime('%Y%m%d%H%M')
      #  scheds = get_many('schedules', /-#{at}\d\d$/)
      #  filter_schedules(scheds, now)
      #else # load all the schedules

      scheds = get_many('schedules')
      filter_schedules(scheds, now)

      #end
    end

    # Places schedule in storage. Returns the id of the 'schedule' document.
    # If the schedule got triggered immediately, nil is returned.
    #
    def put_schedule(flavour, owner_fei, s, msg)

      doc = prepare_schedule_doc(flavour, owner_fei, s, msg)

      return nil unless doc

      r = put(doc)

      raise "put_schedule failed" if r != nil

      doc['_id']
    end

    def delete_schedule(schedule_id)

      return if schedule_id.nil?

      s = get('schedules', schedule_id)
      delete(s) if s
    end

    #--
    # engine variables
    #++

    def get_engine_variable(k)

      get_engine_variables['variables'][k]
    end

    def put_engine_variable(k, v)

      vars = get_engine_variables
      vars['variables'][k] = v

      put_engine_variable(k, v) unless put(vars).nil?
    end

    #--
    # migrations
    #++

    # Copies the content of this storage into a target storage.
    #
    # Of course, the target storage may be a different implementation.
    #
    def copy_to(target, opts={})

      counter = 0

      %w[
        configurations errors expressions msgs schedules variables workitems
      ].each do |type|

        ids(type).each do |id|

          item = get(type, id)

          item.delete('_rev')
          target.put(item)

          counter += 1
          puts("  #{type}/#{item['_id']}") if opts[:verbose]
        end
      end

      counter
    end

    # Used when doing integration tests, removes all
    # msgs, schedules, errors, expressions and workitems.
    #
    # NOTE that it doesn't remove engine variables (danger)
    #
    def clear

      %w[ msgs schedules errors expressions workitems ].each do |type|
        purge_type!(type)
      end
    end

    # Removes a process by removing all its schedules, expressions, errors,
    # workitems and trackers.
    #
    # Warning: will not trigger any cancel behaviours at all, just removes
    # the process.
    #
    def remove_process(wfid)

      2.times do
        # two passes

        Thread.pass

        %w[ schedules expressions errors workitems ].each do |type|
          get_many(type, wfid).each { |d| delete(d) }
        end

        doc = get_trackers

        doc['trackers'].delete_if { |k, v| k.end_with?("!#{wfid}") }

        @context.storage.put(doc)
      end
    end

    def dump(type)

      require 'yaml'

      YAML.dump({ type => get_many(type) })
    end

    protected

    # Used by put_msg
    #
    def prepare_msg_doc(action, options)

      # merge! is way faster than merge (no object creation probably)

      @counter ||= 0

      t = Time.now.utc
      ts = "#{t.strftime('%Y-%m-%d')}!#{t.to_i}.#{'%06d' % t.usec}"
      _id = "#{$$}!#{Thread.current.object_id}!#{ts}!#{'%03d' % @counter}"

      @counter = (@counter + 1) % 1000
        # some platforms (windows) have shallow usecs, so adding that counter...

      msg = options.merge!('type' => 'msgs', '_id' => _id, 'action' => action)

      msg.delete('_rev')
        # in case of message replay

      msg
    end

    # Used by put_schedule
    #
    def prepare_schedule_doc(flavour, owner_fei, s, msg)

      at = if s.is_a?(Time) # at or every
        s
      elsif Ruote.cron_string?(s) # cron
        Rufus::Scheduler::CronLine.new(s).next_time(Time.now + 1)
      else # at or every
        Ruote.s_to_at(s)
      end
      at = at.utc

      if at <= Time.now.utc && flavour == 'at'
        put_msg(msg.delete('action'), msg)
        return false
      end

      sat = at.strftime('%Y%m%d%H%M%S')
      i = "#{flavour}-#{Ruote.to_storage_id(owner_fei)}-#{sat}"

      {
        '_id' => i,
        'type' => 'schedules',
        'flavour' => flavour,
        'original' => s,
        'at' => Ruote.time_to_utc_s(at),
        'owner' => owner_fei,
        'wfid' => owner_fei['wfid'],
        'msg' => msg
      }
    end

    def get_engine_variables

      get('variables', 'variables') || {
        'type' => 'variables', '_id' => 'variables', 'variables' => {} }
    end

    # Returns all the ats whose due date arrived (now or earlier)
    #
    def filter_schedules(schedules, now)

      now = Ruote.time_to_utc_s(now)

      schedules.select { |sch| sch['at'] <= now }
    end

    # Used by #get_many. Returns true whenever one of the keys matches the
    # doc['_id']. Works with strings (_id ends with key) or regexes (_id matches
    # key).
    #
    def key_match?(type, keys, doc)

      _id = doc.is_a?(Hash) ? doc['_id'] : doc

      if keys.first.is_a?(String) && type == 'schedules'
        keys.find { |key| _id.match(/#{key}-\d+$/) }
      elsif keys.first.is_a?(String)
        keys.find { |key| _id.end_with?(key) }
      else # Regexp
        keys.find { |key| _id.match(key) }
      end
    end

    # Used by ruote-couch and ruote-asw
    #
    def self.key_match?(keys, doc)

      _id = doc.is_a?(Hash) ? doc['_id'] : doc

      if keys.first.is_a?(String)
        keys.find { |key| _id.end_with?(key) }
      else # Regexp
        keys.find { |key| _id.match(key) }
      end
    end
  end
end

