require "fiber"
require "channel"
require "crystal/tracing"

{% if flag?(:execution_context) %}
  require "fiber/execution_context"
{% else %}
  require "crystal/scheduler"
{% end %}

# Blocks the current fiber for the specified number of seconds.
#
# While this fiber is waiting this time, other ready-to-execute
# fibers might start their execution.
@[Deprecated("Use `::sleep(Time::Span)` instead")]
def sleep(seconds : Number) : Nil
  if seconds < 0
    raise ArgumentError.new "Sleep seconds must be positive"
  end
  sleep(seconds.seconds)
end

# Blocks the current Fiber for the specified time span.
#
# While this fiber is waiting this time, other ready-to-execute
# fibers might start their execution.
def sleep(time : Time::Span) : Nil
  Crystal.trace :sched, "sleep", for: time
  Crystal::EventLoop.current.sleep(time)
end

# Blocks the current fiber forever.
#
# Meanwhile, other ready-to-execute fibers might start their execution.
def sleep : Nil
  Fiber.suspend
end

{% begin %}
# Spawns a new fiber.
#
# When using execution contexts, the fiber spawns into the current execution
# context (`Fiber::ExecutionContext.current`).
#
# NOTE: The newly created fiber doesn't run as soon as spawned.
#
# Example:
# ```
# # Write "1" every 1 second and "2" every 2 seconds for 6 seconds.
#
# require "wait_group"
#
# wg = WaitGroup.new 2
#
# spawn do
#   6.times do
#     sleep 1.second
#     puts 1
#   end
# ensure
#   wg.done
# end
#
# spawn do
#   3.times do
#     sleep 2.seconds
#     puts 2
#   end
# ensure
#   wg.done
# end
#
# wg.wait
# ```
def spawn(*, name : String? = nil, same_thread = false, &block)
  {% if flag?(:execution_context) %}
    Fiber::ExecutionContext::Scheduler.current.spawn(name: name, same_thread: same_thread, &block)
  {% else %}
    fiber = Fiber.new(name, &block)
    Crystal.trace :sched, "spawn", fiber: fiber
    {% if flag?(:preview_mt) %} fiber.set_current_thread if same_thread {% end %}
    fiber.enqueue
    fiber
  {% end %}
end
{% end %}

# Spawns a fiber by first creating a `Proc`, passing the *call*'s
# expressions to it, and letting the `Proc` finally invoke the *call*.
#
# When using execution contexts, the fiber spawns into the current execution
# context (`Fiber::ExecutionContext.current`).
#
# Compare this:
#
# ```
# i = 0
# while i < 5
#   spawn { print(i) }
#   i += 1
# end
# Fiber.yield
# # Output: 55555
# ```
#
# To this:
#
# ```
# i = 0
# while i < 5
#   spawn print(i)
#   i += 1
# end
# Fiber.yield
# # Output: 01234
# ```
#
# This is because in the first case all spawned fibers refer to
# the same local variable, while in the second example copies of
# *i* are passed to a `Proc` that eventually invokes the call.
macro spawn(call, *, name = nil, same_thread = false, &block)
  {% if block %}
    {% raise "`spawn(call)` can't be invoked with a block, did you mean `spawn(name: ...) { ... }`?" %}
  {% end %}

  {% if call.is_a?(Call) %}
    ->(
      {% for arg, i in call.args %}
        __arg{{i}} : typeof({{arg.is_a?(Splat) ? arg.exp : arg}}),
      {% end %}
      {% if call.named_args %}
        {% for narg, i in call.named_args %}
          __narg{{i}} : typeof({{narg.value}}),
        {% end %}
      {% end %}
      ) {
      spawn(name: {{name}}, same_thread: {{same_thread}}) do
        {% if call.receiver %}{{ call.receiver }}.{% end %}{{call.name}}(
          {% for arg, i in call.args %}
            {% if arg.is_a?(Splat) %}*{% end %}__arg{{i}},
          {% end %}
          {% if call.named_args %}
            {% for narg, i in call.named_args %}
              {{narg.name}}: __narg{{i}},
            {% end %}
          {% end %}
        )
      end
      }.call(
        {% for arg in call.args %}
          {{arg.is_a?(Splat) ? arg.exp : arg}},
        {% end %}
        {% if call.named_args %}
          {{call.named_args.map(&.value).splat}}
        {% end %}
      )
  {% else %}
    spawn do
      {{call}}
    end
  {% end %}
end
