class Card
  # ## What are Machines?
  # {Machine} and {MachineInput} together implement a kind of observer pattern.
  # {Machine} processes a collection of input cards to generate an output card
  # (a {Set::Type::File} card by default). If one of the input cards is changed
  # the output card will be updated.
  #
  # The classic example: A style card observes a collection of css and sccs card
  # to generate a file card with a css file that contains the assembled
  # compressed  css.
  #
  # ## Using Machines
  # Include the Machine module in the card set that is supposed to produce the
  # output card. If the output card should be automatically updated when a input
  # card is changed the input card has to be in a set that includes the
  # MachineInput module.
  #
  # The default machine:
  #
  #  -  uses its item cards as input cards or the card itself if there are no
  # item cards;
  #  -  can be changed by passing a block to collect_input_cards
  #  -  takes the raw view of the input cards to generate the output;
  #  -  can be changed by passing a block to machine_input (in the input card
  # set)
  #  -  stores the output as a .txt file in the '+machine output' card;
  #  -  can be changed by passing a filetype and/or a block to
  #     store_machine_output
  #
  #
  # ## How does it work?
  # Machine cards have a '+machine input' and a '+machine output' card. The
  # '+machine input' card is a pointer to all input cards. Including the
  # MachineInput module creates an 'on: save' event that runs the machines of
  # all cards that are linked to that card via the +machine input pointer.
  module Machine
    module ClassMethods
      attr_accessor :output_config

      def collect_input_cards &block
        define_method :engine_input, &block
      end

      def prepare_machine_input &block
        define_method :before_engine, &block
      end

      def machine_engine &block
        define_method :engine, &block
      end

      def store_machine_output args={}, &block
        output_config.merge!(args)
        return unless block_given?
        define_method :after_engine, &block
      end
    end

    class << self
      def included host_class
        host_class.extend(ClassMethods)
        host_class.output_config = { filetype: "txt" }

        # for compatibility with old migrations
        return unless  Codename[:machine_output]

        host_class.card_accessor :machine_output, type: :file
        host_class.card_accessor :machine_input, type: :pointer

        set_default_machine_behaviour host_class
        define_machine_views host_class
        define_machine_events host_class
      end

      def define_machine_events host_class
        event_suffix = host_class.name.tr ":", "_"
        event_name = "reset_machine_output_#{event_suffix}".to_sym
        host_class.event event_name, after: :expire_related, on: :save do
          reset_machine_output
        end
      end

      def define_machine_views host_class
        host_class.format do
          view :machine_output_url do |_args|
            machine_output_url
          end
        end
      end

      def set_default_machine_behaviour host_class
        set_default_input_collection_method host_class
        set_default_input_preparation_method host_class
        set_default_output_storage_method host_class
        host_class.machine_engine { |input| input }
      end

      def set_default_input_preparation_method host_class
        host_class.prepare_machine_input {}
      end

      def set_default_output_storage_method host_class
        host_class.store_machine_output do |output|
          filetype = host_class.output_config[:filetype]
          file = Tempfile.new [id.to_s, ".#{filetype}"]
          file.write output
          file.rewind
          Card::Auth.as_bot do
            p = machine_output_card
            p.file = file
            p.save!
          end
          file.close
          file.unlink
        end
      end

      def set_default_input_collection_method host_class
        host_class.collect_input_cards do
          # traverse through all levels of pointers and
          # collect all item cards as input
          items = [self]
          new_input = []
          already_extended = {} # avoid loops
          loop_limit = 5
          until items.empty?
            item = items.shift
            next if item.trash || already_extended[item.id].to_i > loop_limit
            if item.item_cards == [item] # no pointer card
              new_input << item
            else
              # item_cards instantiates non-existing cards
              # we don't want those
              items.insert(0, item.item_cards.reject(&:unknown?))
              items.flatten!

              new_input << item if item != self && item.known?
              already_extended[item] = already_extended[item].to_i + 1
            end
          end
          new_input
        end
      end
    end

    def run_machine joint="\n"
      before_engine
      output =
        input_item_cards.map do |input_card|
          run_engine input_card
        end.select(&:present?).join(joint)
      after_engine output
    end

    def run_engine input_card
      return if input_card.is_a? Card::Set::Type::Pointer
      if (cached = fetch_cache_card(input_card))
        return cached.content
      end

      input = if input_card.respond_to? :machine_input
                input_card.machine_input
              else
                input_card.format._render_raw
              end
      output = engine(input)
      cache_output_part input_card, output
      output
    end

    def fetch_cache_card input_card, new=nil
      new &&= { type_id: PlainTextID }
      Card.fetch input_card.name, name, :machine_cache, new: new
    end

    def cache_output_part input_card, output
      Auth.as_bot do
        cache_card = fetch_cache_card(input_card, true)
        cache_card.update_attributes! content: output
      end
    end

    def reset_machine_output
      Auth.as_bot do
        (moc = machine_output_card) && moc.real? && moc.delete!
        update_input_card
      end
    end

    def update_machine_output
      lock do
        update_input_card
        run_machine
      end
    end

    def regenerate_machine_output
      lock do
        run_machine
      end
    end

    def lock
      if ok?(:read) && !(was_already_locked = locked?)
        Auth.as_bot do
          lock!
          yield
        end
      end
    ensure
      unlock! unless was_already_locked
    end

    def lock_cache_key
      "UPDATE-LOCK:#{key}"
    end

    def locked?
      Card.cache.read lock_cache_key
    end

    def lock!
      Card.cache.write lock_cache_key, true
    end

    def unlock!
      Card.cache.write lock_cache_key, false
    end

    def update_input_card
      if DirectorRegister.running_act?
        input_card = attach_subcard! machine_input_card
        input_card.content = ""
        engine_input.each { |input| input_card << input }
      else
        machine_input_card.items = engine_input
      end
    end

    def input_item_cards
      machine_input_card.item_cards
    end

    def machine_output_url
      ensure_machine_output
      machine_output_card.file.url # (:default, timestamp: false)
      # to get rid of additional number in url
    end

    def machine_output_path
      ensure_machine_output
      machine_output_card.file.path
    end

    def ensure_machine_output
      output = fetch trait: :machine_output
      return if output && output.selected_content_action_id
      update_machine_output
    end
  end
end