# encoding: UTF-8
#
# Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

require 'multi_json'

require_relative '../core/project'

require_relative '../mixins/mixins'
require_relative '../rest/object'

module GoodData
  class MdObject < Rest::Resource
    MD_OBJ_CTG = 'obj'
    IDENTIFIERS_CFG = 'instance-identifiers'

    extend Mixin::MdIdToUri
    extend Mixin::MdObjectIndexer
    extend Mixin::MdObjectQuery

    extend Mixin::MdFinders
    extend Mixin::MdObjId

    include Mixin::Links
    include Mixin::ObjId
    include Mixin::MdRelations
    include Mixin::MdGrantees

    class << self
      # Method used for replacing objects like Attribute, Fact or Metric.
      # It takes the object. Scans its JSON representation and returns
      # a new one with object references changed according to mapping.
      # The references an be found either in the object structure or in
      # the MAQL in bracketed form. This implementation takes care only
      # of those in bracketed form.
      #
      # @param obj [GoodData::MdObject] what Object that should be replaced
      # @param mapping [Array[Array]] Array of mapping pairs.
      # @return [GoodData::MdObject]
      def replace_bracketed(obj, mapping)
        replace(obj, mapping) { |e, a, b| e.gsub("[#{a}]", "[#{b}]") }
      end

      # Method used for replacing objects like Attribute, Fact or Metric.
      # It takes the object. Scans its JSON representation and returns
      # a new one with object references changed according to mapping.
      # The references an be found either in the object structure or in the MAQL
      # in bracketed form. This implementation takes care only of those
      # in object structure where they are as a string in JSON.
      #
      # @param obj [GoodData::MdObject] Object that should be replaced
      # @param mapping [Array[Array]] Array of mapping pairs.
      # @return [GoodData::MdObject]
      def replace_quoted(obj, mapping)
        replace(obj, mapping) do |e, a, b|
          e.gsub("\"#{a}\"", "\"#{b}\"")
        end
      end

      # Helper method used for replacing objects like Attribute, Fact or Metric. It takes the object. Scans its JSON
      # representation yields for a client to perform replacement for each mapping pair and returns a new one
      # with object of the same type as obj.
      #
      # @param obj [GoodData::MdObject] Object that should be replaced
      # @param mapping [Array[Array]] Array of mapping pairs.
      # @param block [Proc] Block that receives the object state as a JSON string and mapping pair and expects a new object state as a JSON string back
      # @return [GoodData::MdObject]
      def replace(obj, mapping, &block)
        json = mapping.reduce(obj.to_json) do |a, e|
          obj_a, obj_b = e
          uri_what = obj_a.respond_to?(:uri) ? obj_a.uri : obj_a
          uri_for_what = obj_b.respond_to?(:uri) ? obj_b.uri : obj_b
          block.call(a, uri_what, uri_for_what)
        end
        client = obj.client
        client.create(obj.class, MultiJson.load(json), :project => obj.project)
      end

      # Helper method used for finding attribute elements that are interesting becuase they can be possibly
      # replaced according to mapping specification. This walks through all the attribute elemets. Picks only those
      # whose attribute is mentioned in the mapping. Walks through all the labels of that particular attribute and
      # tries to find a value from one to be translated into a label in second. Obviously this is not guaranteed to
      # find any results or in some cases can yield to incorrect results.
      #
      # @param obj [GoodData::MdObject] Object that should be replaced
      # @param mapping [Array[Array]] Array of mapping pairs.
      # @param block [Proc] Block that receives the object state as a JSON string and mapping pair and expects a new object state as a JSON string back
      # @return [GoodData::MdObject]
      def find_replaceable_values(obj, mapping)
        values_to_replace = GoodData::SmallGoodZilla.extract_element_uri_pairs(MultiJson.dump(obj.to_json))
        values_from_mapping = values_to_replace.select { |i| mapping.map { |a, _| a.uri }.include?(i.first) }
        replaceable_vals = values_from_mapping.map do |a_uri, id|
          from_attribute, to_attribute = mapping.find { |k, _| k.uri == a_uri }
          vals = from_attribute.values_for(id)
          labels = to_attribute.labels

          result = nil
          catch :found_value do
            labels.each do |l|
              vals.each do |v|
                throw :found_value if result
                result = begin
                           l.find_value_uri(v)
                         rescue
                           nil
                         end
              end
            end
          end
          fail "Unable to find replacement for #{a_uri}" unless result
          [a_uri, id, result]
        end
        replaceable_vals.map { |a, id, r| ["#{a}/elements?id=#{id}", r] }
      end
    end

    metadata_property_reader :uri, :identifier, :title, :summary, :tags, :category
    metadata_property_writer :tags, :summary, :title, :identifier

    def initialize(data)
      @json = data.to_hash
    end

    def add_tag(a_tag)
      self.tags = tag_set.add(a_tag).to_a.join(' ')
      self
    end

    def delete
      if saved? # rubocop:disable Style/GuardClause
        client.delete(uri)
        meta.delete('uri')
      end
    end

    def reload!
      @json = client.get(uri) if saved?
      self
    end

    alias_method :refresh, :reload!

    def browser_uri
      client.connection.server_url + meta['uri']
    end

    def deprecated
      meta['deprecated'] == '1' || get_flag?('deprecated')
    end
    alias_method :deprecated?, :deprecated

    def deprecated=(flag)
      if flag == '1' || flag == 1 || flag == true
        meta['deprecated'] = '1'
      elsif flag == '0' || flag == 0 || flag == false # rubocop:disable Style/NumericPredicate
        meta['deprecated'] = '0'
      else
        fail 'You have to provide flag as either 1 or "1" or 0 or "0" or true/false'
      end

      set_flag('deprecated', flag)
    end

    def production
      meta['isProduction'] == '1' || get_flag?('production')
    end
    alias_method :production?, :production

    def production=(flag)
      if flag
        meta['isProduction'] = '1'
      else
        meta['isProduction'] == '0'
      end

      set_flag('production', flag)
    end

    def restricted
      get_flag?('restricted')
    end
    alias_method :restricted?, :restricted

    def restricted=(flag)
      set_flag('restricted', flag)
    end

    def project
      @project ||= Project[uri.gsub(%r{\/obj\/\d+$}, ''), :client => client]
    end

    # Method used for replacing objects like Attribute, Fact or Metric. Returns new object of the same type.
    #
    # @param [GoodData::MdObject] what Object that should be replaced
    # @param [GoodData::MdObject] for_what Object it is replaced with
    # @return [GoodData::Metric]
    def replace(mapping)
      GoodData::MdObject.replace_quoted(self, mapping)
    end

    # Method used for replacing objects like Attribute, Fact or Metric. Returns itself mutated.
    # @param [GoodData::MdObject] what Object that should be replaced
    # @param [GoodData::MdObject] for_what Object it is replaced with
    # @return [GoodData::Metric]
    def replace!(mapping)
      x = replace(mapping)
      @json = x.json
      self
    end

    def remove_tag(a_tag)
      self.tags = tag_set.delete(a_tag).to_a.join(' ')
      self
    end

    def save
      fail('Validation failed') unless validate

      opts = {
        :client => client,
        :project => project
      }

      if saved?
        client.put(uri, to_json)
      else
        explicit_identifier = meta['identifier']
        # Pre-check to provide a user-friendly error rather than
        # failing later
        klass = self.class
        if explicit_identifier && klass[explicit_identifier, opts]
          fail "Identifier '#{explicit_identifier}' already in use"
        end

        req_uri = project.md['obj']
        result = client.post(req_uri, to_json)
        saved_object = self.class[result['uri'], opts]
        # TODO: add test for explicitly provided identifier

        @json = saved_object.json
        if explicit_identifier
          # Object creation API discards the identifier. If an identifier
          # was explicitely provided in the origina object, we need to set
          # it explicitly with an extra PUT call.
          meta['identifier'] = explicit_identifier
          begin
            client.put(uri, to_json)
          rescue => e
            # Cannot change the identifier (perhaps because it's in use
            # already?), cleaning up.
            client.delete(uri)
            raise e
          end
        end
      end
      self
    end

    # Saves an object with a different name
    #
    # @param new_title [String] New title. If not provided one is provided
    # @return [GoodData::MdObject] MdObject that has been saved as
    def save_as(new_title = nil)
      new_title = "Clone of #{title}" if new_title.nil?
      # rubocop:disable Security/MarshalLoad
      dupped = Marshal.load(Marshal.dump(json))
      # rubocop:enable Security/MarshalLoad
      dupped[root_key]['meta'].delete('uri')
      dupped[root_key]['meta'].delete('identifier')
      dupped[root_key]['meta']['title'] = new_title
      x = client.create(self.class, dupped, :project => project)
      x.save
    end

    def tag_set
      tags.split.to_set
    end

    def ==(other)
      other.respond_to?(:uri) && other.uri == uri && other.respond_to?(:to_hash) && other.to_hash == to_hash
    end

    def listed?
      !unlisted?
    end

    def unlisted
      meta['unlisted'] == '1'
    end
    alias_method :unlisted?, :unlisted

    def unlisted=(flag)
      if flag == true
        meta['unlisted'] = '1'
      elsif flag == false
        meta['unlisted'] = '0'
      else
        fail 'You have to provide flag as either true or false'
      end
    end

    def validate
      true
    end

    def get_flag?(flag)
      meta['flags'] && meta['flags'].include?(flag)
    end
    alias_method :has_flag?, :get_flag?

    def set_flag(flag, value)
      meta['flags'] = [] unless meta['flags']

      if (value == '1' || value == 1 || value == true) && !has_flag?(flag)
        meta['flags'].push(flag)
        meta['flags'].sort!
      elsif !value && has_flag?(flag)
        meta['flags'].delete(flag)
      end
    end
  end
end
