# encoding: utf-8

require 'ting_yun/agent/transaction/transaction_state'
require 'ting_yun/instrumentation/support/transaction_namer'
require 'ting_yun/agent/transaction'
require 'ting_yun/agent'
require 'ting_yun/support/helper'

module TingYun
  module Instrumentation
    module Support

      # This module can also be used to capture performance information for
      # background tasks and other non-web transactions, including
      # detailed transaction traces and traced errors.
      module ControllerInstrumentation

        extend self

        def self.included klass
          klass.extend ClassMethods
        end

        def self.extended klass
          klass.extend ClassMethods
        end


        module ClassMethods
          # Add transaction tracing to the given method.  This will treat
          # the given method as a main entrypoint for instrumentation, just
          # like controller actions are treated by default.  Useful especially
          # for background tasks.
          # Example for background job:
          #   class Job
          #     include TingYun::Instrumentation::Support::ControllerInstrumentation
          #     def run(task)
          #        ...
          #     end
          #     # Instrument run so tasks show up under task.name.  Note single
          #     # quoting to defer eval to runtime.
          #     add_transaction_tracer :run, :name => '#{args[0].name}'
          #   end
          #
          # Here's an example of a controller that uses a dispatcher
          # action to invoke operations which you want treated as top
          # level actions, so they aren't all lumped into the invoker
          # action.
          #
          #   MyController < ActionController::Base
          #     include TingYun::Instrumentation::Support::ControllerInstrumentation
          #     # dispatch the given op to the method given by the service parameter.
          #     def invoke_operation
          #       op = params['operation']
          #       send op
          #     end
          #     # Ignore the invoker to avoid double counting
          #     tingyun_ignore :only => 'invoke_operation'
          #     # Instrument the operations:
          #     add_transaction_tracer :print
          #     add_transaction_tracer :show
          #     add_transaction_tracer :forward
          #   end
          #
          # Here's an example of how to pass contextual information into the transaction
          # so it will appear in transaction traces:
          #
          #   class Job
          #    include TingYun::Instrumentation::Support::ControllerInstrumentation
          #     def process(account)
          #        ...
          #     end
          #     # Include the account name in the transaction details.  Note the single
          #     # quotes to defer eval until call time.
          #     add_transaction_tracer :process, :params => '{ :account_name => args[0].name }'
          #   end
          #``
          #
          # @api public
          #
          def add_transaction_tracer(method, options={})
            options[:name] ||= method.to_s
            argument_list = generate_argument_list(options)
            traced_method, punctuation = parse_punctuation(method)
            with_method_name, without_method_name = build_method_names(traced_method, punctuation)

            if already_added_transaction_tracer?(self, with_method_name)
              ::TingYun::Agent.logger.warn("Transaction tracer already in place for class = #{self.name}, method = #{method.to_s}, skipping")
              return
            end

            class_eval <<-EOC
            def #{with_method_name}(*args,&block)
              perform_action_with_tingyun_trace(#{argument_list.join(',')}) do
                #{without_method_name}(*args, &block)
              end
            end
            EOC

            visibility = TingYun::Helper.instance_method_visibility self, method

            alias_method without_method_name, method.to_s
            alias_method method.to_s, with_method_name
            send visibility, method
            send visibility, with_method_name
            ::TingYun::Agent.logger.debug("Traced transaction: class = #{self.name}, method = #{method.to_s}, options = #{options.inspect}")
          end

          def already_added_transaction_tracer?(target, with_method_name)
            if TingYun::Helper.instance_methods_include?(target, with_method_name)
              true
            else
              false
            end
          end

          def parse_punctuation(method)
            [method.to_s.sub(/([?!=])$/, ''), $1]
          end

          def build_method_names(traced_method, punctuation)
            [ "#{traced_method.to_s}_with_tingyun_transaction_trace#{punctuation}",
              "#{traced_method.to_s}_without_tingyun_transaction_trace#{punctuation}" ]
          end

          def generate_argument_list(options)
            options.map do |key, value|
              value = if value.is_a?(Symbol)
                        value.inspect
                      elsif key == :params
                        value.to_s
                      else
                        %Q["#{value.to_s}"]
                      end

              %Q[:#{key} => #{value}]
            end
          end



        end



        # Yield to the given block with TingYun tracing.  Used by
        # default instrumentation on controller actions in Rails and Merb.
        # But it can also be used in custom instrumentation of controller
        # methods and background tasks.

        # This is the method invoked by instrumentation added by the
        # <tt>ClassMethods#add_transaction_tracer</tt>.

        # Below is a controller with an +invoke_operation+ action which
        # dispatches to more specific operation methods based on a
        # parameter (very dangerous, btw!).  With this instrumentation,
        # the +invoke_operation+ action is ignored but the operation
        # methods show up in TingYun as if they were first class controller
        # actions
        #
        #   MyController < ActionController::Base
        #     include TingYun::Instrumentation::Support::ControllerInstrumentation
        #     # dispatch the given op to the method given by the service parameter.
        #     def invoke_operation
        #       op = params['operation']
        #       perform_action_with_tingyun_trace(:name => op) do
        #         send op, params['message']
        #       end
        #     end
        #     # Ignore the invoker to avoid double counting
        #     tingyun_ignore :only => 'invoke_operation'
        #   end
        #
        # When invoking this method explicitly as in the example above, pass in a
        # block to measure with some combination of options:
        #
        # * <tt>:category => :controller</tt> indicates that this is a
        #   controller action and will appear with all the other actions.  This
        #   is the default.
        # * <tt>:category => :task</tt> indicates that this is a
        #   background task and will show up in Ting Yun with other background
        #   tasks instead of in the controllers list
        # * <tt>:category => :middleware</tt> if you are instrumenting a rack
        #   middleware call.  The <tt>:name</tt> is optional, useful if you
        #   have more than one potential transaction in the #call.
        # * <tt>:category => :uri</tt> indicates that this is a
        #   web transaction whose name is a normalized URI, where  'normalized'
        #   means the URI does not have any elements with data in them such
        #   as in many REST URIs.
        # * <tt>:name => action_name</tt> is used to specify the action
        #   name used as part of the metric name
        # * <tt>:params => {...}</tt> to provide information about the context
        #   of the call, used in transaction trace display, for example:
        #   <tt>:params => { :account => @account.name, :file => file.name }</tt>
        #   These are treated similarly to request parameters in web transactions.
        #
        # Seldomly used options:
        #
        # * <tt>:class_name => aClass.name</tt> is used to override the name
        #   of the class when used inside the metric name.  Default is the
        #   current class.
        # * <tt>:path => metric_path</tt> is *deprecated* in the public API.  It
        #   allows you to set the entire metric after the category part.  Overrides
        #   all the other options.
        # * <tt>:request => Rack::Request#new(env)</tt> is used to pass in a
        #   request object that may respond to path and referer.
        #
        # @api public
        #
        NR_DEFAULT_OPTIONS    = {}.freeze          unless defined?(NR_DEFAULT_OPTIONS   )

        def perform_action_with_tingyun_trace (*args, &block)

          state = TingYun::Agent::TransactionState.tl_get

          skip_tracing = !state.execution_traced?

          if skip_tracing
            state.current_transaction.ignore! if state.current_transaction
            TingYun::Agent.disable_all_tracing { return yield }
          end
          trace_options = args.last.is_a?(Hash) ? args.last : NR_DEFAULT_OPTIONS
          category = trace_options[:category] || :controller
          txn_options = create_transaction_options(trace_options, category)

          begin
            TingYun::Agent::Transaction.start(state, category, txn_options)
            begin
              yield
            rescue => e
              ::TingYun::Agent.notice_error(e)
              raise
            end
          ensure
            TingYun::Agent::Transaction.stop(state)
          end
        end

        private

        def create_transaction_options(trace_options, category)
          txn_options = {}

          txn_options[:request] ||= request if respond_to?(:request)
          txn_options[:request] ||= trace_options[:request] if trace_options[:request]
          txn_options[:filtered_params] = trace_options[:params]
          txn_options[:transaction_name] = TingYun::Instrumentation::Support::TransactionNamer.name_for(nil, self, category, trace_options)
          txn_options[:apdex_start_time] = Time.now

          txn_options
        end
      end
    end
  end

end