%%%------------------------------------------------------------------------
%% Copyright 2019, OpenTelemetry Authors
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% @doc The types defined here, and referencing records in opentelemetry.hrl
%% are used to store trace information while being collected on the
%% Erlang node.
%%
%% Thus, while the types are based on protos found in the opentelemetry-proto
%% repo: src/opentelemetry/proto/trace/v1/trace.proto,
%% they are not exact translations because further processing is done after
%% the span has finished and can be vendor specific. For example, there is
%% no count of the number of dropped attributes in the span record. And
%% an attribute's value can be a function to only evaluate the value if it
%% is actually used (at the time of exporting). And the stacktrace is a
%% regular Erlang stack trace.
%% @end
%%%-------------------------------------------------------------------------
-module(opentelemetry).

-export([start_tracer_provider/2,
         set_default_tracer/1,
         set_default_tracer/2,
         create_application_tracers/1,
         get_tracer/0,
         get_tracer/1,
         get_tracer/3,
         set_tracer/2,
         get_application/1,
         get_application_tracer/1,
         get_application_scope/1,
         set_text_map_propagator/1,
         set_text_map_extractor/1,
         get_text_map_extractor/0,
         set_text_map_injector/1,
         get_text_map_injector/0,
         instrumentation_scope/3,
         instrumentation_library/3,
         timestamp/0,
         timestamp_to_nano/1,
         convert_timestamp/2,
         link/1,
         link/2,
         link/4,
         links/1,
         event/2,
         event/3,
         events/1,
         status/1,
         status/2,
         verify_and_set_term/3,
         vsn_to_binary/1]).

-include("opentelemetry.hrl").
-include_lib("kernel/include/logger.hrl").
-compile({no_auto_import,[link/2]}).

-export_type([tracer/0,
              instrumentation_scope/0,
              trace_id/0,
              span_id/0,
              hex_trace_id/0,
              hex_span_id/0,
              trace_flags/0,
              timestamp/0,
              span_name/0,
              span_ctx/0,
              span/0,
              span_kind/0,
              link/0,
              attribute_key/0,
              attribute_value/0,
              attribute/0,
              attributes_map/0,
              event/0,
              event_name/0,
              tracestate/0,
              status/0,
              status_code/0,
              resource/0,
              text_map/0]).

-type tracer()             :: {module(), term()}.

-type instrumentation_scope() :: #instrumentation_scope{}.

-type trace_id()           :: non_neg_integer().
-type span_id()            :: non_neg_integer().

-type hex_trace_id()       :: binary().
-type hex_span_id()        :: binary().

-type trace_flags()        :: non_neg_integer().

-type timestamp()          :: integer().

-type span_ctx()           :: #span_ctx{}.
-type span()               :: term().
-type span_name()          :: unicode:unicode_binary() | atom().

-type attribute_key()      :: unicode:unicode_binary() | atom().
-type attribute_value()    :: unicode:unicode_binary()   |
                              atom()                     |
                              number()                   |
                              boolean()                  |
                              [unicode:unicode_binary() | atom() | float() | integer() | boolean()] |
                              tuple().
-type attribute()          :: {attribute_key(), attribute_value()}.
-type attributes_map()     :: #{attribute_key() => attribute_value()} |
                              [attribute()].

-type span_kind()          :: ?SPAN_KIND_INTERNAL    |
                              ?SPAN_KIND_SERVER      |
                              ?SPAN_KIND_CLIENT      |
                              ?SPAN_KIND_PRODUCER    |
                              ?SPAN_KIND_CONSUMER.
-type event()              :: #{system_time_native => integer(),
                                name              := event_name(),
                                attributes        := attributes_map()}.
-type event_name()         :: unicode:unicode_binary() | atom().
-type link()               :: #{trace_id   := trace_id(),
                                span_id    := span_id(),
                                attributes := attributes_map(),
                                tracestate := otel_tracestate:t()}.
-type status()             :: #status{}.
-type status_code()        :: ?OTEL_STATUS_UNSET | ?OTEL_STATUS_OK | ?OTEL_STATUS_ERROR.

%% The key must begin with a lowercase letter, and can only contain
%% lowercase letters 'a'-'z', digits '0'-'9', underscores '_', dashes
%% '-', asterisks '*', and forward slashes '/'.
%% The value is opaque string up to 256 characters printable ASCII
%% RFC0020 characters (i.e., the range 0x20 to 0x7E) except ',' and '='.
%% Note that this also excludes tabs, newlines, carriage returns, etc.
-type tracestate()         :: otel_tracestate:t().

-type resource()           :: #{unicode:unicode_binary() => unicode:unicode_binary()}.

