defmodule Raxol.Animation.DSL do
  @moduledoc """
  Declarative animation DSL for composing complex animation sequences.

  This module provides a fluent, chainable API for creating sophisticated animations
  with timing controls, parallel execution, and conditional logic.

  ## Examples

      # Simple animation sequence
      import Raxol.Animation.DSL

      sequence()
      |> fade_in(duration: 300)
      |> slide_up(distance: 20, duration: 400)
      |> scale_bounce(scale: 1.2, duration: 200)
      |> execute(element_id)

      # Parallel animations
      parallel()
      |> with_animation(fade_in(duration: 500))
      |> with_animation(rotate(degrees: 360, duration: 1000))
      |> execute(element_id)

      # Conditional animations
      sequence()
      |> fade_in(duration: 300)
      |> when_condition(fn state -> state.user_type == :premium end)
      |> sparkle_effect(duration: 1000)
      |> end_condition()
      |> slide_out(direction: :right, duration: 400)
      |> execute(element_id)

      # Complex choreographed animation
      choreography()
      |> stage("entrance",
           sequence()
           |> fade_in(duration: 300)
           |> slide_up(distance: 30, duration: 500, easing: :ease_out_back)
         )
      |> stage("highlight",
           parallel()
           |> with_animation(glow_effect(duration: 800))
           |> with_animation(pulse_scale(scale: 1.05, duration: 600))
         )
      |> stage("exit",
           sequence()
           |> fade_out(duration: 400)
           |> scale_down(scale: 0.8, duration: 300)
         )
      |> execute_choreography(element_id)
  """

  alias Raxol.Animation.DSL.{Compiler, Transforms}
  alias Raxol.Animation.Framework

  @compile {:no_warn_undefined, Raxol.Animation.DSL.Compiler}
  @compile {:no_warn_undefined, Raxol.Animation.DSL.Transforms}

  # Animation sequence structure

  defmodule Sequence do
    @moduledoc """
    Represents an animation sequence with type, animations, options, conditions and context.
    """

    defstruct [:type, :animations, :options, :conditions, :context]

    def new(type \\ :sequence, options \\ %{}) do
      %__MODULE__{
        type: type,
        animations: [],
        options: options,
        conditions: [],
        context: %{}
      }
    end

    def add_animation(sequence, animation) do
      %{sequence | animations: [animation | sequence.animations]}
    end

    def add_condition(sequence, condition) do
      %{sequence | conditions: [condition | sequence.conditions]}
    end
  end

  defmodule Animation do
    @moduledoc """
    Defines an animation with name, type, parameters, conditions and metadata.
    """

    defstruct [:name, :type, :params, :conditions, :meta]

    def new(name, type, params \\ %{}) do
      %__MODULE__{
        name: name,
        type: type,
        params: params,
        conditions: [],
        meta: %{}
      }
    end
  end

  defmodule Choreography do
    @moduledoc """
    Manages complex multi-stage animations with stages, options and context.
    """

    defstruct [:stages, :options, :context]

    def new(options \\ %{}) do
      %__MODULE__{stages: [], options: options, context: %{}}
    end

    def add_stage(choreo, name, sequence) do
      %{choreo | stages: [{name, sequence} | choreo.stages]}
    end
  end

  # DSL entry points

  @doc "Creates a new animation sequence."
  def sequence(options \\ %{}), do: Sequence.new(:sequence, options)

  @doc "Creates a parallel animation group."
  def parallel(options \\ %{}), do: Sequence.new(:parallel, options)

  @doc "Creates a new choreography for complex multi-stage animations."
  def choreography(options \\ %{}), do: Choreography.new(options)

  # Basic animation primitives

  @doc "Adds a fade in animation to the sequence."
  def fade_in(sequence_or_options \\ %{})

  def fade_in(%Sequence{} = sequence), do: fade_in(sequence, %{})

  def fade_in(options) when is_map(options), do: sequence() |> fade_in(options)

  def fade_in(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :fade_in,
        :opacity,
        Map.merge(
          %{from: 0.0, to: 1.0, duration: 300, easing: :ease_out},
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a fade out animation to the sequence."
  def fade_out(sequence_or_options \\ %{})

  def fade_out(%Sequence{} = sequence), do: fade_out(sequence, %{})

  def fade_out(options) when is_map(options),
    do: sequence() |> fade_out(options)

  def fade_out(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :fade_out,
        :opacity,
        Map.merge(
          %{from: 1.0, to: 0.0, duration: 300, easing: :ease_in},
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a slide animation to the sequence."
  def slide(sequence_or_options \\ %{})

  def slide(%Sequence{} = sequence), do: slide(sequence, %{})

  def slide(options) when is_map(options), do: sequence() |> slide(options)

  def slide(%Sequence{} = sequence, options) do
    direction = Map.get(options, :direction, :up)
    distance = Map.get(options, :distance, 20)

    {from_transform, to_transform} =
      Transforms.calculate_slide_transform(direction, distance)

    animation =
      Animation.new(
        :slide,
        :transform,
        Map.merge(
          %{
            from: from_transform,
            to: to_transform,
            duration: 400,
            easing: :ease_out
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  # Convenience slide directions
  def slide_up(sequence, options \\ %{}),
    do: slide(sequence, Map.put(options, :direction, :up))

  def slide_down(sequence, options \\ %{}),
    do: slide(sequence, Map.put(options, :direction, :down))

  def slide_left(sequence, options \\ %{}),
    do: slide(sequence, Map.put(options, :direction, :left))

  def slide_right(sequence, options \\ %{}),
    do: slide(sequence, Map.put(options, :direction, :right))

  @doc "Adds a slide in animation."
  def slide_in(sequence_or_options \\ %{}, options \\ %{})

  def slide_in(%Sequence{} = sequence, options) do
    direction = Map.get(options, :direction, :up)
    distance = Map.get(options, :distance, 30)

    {from_transform, to_transform} =
      Transforms.calculate_slide_in_transform(direction, distance)

    animation =
      Animation.new(
        :slide_in,
        :transform,
        Map.merge(
          %{
            from: from_transform,
            to: to_transform,
            duration: 500,
            easing: :ease_out_back
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  def slide_in(options, _extra) when is_map(options),
    do: sequence() |> slide_in(options)

  @doc "Adds a slide out animation."
  def slide_out(sequence_or_options \\ %{}, options \\ %{})

  def slide_out(%Sequence{} = sequence, options) do
    direction = Map.get(options, :direction, :down)
    distance = Map.get(options, :distance, 30)

    {from_transform, to_transform} =
      Transforms.calculate_slide_out_transform(direction, distance)

    animation =
      Animation.new(
        :slide_out,
        :transform,
        Map.merge(
          %{
            from: from_transform,
            to: to_transform,
            duration: 400,
            easing: :ease_in
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  def slide_out(options, _extra) when is_map(options),
    do: sequence() |> slide_out(options)

  @doc "Adds a scale animation to the sequence."
  def scale(sequence_or_options \\ %{})

  def scale(%Sequence{} = sequence), do: scale(sequence, %{})

  def scale(options) when is_map(options), do: sequence() |> scale(options)

  def scale(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :scale,
        :scale,
        Map.merge(
          %{
            from: 1.0,
            to: Map.get(options, :scale, 1.2),
            duration: 300,
            easing: :ease_out
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a scale bounce animation."
  def scale_bounce(sequence_or_options \\ %{})

  def scale_bounce(%Sequence{} = sequence), do: scale_bounce(sequence, %{})

  def scale_bounce(options) when is_map(options),
    do: sequence() |> scale_bounce(options)

  def scale_bounce(%Sequence{} = sequence, options) do
    scale_to = Map.get(options, :scale, 1.2)
    half_dur = Map.get(options, :duration, 200) |> div(2)

    bounce_sequence =
      parallel(%{})
      |> with_animation(
        Animation.new(:scale_up, :scale, %{
          from: 1.0,
          to: scale_to,
          duration: half_dur,
          easing: :ease_out_bounce
        })
      )
      |> with_animation(
        Animation.new(:scale_down, :scale, %{
          from: scale_to,
          to: 1.0,
          duration: half_dur,
          easing: :ease_in_bounce,
          delay: half_dur
        })
      )

    Sequence.add_animation(sequence, bounce_sequence)
  end

  @doc "Adds a scale down animation."
  def scale_down(sequence_or_options \\ %{})

  def scale_down(%Sequence{} = sequence), do: scale_down(sequence, %{})

  def scale_down(options) when is_map(options),
    do: sequence() |> scale_down(options)

  def scale_down(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :scale_down,
        :scale,
        Map.merge(
          %{
            from: 1.0,
            to: Map.get(options, :scale, 0.8),
            duration: 300,
            easing: :ease_in
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a rotation animation."
  def rotate(sequence_or_options \\ %{})

  def rotate(%Sequence{} = sequence), do: rotate(sequence, %{})

  def rotate(options) when is_map(options), do: sequence() |> rotate(options)

  def rotate(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :rotate,
        :rotate,
        Map.merge(
          %{
            from: 0,
            to: Map.get(options, :degrees, 360),
            duration: 1000,
            easing: :ease_in_out
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  # Advanced effects

  @doc "Adds a glow effect animation."
  def glow_effect(sequence_or_options \\ %{})

  def glow_effect(%Sequence{} = sequence), do: glow_effect(sequence, %{})

  def glow_effect(options) when is_map(options),
    do: sequence() |> glow_effect(options)

  def glow_effect(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :glow,
        :glow,
        Map.merge(
          %{
            from: 0.0,
            to: 1.0,
            duration: 800,
            easing: :ease_in_out_sine,
            type: :glow_effect
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a pulse scale animation."
  def pulse_scale(sequence_or_options \\ %{})

  def pulse_scale(%Sequence{} = sequence), do: pulse_scale(sequence, %{})

  def pulse_scale(options) when is_map(options),
    do: sequence() |> pulse_scale(options)

  def pulse_scale(%Sequence{} = sequence, options) do
    scale_to = Map.get(options, :scale, 1.05)
    duration = Map.get(options, :duration, 600)

    pulse_animation =
      Animation.new(
        :pulse_scale,
        :scale,
        Map.merge(
          %{
            keyframes: %{
              "0%" => %{scale: 1.0},
              "50%" => %{scale: scale_to},
              "100%" => %{scale: 1.0}
            },
            duration: duration,
            easing: :ease_in_out_sine,
            type: :pulse
          },
          options
        )
      )

    Sequence.add_animation(sequence, pulse_animation)
  end

  @doc "Adds a sparkle effect animation."
  def sparkle_effect(sequence_or_options \\ %{})

  def sparkle_effect(%Sequence{} = sequence), do: sparkle_effect(sequence, %{})

  def sparkle_effect(options) when is_map(options),
    do: sequence() |> sparkle_effect(options)

  def sparkle_effect(%Sequence{} = sequence, options) do
    animation =
      Animation.new(
        :sparkle,
        :particle_system,
        Map.merge(
          %{
            particle_count: 12,
            spawn_area: %{x: 0, y: 0, width: 50, height: 30},
            velocity_range: %{min: 20, max: 80},
            duration: 1000,
            particle_lifetime: 800,
            type: :sparkle
          },
          options
        )
      )

    Sequence.add_animation(sequence, animation)
  end

  # Sequence composition and control

  @doc "Adds an animation to a parallel group."
  def with_animation(%Sequence{type: :parallel} = parallel_sequence, animation) do
    Sequence.add_animation(parallel_sequence, animation)
  end

  @doc "Adds a delay to the sequence."
  def delay(sequence, duration) do
    animation = Animation.new(:delay, :delay, %{duration: duration})
    Sequence.add_animation(sequence, animation)
  end

  @doc "Adds a conditional block to the sequence."
  def when_condition(%Sequence{} = sequence, condition_fn)
      when is_function(condition_fn, 1) do
    condition = %{type: :when, condition: condition_fn}
    Sequence.add_condition(sequence, condition)
  end

  @doc "Ends a conditional block."
  def end_condition(%Sequence{} = sequence) do
    condition = %{type: :end_when}
    Sequence.add_condition(sequence, condition)
  end

  @doc "Repeats the previous animation or sequence."
  def repeat(%Sequence{} = sequence, times) when is_integer(times) do
    repeat_animation = Animation.new(:repeat, :repeat, %{times: times})
    Sequence.add_animation(sequence, repeat_animation)
  end

  @doc "Loops the sequence infinitely or for a specified number of iterations."
  def loop(%Sequence{} = sequence, iterations \\ :infinite) do
    loop_animation = Animation.new(:loop, :loop, %{iterations: iterations})
    Sequence.add_animation(sequence, loop_animation)
  end

  # Choreography functions

  @doc "Adds a stage to a choreography."
  def stage(%Choreography{} = choreo, name, sequence) do
    Choreography.add_stage(choreo, name, sequence)
  end

  # Execution functions

  @doc "Executes the animation sequence for the given element."
  def execute(%Sequence{} = sequence, element_id, options \\ %{}) do
    compiled_animations = Compiler.compile_sequence(sequence)

    Enum.each(compiled_animations, fn {animation_name, animation_config} ->
      _ = Framework.create_animation(animation_name, animation_config)
      Framework.start_animation(animation_name, element_id, options)
    end)

    :ok
  end

  @doc "Executes a choreography with multiple stages."
  def execute_choreography(%Choreography{} = choreo, element_id, options \\ %{}) do
    _final_delay =
      Enum.reduce(choreo.stages, 0, fn {stage_name, sequence}, acc_delay ->
        stage_options = Map.put(options, :delay, acc_delay)
        execute(sequence, "#{element_id}_#{stage_name}", stage_options)
        acc_delay + Compiler.calculate_sequence_duration(sequence)
      end)

    :ok
  end
end
