module JSON::LD
  module Frame
    include Utils

    ##
    # Frame input. Input is expected in expanded form, but frame is in compacted form.
    #
    # @param [Hash{Symbol => Object}] state
    #   Current framing state
    # @param [Hash{String => Hash}] subjects
    #   Map of flattened subjects
    # @param [Hash{String => Object}] frame
    # @param [Hash{String => Object}] parent
    #   Parent subject or top-level array
    # @param [String] property
    #   Property referencing this frame, or null for array.
    # @raise [JSON::LD::InvalidFrame]
    def frame(state, subjects, frame, parent, property)
      raise ProcessingError, "why isn't @subjects a hash?: #{@subjects.inspect}" unless @subjects.is_a?(Hash)
      depth do
        debug("frame") {"state: #{state.inspect}"}
        debug("frame") {"subjects: #{subjects.keys.inspect}"}
        debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
        debug("frame") {"parent: #{parent.to_json(JSON_STATE)}"}
        debug("frame") {"property: #{property.inspect}"}
        # Validate the frame
        validate_frame(state, frame)

        # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
        # This gives us a hash of objects indexed by @id
        matches = filter_subjects(state, subjects, frame)
        debug("frame") {"matches: #{matches.keys.inspect}"}

        # Get values for embedOn and explicitOn
        embed = get_frame_flag(state, frame, 'embed');
        explicit = get_frame_flag(state, frame, 'explicit');
        debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
      
        # For each id and subject from the set of matched subjects ordered by id
        matches.keys.sort.each do |id|
          element = matches[id]
          # If the active property is null, set the map of embeds in state to an empty map
          state = state.merge(:embeds => {}) if property.nil?

          output = {'@id' => id}
        
          # prepare embed meta info
          embedded_subject = {:parent => parent, :property => property}
        
          # If embedOn is true, and id is in map of embeds from state
          if embed && (existing = state[:embeds].fetch(id, nil))
            # only overwrite an existing embed if it has already been added to its
            # parent -- otherwise its parent is somewhere up the tree from this
            # embed and the embed would occur twice once the tree is added
            embed = false
          
            embed = if existing[:parent].is_a?(Array)
              # If existing has a parent which is an array containing a JSON object with @id equal to id, element has already been embedded and can be overwritten, so set embedOn to true
              existing[:parent].detect {|p| p['@id'] == id}
            else
              # Otherwise, existing has a parent which is a subject definition. Set embedOn to true if any of the items in parent property is a subject definition or subject reference for id because the embed can be overwritten
              existing[:parent].fetch(existing[:property], []).any? do |v|
                v.is_a?(Hash) && v.fetch('@id', nil) == id
              end
            end
            debug("frame") {"embed now: #{embed.inspect}"}

            # If embedOn is true, existing is already embedded but can be overwritten
            remove_embed(state, id) if embed
          end

          unless embed
            # not embedding, add output without any other properties
            add_frame_output(state, parent, property, output)
          else
            # Add embed to map of embeds for id
            state[:embeds][id] = embedded_subject
            debug("frame") {"add embedded_subject: #{embedded_subject.inspect}"}
        
            # Process each property and value in the matched subject as follows
            element.keys.sort.each do |prop|
              value = element[prop]
              if prop[0,1] == '@'
                # If property is a keyword, add property and a copy of value to output and continue with the next property from subject
                output[prop] = value.dup
                next
              end

              # If property is not in frame:
              unless frame.has_key?(prop)
                debug("frame") {"non-framed property #{prop}"}
                # If explicitOn is false, Embed values from subject in output using subject as element and property as active property
                embed_values(state, element, prop, output) unless explicit
                
                # Continue to next property
                next
              end
          
              # Process each item from value as follows
              value.each do |item|
                debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
                
                # FIXME: If item is a JSON object with the key @list
                if list?(item)
                  # create a JSON object named list with the key @list and the value of an empty array
                  list = {'@list' => []}
                  
                  # Append list to property in output
                  add_frame_output(state, output, prop, list)
                  
                  # Process each listitem in the @list array as follows
                  item['@list'].each do |listitem|
                    if subject_reference?(listitem)
                      itemid = listitem['@id']
                      debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}

                      # If listitem is a subject reference process listitem recursively using this algorithm passing a new map of subjects that contains the @id of listitem as the key and the subject reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
                      frame(state, {itemid => @subjects[itemid]}, frame[prop].first, list, '@list')
                    else
                      # Otherwise, append a copy of listitem to @list in list.
                      debug("frame") {"list item of #{prop} non-subject ref #{listitem.inspect}"}
                      add_frame_output(state, list, '@list', listitem)
                    end
                  end
                elsif subject_reference?(item)
                  # If item is a subject reference process item recursively
                  # Recurse into sub-objects
                  itemid = item['@id']
                  debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
                  
                  # passing a new map as subjects that contains the @id of item as the key and the subject reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
                  frame(state, {itemid => @subjects[itemid]}, frame[prop].first, output, prop)
                else
                  # Otherwise, append a copy of item to active property in output.
                  debug("frame") {"value property #{prop} non-subject ref #{item.inspect}"}
                  add_frame_output(state, output, prop, item)
                end
              end
            end

            # Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
            frame.keys.sort.each do |prop|
              next if prop[0,1] == '@' || output.has_key?(prop)
              property_frame = frame[prop]
              debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}

              # Set property frame to the first item in value or a newly created JSON object if value is empty.
              property_frame = property_frame.first || {}

              # Skip to the next property in frame if property is in output or if property frame contains @omitDefault which is true or if it does not contain @omitDefault but the value of omit default flag true.
              next if output.has_key?(prop) || get_frame_flag(state, property_frame, 'omitDefault')

              # Set the value of property in output to a new JSON object with a property @preserve and a value that is a copy of the value of @default in frame if it exists, or the string @null otherwise
              default = property_frame.fetch('@default', '@null').dup
              default = [default] unless default.is_a?(Array)
              output[prop] = [{"@preserve" => default.compact}]
              debug("=>") {"add default #{output[prop].inspect}"}
            end
          
            # Add output to parent
            add_frame_output(state, parent, property, output)
          end
        end
      end
    end

    ##
    # Build hash of subjects used for framing. Also returns flattened representation
    # of input.
    #
    # @param [Hash{String => Hash}] subjects
    #   destination for mapped subjects and their Object representations
    # @param [Array, Hash] input
    #   JSON-LD in expanded form
    # @param [BlankNodeNamer] namer
    # @return
    #   input with subject definitions changed to references
    def get_framing_subjects(subjects, input, namer)
      depth do
        debug("framing subjects") {"input: #{input.inspect}"}
        case input
        when Array
          input.map {|o| get_framing_subjects(subjects, o, namer)}
        when Hash
          case
          when subject?(input) || subject_reference?(input)
            # Get name for subject, mapping old blank node identifiers to new
            name = blank_node?(input) ? namer.get_name(input.fetch('@id', nil)) : input['@id']
            debug("framing subjects") {"new subject: #{name.inspect}"} unless subjects.has_key?(name)
            subject = subjects[name] ||= {'@id' => name}

            # In property order
            input.keys.sort.each do |prop|
              value = input[prop]
              case prop
              when '@id'
                # Skip @id, already assigned
              when /^@/
                # Copy other keywords
                subject[prop] = value
              else
                case value
                when Hash
                  # Special case @list, which is not in expanded form
                  raise InvalidFrame::Syntax, "Unexpected hash value: #{value.inspect}" unless value.has_key?('@list')
                
                  # Map entries replacing subjects with subject references
                  subject[prop] = {"@list" =>
                    value['@list'].map {|o| get_framing_subjects(subjects, o, namer)}
                  }
                when Array
                  # Map array entries
                  subject[prop] = get_framing_subjects(subjects, value, namer)
                else
                  raise InvalidFrame::Syntax, "unexpected value: #{value.inspect}"
                end
              end
            end
            
            # Return as subject reference
            {"@id" => name}
          else
            # At this point, it's not a subject or a reference, just return input
            input
          end
        else
          # Returns equivalent representation
          input
        end
      end
    end

    ##
    # Flatten input, used in framing.
    #
    # This algorithm works by transforming input to statements, and then back to JSON-LD
    #
    # @return [Array{Hash}]
    def flatten
      debug("flatten")
      expanded = depth {self.expand(self.value, nil, context)}
      statements = []
      depth {self.statements("", expanded, nil, nil, nil ) {|s| statements << s}}
      debug("flatten") {"statements: #{statements.map(&:to_nquads).join("\n")}"}

      # Transform back to JSON-LD, not flattened
      depth {self.from_statements(statements)}
    end

    ##
    # Replace @preserve keys with the values, also replace @null with null
    #
    # @param [Array, Hash] input
    # @return [Array, Hash]
    def cleanup_preserve(input)
      depth do
        #debug("cleanup preserve") {input.inspect}
        result = case input
        when Array
          # If, after replacement, an array contains only the value null remove the value, leaving an empty array. 
          input.map {|o| cleanup_preserve(o)}.compact
        when Hash
          output = Hash.ordered
          input.each do |key, value|
            if key == '@preserve'
              # replace all key-value pairs where the key is @preserve with the value from the key-pair
              output = cleanup_preserve(value)
            else
              v = cleanup_preserve(value)
              
              # Because we may have added a null value to an array, we need to clean that up, if we possible
              v = v.first if v.is_a?(Array) && v.length == 1 &&
                context.expand_iri(key) != "@graph" && context.container(key).nil?
              output[key] = v
            end
          end
          output
        when '@null'
          # If the value from the key-pair is @null, replace the value with nul
          nil
        else
          input
        end
        #debug(" => ") {result.inspect}
        result
      end
    end

    private
    
    ##
    # Returns a map of all of the subjects that match a parsed frame.
    # 
    # @param state the current framing state.
    # @param subjects the set of subjects to filter.
    # @param frame the parsed frame.
    # 
    # @return all of the matched subjects.
    def filter_subjects(state, subjects, frame)
      subjects.dup.keep_if {|id, element| filter_subject(state, element, frame)}
    end

    ##
    # Returns true if the given subject matches the given frame.
    #
    # Matches either based on explicit type inclusion where the subject
    # has any type listed in the frame. If the frame has empty types defined
    # matches subjects not having a @type. If the frame has a type of {} defined
    # matches subjects having any type defined.
    #
    # Otherwise, does duck typing, where the subject must have all of the properties
    # defined in the frame.
    # 
    # @param [Hash{Symbol => Object}] state the current frame state.
    # @param [Hash{String => Object}] subject the subject to check.
    # @param [Hash{String => Object}] frame the frame to check.
    # 
    # @return true if the subject matches, false if not.
    def filter_subject(state, subject, frame)
      if types = frame.fetch('@type', nil)
        subject_types = subject.fetch('@type', [])
        raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
        raise InvalidFrame::Syntax, "subject @type must be an array: #{subject_types.inspect}" unless subject_types.is_a?(Array)
        # If frame has an @type, use it for selecting appropriate subjects.
        debug("frame") {"filter subject: #{subject_types.inspect} has any of #{types.inspect}"}

        # Check for type wild-card, or intersection
        types == [{}] ? !subject_types.empty? : subject_types.any? {|t| types.include?(t)}
      else
        # Duck typing, for subjects not having a type, but having @id
        
        # Subject matches if it has all the properties in the frame
        frame_keys = frame.keys.reject {|k| k[0,1] == '@'}
        subject_keys = subject.keys.reject {|k| k[0,1] == '@'}
        (frame_keys & subject_keys) == frame_keys
      end
    end

    def validate_frame(state, frame)
      raise InvalidFrame::Syntax,
            "Invalid JSON-LD syntax; a JSON-LD frame must be an object" unless frame.is_a?(Hash)
    end
    
    # Return value of @name in frame, or default from state if it doesn't exist
    def get_frame_flag(state, frame, name)
      value = frame.fetch("@#{name}", [state[name.to_sym]]).first
      !!(value?(value) ? value['@value'] : value)
    end

    ##
    # Removes an existing embed.
    #
    # @param state the current framing state.
    # @param id the @id of the embed to remove.
    def remove_embed(state, id)
      debug("frame") {"remove embed #{id.inspect}"}
      # get existing embed
      embeds = state[:embeds];
      embed = embeds[id];
      parent = embed[:parent];
      property = embed[:property];

      # create reference to replace embed
      subject = {}
      subject['@id'] = id
      ref = {'@id' => id}
      
      # remove existing embed
      if subject?(parent)
        # replace subject with reference
        parent[property].map! do |v|
          v.is_a?(Hash) && v.fetch('@id', nil) == id ? ref : v
        end
      end

      # recursively remove dependent dangling embeds
      def remove_dependents(id, embeds)
        debug("frame") {"remove dependents for #{id}"}

        depth do
          # get embed keys as a separate array to enable deleting keys in map
          embeds.each do |id_dep, e|
            p = e.fetch(:parent, {}) if e.is_a?(Hash)
            next unless p.is_a?(Hash)
            pid = p.fetch('@id', nil)
            if pid == id
              debug("frame") {"remove #{id_dep} from embeds"}
              embeds.delete(id_dep)
              remove_dependents(id_dep, embeds)
            end
          end
        end
      end
      
      remove_dependents(id, embeds)
    end

    ##
    # Adds framing output to the given parent.
    #
    # @param state the current framing state.
    # @param parent the parent to add to.
    # @param property the parent property, null for an array parent.
    # @param output the output to add.
    def add_frame_output(state, parent, property, output)
      if parent.is_a?(Hash)
        debug("frame") { "add for property #{property.inspect}: #{output.inspect}"}
        parent[property] ||= []
        parent[property] << output
      else
        debug("frame") { "add top-level: #{output.inspect}"}
        parent << output
      end
    end
    
    ##
    # Embeds values for the given element and property into output.
    def embed_values(state, element, property, output)
      element[property].each do |o|
        # Get element @id, if this is an object
        sid = o['@id'] if subject_reference?(o)
        if sid
          unless state[:embeds].has_key?(sid)
            debug("frame") {"embed element #{sid.inspect}"}
            # Embed full element, if it isn't already embedded
            embed = {:parent => output, :property => property}
            state[:embeds][sid] = embed
          
            # Recurse into element
            s = @subjects.fetch(sid, {'@id' => sid})
            o = {}
            s.each do |prop, value|
              if prop[0,1] == '@'
                # Copy keywords
                o[prop] = s[prop].dup
              else
                depth do
                  debug("frame") {"embed property #{prop.inspect} value #{value.inspect}"}
                  embed_values(state, s, prop, o)
                end
              end
            end
          else
            debug("frame") {"don't embed element #{sid.inspect}"}
          end
        else
          debug("frame") {"embed property #{property.inspect}, value #{o.inspect}"}
        end
        add_frame_output(state, output, property, o.dup)
      end
    end
  end
end