-type text_map()       :: [{unicode:unicode_binary(), unicode:unicode_binary()}].

-define(TRACER_KEY(TracerProvider, Name), {?MODULE, TracerProvider, tracer, Name}).
-define(DEFAULT_TRACER_KEY(TracerProvider), ?TRACER_KEY(TracerProvider, '$__default_tracer')).
-define(MODULE_TO_APPLICATION_KEY, {?MODULE, otel_module_to_application_key}).
-define(TEXT_MAP_EXTRACTOR_KEY, {?MODULE, text_map_extractor}).
-define(TEXT_MAP_INJECTOR_KEY, {?MODULE, text_map_injector}).

%% uncomment when OTP-23 becomes the minimum required version
%% -deprecated({start_tracer_provider, 2, "start the TracerProvider through the SDK"}).

-include("gradualizer.hrl").

%% @deprecated Start the TracerProvider through the SDK"
-spec start_tracer_provider(atom(), map()) -> {ok, pid() | undefined} | {error, term()}.
start_tracer_provider(Name, Config) ->
    otel_tracer_provider:start(Name, Config).

-spec set_default_tracer(tracer()) -> boolean().
set_default_tracer(Tracer) ->
    set_default_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Tracer).

-spec set_default_tracer(atom(), tracer()) -> boolean().
set_default_tracer(TracerProvider, Tracer) ->
    verify_and_set_term(Tracer, ?DEFAULT_TRACER_KEY(TracerProvider), otel_tracer).

-spec set_tracer(atom(), tracer()) -> boolean().
set_tracer(Name, Tracer) ->
    set_tracer(Name, <<>>, undefined, Tracer).

-spec set_tracer(Name, Vsn, SchemaUrl, Tracer) -> boolean() when
      Name :: atom(),
      Vsn :: unicode:chardata() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined,
      Tracer:: opentelemetry:tracer().
set_tracer(Name, Vsn, SchemaUrl, Tracer) ->
    set_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Name, Vsn, SchemaUrl, Tracer).

-spec set_tracer(TracerProvider, Name, Vsn, SchemaUrl, Tracer) -> boolean() when
      TracerProvider :: atom(),
      Name :: atom(),
      Vsn :: unicode:chardata() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined,
      Tracer:: opentelemetry:tracer().
set_tracer(TracerProvider, Name, Vsn, SchemaUrl, Tracer) ->
    verify_and_set_term(Tracer, ?TRACER_KEY(TracerProvider, {Name, Vsn, SchemaUrl}), otel_tracer).

-spec create_application_tracers([{Application, Description, Vsn}]) -> ok when
      Application :: atom(),
      Description :: string(),
      Vsn :: string().
