module Sequel::Plugins::VcapSerialization
  # This plugin implements serialization and deserialization of
  # Sequel::Models to/from hashes and json.

  module InstanceMethods
    # Return a hash of the model instance containing only the parameters
    # specified by export_attributes.
    #
    # @option opts [Array<String>] :only Only export an attribute if it is both
    # included in export_attributes and in the :only option.
    #
    # @return [Hash] The hash representation of the instance only containing
    # the attributes specified by export_attributes and the optional :only
    # parameter.
    def to_hash(opts={})
      hash = {}
      redact_vals = opts[:redact]
      attrs = opts[:attrs] || export_attrs

      attrs.each do |k|
        next unless opts[:only].nil? || opts[:only].include?(k)

        value = send(k)
        hash[k.to_s] = if value.respond_to?(:nil_object?) && value.nil_object?
                         nil
                       elsif !redact_vals.nil? && redact_vals.include?(k.to_s)
                         { redacted_message: VCAP::CloudController::Presenters::Censorship::PRIVATE_DATA_HIDDEN }
                       else
                         value
                       end
      end
      hash
    end

    # Returns an array of attribute names that will be used when calling to_hash
    def export_attrs
      self.class.export_attrs || []
    end

    delegate :export_attrs_from_methods, to: :class

    # Update the model instance from the supplied json string.  Only update
    # attributes specified by import_attributes.
    #
    # @param [String] Json encoded representation of the updated attributes.
    #
    # @option opts [Array<String>] :only Only import an attribute if it is both
    # included in import_attributes and in the :only option.
    def update_from_json(json, opts={})
      parsed = Oj.load(json)
      update_from_hash(parsed, opts)
    end

    # Update the model instance from the supplied hash.  Only update
    # attributes specified by import_attributes.
    #
    # @param [Hash] Hash of the updated attributes.
    #
    # @option opts [Array<String>] :only Only import an attribute if it is both
    # included in import_attributes and in the :only option.
    def update_from_hash(hash, opts={})
      update_opts = self.class.update_or_create_options(hash, opts)

      # Cannot use update(update_opts) because it does not
      # update updated_at timestamp when no changes are being made.
      # Arguably this should avoid updating updated_at if nothing changed.
      set(update_opts)
      save
    end
  end

  module ClassMethods
    # Create a new model instance from the supplied json string.  Only include
    # attributes specified by import_attributes.
    #
    # @param [String] Json encoded representation attributes.
    #
    # @option opts [Array<String>] :only Only include an attribute if it is
    # both included in import_attributes and in the :only option.
    #
    # @return [Sequel::Model] The created model.
    def create_from_json(json, opts={})
      hash = Oj.load(json)
      create_from_hash(hash, opts)
    end

    # Create and save a new model instance from the supplied json string.
    # Only include attributes specified by import_attributes.
    #
    # @param [Hash] Hash of the attributes.
    #
    # @option opts [Array<String>] :only Only include an attribute if it is
    # both included in import_attributes and in the :only option.
    #
    # @return [Sequel::Model] The created model.
    def create_from_hash(hash, opts={})
      create_opts = update_or_create_options(hash, opts)
      create { |instance| instance.set(create_opts) }
    end

    # Set the default order during a to_json on the model class.
    #
    # @param [Array<Symbol>] List of attributes to include when serializing to
    # json or a hash.
    def export_attributes(*attributes)
      self.export_attrs = attributes.freeze
    end

    # after serializing an object to a hash based on export_attributes and
    # transient_attributes, swap in different values from methods on the object
    def export_attributes_from_methods(hash)
      self.export_attrs_from_methods = hash.freeze
    end

    # @param [Array<Symbol>] List of attributes to include when importing
    # from json or a hash.
    def import_attributes(*attributes)
      self.import_attrs = attributes
    end

    # Not intended to be called by consumers of the API, but needed
    # by instance of the class, so it can't be made private.
    def update_or_create_options(hash, opts)
      results = {}
      attrs = import_attrs || []
      attrs -= opts[:only] unless opts[:only].nil?
      attrs.each do |attr|
        key = nil
        if hash.key?(attr)
          key = attr
        elsif hash.key?(attr.to_s)
          key = attr.to_s
        end
        results[attr] = hash[key] unless key.nil?
      end
      results
    end

    attr_accessor :export_attrs, :import_attrs, :export_attrs_from_methods
  end
end