create_application_tracers(Applications) ->
    TracerMap = lists:foldl(fun({Name, _Description, Version}, Acc) ->
                                    Vsn = vsn_to_binary(Version),
                                    SchemaUrl = application:get_env(Name, otel_schema_url, undefined),
                                    _ = get_tracer(Name, Version, SchemaUrl),
                                    maps:merge(Acc, module_to_application(Name, Vsn, SchemaUrl))
                            end, #{}, Applications),
    persistent_term:put(?MODULE_TO_APPLICATION_KEY, TracerMap).

%% creates a map of modules to application name, version and schema_url tuple
module_to_application(Name, Version, SchemaUrl) ->
    {ok, Modules} = application:get_key(Name, modules),
    lists:foldl(fun(M, Acc) ->
                        Acc#{M => {Name, Version, SchemaUrl}}
                end, #{}, Modules).

%% @doc Returns the default global tracer.
%%
%% Without the `opentelemetry' application loaded (as a dependency) and
%% started (or another SDK), this function returns the default value
%% `{otel_tracer_noop, []}'.
-spec get_tracer() -> tracer().
get_tracer() ->
    get_tracer_(?GLOBAL_TRACER_PROVIDER_NAME).

-spec get_tracer_(atom()) -> tracer().
get_tracer_(TracerProvider) ->
    persistent_term:get(?DEFAULT_TRACER_KEY(TracerProvider), {otel_tracer_noop, []}).

%% @doc Returns the tracer for the given name.
%%
%% Passing `{Name, Vsn, SchemaUrl}' is the same as calling
%% `get_tracer(Name, Vsn, SchemaUrl)'.
%% @see get_tracer/3
-spec get_tracer(Name) -> Tracer when
      Name :: atom() | {atom(), Vsn, SchemaUrl},
      Vsn :: unicode:chardata() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined,
      Tracer :: opentelemetry:tracer().
get_tracer('$__default_tracer') ->
    get_tracer();
get_tracer({Name, Vsn, SchemaUrl}) ->
    get_tracer(Name, Vsn, SchemaUrl);
get_tracer(Name) ->
    get_tracer(Name, undefined, undefined).

%% @equiv get_tracer({Name, Vsn, SchemaUrl})
-spec get_tracer(Name, Vsn, SchemaUrl) -> Tracer when
      Name :: atom(),
      Vsn :: unicode:chardata() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined,
      Tracer :: opentelemetry:tracer().
get_tracer(Name, Vsn, SchemaUrl) ->
    get_tracer(?GLOBAL_TRACER_PROVIDER_NAME, Name, Vsn, SchemaUrl).

-spec get_tracer(TracerProvider, Name, Vsn, SchemaUrl) -> Tracer when
      TracerProvider :: atom() | pid(),
      Name :: atom(),
      Vsn :: unicode:chardata() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined,
      Tracer :: opentelemetry:tracer().
get_tracer(TracerProvider, Name, Vsn, SchemaUrl) ->
    %% check cache and then use provider to get the tracer if it isn't cached yet
    case persistent_term:get(?TRACER_KEY(TracerProvider, {Name, Vsn, SchemaUrl}), undefined) of
        undefined ->
            VsnBin = vsn_to_binary(Vsn),
            Tracer = otel_tracer_provider:get_tracer(TracerProvider, Name, VsnBin, SchemaUrl),

            %% cache the tracer
            _ = set_tracer(Name, Vsn, SchemaUrl, Tracer),

            Tracer;
        Tracer ->
            Tracer
    end.

-spec get_application_tracer(module()) -> tracer().
get_application_tracer(ModuleName) ->
    get_tracer(get_application(ModuleName)).

-spec get_application_scope(module()) -> instrumentation_scope() | undefined.
get_application_scope(ModuleName) ->
    case get_application(ModuleName) of
        {Name, Vsn, SchemaUrl} ->
            instrumentation_scope(Name, Vsn, SchemaUrl);
        _ ->
            instrumentation_scope(<<>>, <<>>, <<>>)
    end.

%% looks up the name, version and schema_url used to create a Trace for the OTP
%% Application a module is in. This name is used to look up a Tracer to use so
%% if none is found for the ModuleName the key used for the default tracer.
-spec get_application(module()) -> ApplicationTuple when
      ApplicationTuple :: {Name, Vsn, SchemaUrl} | atom(),
      Name :: atom(),
      Vsn :: unicode:unicode_binary() | undefined,
      SchemaUrl :: uri_string:uri_string() | undefined.
get_application(ModuleName) ->
    Map = persistent_term:get(?MODULE_TO_APPLICATION_KEY, #{}),
    maps:get(ModuleName, Map, '$__default_tracer').

%% setting the propagator is the same as setting the same injector and extractor
set_text_map_propagator(Propagator) ->
    set_text_map_injector(Propagator),
    set_text_map_extractor(Propagator).

set_text_map_extractor(Propagator) ->
    persistent_term:put(?TEXT_MAP_EXTRACTOR_KEY, Propagator).

set_text_map_injector(Propagator) ->
    persistent_term:put(?TEXT_MAP_INJECTOR_KEY, Propagator).

get_text_map_extractor() ->
    persistent_term:get(?TEXT_MAP_EXTRACTOR_KEY, otel_propagator_text_map_noop).

get_text_map_injector() ->
    persistent_term:get(?TEXT_MAP_INJECTOR_KEY, otel_propagator_text_map_noop).

%% @doc A monotonically increasing time provided by the Erlang runtime system in the native time unit.
%% This value is the most accurate and precise timestamp available from the Erlang runtime and
%% should be used for finding durations or any timestamp that can be converted to a system
%% time before being sent to another system.

%% Use {@link convert_timestamp/2} or {@link timestamp_to_nano/1} to convert a native monotonic time to a
%% system time of either nanoseconds or another {@link erlang:time_unit()}.

%% Using these functions allows timestamps to be accurate, used for duration and be exportable
%% as POSIX time when needed.
%% @end
-spec timestamp() -> integer().
timestamp() ->
    erlang:monotonic_time().

%% @doc Convert a native monotonic timestamp to nanosecond POSIX time. Meaning the time since Epoch.
%% Epoch is defined to be 00:00:00 UTC, 1970-01-01.
%% @end
-spec timestamp_to_nano(timestamp()) -> pos_integer().
timestamp_to_nano(Timestamp) ->
    ?assert_type(convert_timestamp(Timestamp, nanosecond), pos_integer()).

%% @doc Convert a native monotonic timestamp to POSIX time of any {@link erlang:time_unit()}.
%% Meaning the time since Epoch. Epoch is defined to be 00:00:00 UTC, 1970-01-01.
%% @end
-spec convert_timestamp(timestamp(), erlang:time_unit()) -> integer().
convert_timestamp(Timestamp, Unit) ->
    Offset = erlang:time_offset(),
    erlang:convert_time_unit(Timestamp + Offset, native, Unit).

%% @doc Creates a list of Span links from the given `List'.
%%
%% This is equivalent to calling {@link link/2} or {@link link/4} multiple times.
-spec links([TraceIdAndSpanId | span_ctx() | {span_ctx(), Attributes}]) -> [link()] when
      TraceIdAndSpanId :: {trace_id(), span_id(), Attributes, TraceState},
      Attributes :: attributes_map(),
      TraceState :: otel_tracestate:t() | [{string(), string()}].
links(List) when is_list(List) ->
    lists:filtermap(fun({TraceId, SpanId, Attributes, TraceState}) when is_integer(TraceId) ,
                                                                        is_integer(SpanId) ->
                            link_or_false(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
                       ({#span_ctx{trace_id=TraceId,
                                   span_id=SpanId,
                                   tracestate=TraceState}, Attributes}) when is_integer(TraceId) ,
                                                                             is_integer(SpanId) ->
                            link_or_false(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
                       (#span_ctx{trace_id=TraceId,
                                  span_id=SpanId,
                                  tracestate=TraceState}) when is_integer(TraceId) ,
                                                               is_integer(SpanId) ->
                            link_or_false(TraceId, SpanId, [], TraceState);
                       (_) ->
                            false
              end, List);
links(_) ->
    [].


%% @equiv link(SpanCtx, [])
-spec link(span_ctx() | undefined) -> link() | undefined.
link(SpanCtx) ->
    link(SpanCtx, []).

%% @doc Creates a Span link to the Span represented by the given `SpanCtx'.
%%
%% The returned link can be used in the `links' field of a Span.
-spec link(span_ctx() | undefined, attributes_map()) -> link() | undefined.
link(_SpanCtx = #span_ctx{trace_id=TraceId,
                          span_id=SpanId,
                          tracestate=TraceState}, Attributes) ->
    ?MODULE:link(TraceId, SpanId, otel_attributes:process_attributes(Attributes), TraceState);
link(_, _) ->
    undefined.

%% @doc Creates a Span link to the Span represented by the given `TraceId' and `SpanId'.
%%
%% The returned link can be used in the `links' field of a Span.
-spec link(TraceId, SpanId, Attributes, TraceState) -> link() | undefined when
      TraceId :: trace_id(),
      SpanId :: span_id(),
      Attributes :: attributes_map(),
      TraceState :: otel_tracestate:t().
link(TraceId, SpanId, Attributes, TraceState) when is_integer(TraceId),
                                                   is_integer(SpanId),
                                                   (is_list(Attributes) orelse is_map(Attributes)) ->
    #{trace_id => TraceId,
      span_id => SpanId,
      attributes => otel_attributes:process_attributes(Attributes),
      tracestate => TraceState};
link(_, _, _, _) ->
    undefined.

%% @equiv event(opentelemetry:timestamp(), Name, Attributes)
-spec event(Name, Attributes) -> event() | undefined when
      Name :: event_name(),
      Attributes :: attributes_map().
event(Name, Attributes) ->
    event(opentelemetry:timestamp(), Name, Attributes).

%% @doc Creates a Span event with the given `Name' and `Attributes'.
%%
%% The Span event is marked to have happened at `Timestamp'. The returned
%% event can be used to add an event to a Span through {@link otel_span:add_events/2},
%% for example.
-spec event(Timestamp, Name, Attributes) -> event() | undefined when
      Timestamp :: integer(),
      Name :: event_name(),
      Attributes :: attributes_map().
event(Timestamp, Name, Attributes) when is_integer(Timestamp),
                                        (is_list(Attributes) orelse is_map(Attributes)) ->

    case otel_span:is_valid_name(Name) of
        true ->
            #{system_time_native => Timestamp,
              name => Name,
              attributes => otel_attributes:process_attributes(Attributes)};
        false ->
            undefined
    end;
event(_, _, _) ->
    undefined.

%% @doc Creates a list of Span events from the given `List'.
%%
%% This is a convenience function to create a list of Span events from a list
%% of `{Time, Name, Attributes}' or `{Name, Attributes}' tuples. It's equivalent
%% to calling {@link event/2} or {@link event/3} multiple times. This function
%% also automatically filters out any invalid tuple.
-spec events([Event]) -> [event()] when
      Event :: {Timestamp :: integer(), event_name(), attributes_map()} |
               {event_name(), attributes_map()}.
events(Events) ->
    Now = opentelemetry:timestamp(),
    lists:filtermap(fun({Time, Name, Attributes}) ->
                            case event(Time, Name, Attributes) of
                                undefined ->
                                    false;
                                Event ->
                                    {true, Event}
                            end;
                       ({Name, Attributes}) ->
                            case event(Now, Name, Attributes) of
                                undefined ->
                                    false;
                                Event ->
                                    {true, Event}
                            end;
                       (_) ->
                            false
                    end, Events).

%% @doc Create a Span status from the given `Code'.
%%
%% The returned status can be used to set the status of a Span through
%% {@link otel_span:set_status/2}, for example.
-spec status(Code) -> status() | undefined when
      Code :: status_code().
status(Code) ->
    status(Code, <<>>).

%% @doc Create a Span status from the given `Code' and with the given `Message'.
%%
%% The returned status can be used to set the status of a Span through
%% {@link otel_span:set_status/2}, for example.
-spec status(Code, Message) -> status() | undefined when
      Code :: status_code(),
      Message :: unicode:unicode_binary().
status(?OTEL_STATUS_ERROR, Message) when is_binary(Message) ->
    #status{code=?OTEL_STATUS_ERROR, message=Message};
status(?OTEL_STATUS_OK, _Message) ->
    #status{code=?OTEL_STATUS_OK};
status(?OTEL_STATUS_UNSET, _Message) ->
    #status{code=?OTEL_STATUS_UNSET};
status(_, _) ->
    undefined.

%% internal functions

%% @private
-spec verify_and_set_term(module() | {module(), term()}, term(), atom()) -> boolean().
verify_and_set_term(Module, TermKey, Behaviour) ->
    case verify_module_exists(Module) of
        true ->
            persistent_term:put(TermKey, Module),
            true;
        false ->
            ?LOG_WARNING("Module ~p does not exist. "
                         "A noop ~p will be used.",
                         [Module, Behaviour]),
            false
    end.

%% @private
-spec verify_module_exists(module() | {module(), term()}) -> boolean().
verify_module_exists({Module, _}) ->
    verify_module_exists(Module);
verify_module_exists(Module) ->
    try Module:module_info() of
        _ ->
          true
    catch
        error:undef ->
            false
    end.

%% for use in a filtermap
%% return {true, Link} if a link is returned or return false
%% a list is supported for tracestate for backwards compatibility
%% @private
link_or_false(TraceId, SpanId, Attributes, TraceState) when is_list(TraceState) ->
    link_or_false(TraceId, SpanId, Attributes, otel_tracestate:new(TraceState));
link_or_false(TraceId, SpanId, Attributes, TraceState) ->
    case link(TraceId, SpanId, Attributes, TraceState) of
        Link=#{}->
            {true, Link};
        _ ->
            false
    end.

instrumentation_scope(Name, Vsn, SchemaUrl) ->
    case name_to_binary(Name) of
        undefined ->
            undefined;
        BinaryName ->
            #instrumentation_scope{name=BinaryName,
                                   version=vsn_to_binary(Vsn),
                                   schema_url=schema_url_to_binary(SchemaUrl)}
    end.

%% this function remains solely to keep backwards compatibility
%% but `instrumentation_scope' should be used instead
instrumentation_library(Name, Vsn, SchemaUrl) ->
    instrumentation_scope(Name, Vsn, SchemaUrl).

%% schema_url is option, so set to undefined if its not a string
schema_url_to_binary(SchemaUrl) when is_binary(SchemaUrl) ; is_list(SchemaUrl) ->
    unicode:characters_to_binary(SchemaUrl);
schema_url_to_binary(_) ->
    undefined.

%% Vsn can't be an atom or anything but a list or binary
%% so return empty binary string if it isn't a list or binary.
%% @private
vsn_to_binary(Vsn) when is_binary(Vsn) ; is_list(Vsn) ->
    unicode:characters_to_binary(Vsn);
vsn_to_binary(_) ->
    <<>>.

%% Instrumentation name can be atom, list or binary. But atom `undefined'
%% must stay as `undefined' atom.
name_to_binary(undefined)->
    undefined;
name_to_binary(T) when is_atom(T) ->
    atom_to_binary(T, utf8);
name_to_binary(T) when is_list(T) ->
    list_to_binary(T);
name_to_binary(T) when is_binary(T) ->
    T.
