///|
pub(all) enum TransformationKind {
  Transpose
  Invert
  Retrograde
  Stretch
  Compress
  Rotate
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Transformation {
  kind : TransformationKind
  amount : Int
  description : String
} derive(Debug, Eq, ToJson)

///|
pub fn transformation_name(kind : TransformationKind) -> String {
  match kind {
    Transpose => "transpose"
    Invert => "invert"
    Retrograde => "retrograde"
    Stretch => "stretch"
    Compress => "compress"
    Rotate => "rotate"
  }
}

///|
pub fn transpose_sequence(notes : Array[Note], semitones : Int) -> Array[Note] {
  transpose(notes, semitones)
}

///|
pub fn invert_sequence(notes : Array[Note], axis : Int) -> Array[Note] {
  let result : Array[Note] = []
  for note in notes {
    result.push({
      ..note,
      midi: axis * 2 - note.midi,
      label: pitch_label(axis * 2 - note.midi),
    })
  }
  result
}

///|
pub fn retrograde_sequence(notes : Array[Note]) -> Array[Note] {
  let result : Array[Note] = []
  let total = sequence_end(notes)
  for i in 0.. Array[Note] {
  let result : Array[Note] = []
  if notes.length() == 0 {
    return result
  }
  let offset = (amount % notes.length() + notes.length()) % notes.length()
  for i in 0.. Array[Note] {
  scale_time(notes, if factor <= 0 { 1 } else { factor })
}

///|
pub fn compress_sequence(notes : Array[Note], factor : Int) -> Array[Note] {
  if factor <= 0 {
    return notes
  }
  let result : Array[Note] = []
  for note in notes {
    result.push({
      ..note,
      onset: note.onset / factor,
      duration: if note.duration / factor <= 0 {
        1
      } else {
        note.duration / factor
      },
    })
  }
  result
}

///|
pub fn apply_transformation(
  notes : Array[Note],
  transformation : Transformation,
) -> Array[Note] {
  match transformation.kind {
    Transpose => transpose_sequence(notes, transformation.amount)
    Invert => invert_sequence(notes, transformation.amount)
    Retrograde => retrograde_sequence(notes)
    Stretch => stretch_sequence(notes, transformation.amount)
    Compress => compress_sequence(notes, transformation.amount)
    Rotate => rotate_sequence(notes, transformation.amount)
  }
}

///|
pub fn transformation_description(value : Transformation) -> String {
  "\{transformation_name(value.kind)}(\{value.amount}): \{value.description}"
}

///|
pub fn transformation_round_trip(notes : Array[Note], semitones : Int) -> Bool {
  same_sequence(
    notes,
    transpose_sequence(transpose_sequence(notes, semitones), -semitones),
  )
}

///|
pub fn contour_preserved_after_transpose(
  notes : Array[Note],
  semitones : Int,
) -> Bool {
  compare_contours(notes, transpose_sequence(notes, semitones)) == 0
}

///|
pub fn interval_profile(notes : Array[Note]) -> Array[Int] {
  let result : Array[Int] = []
  if notes.length() < 2 {
    return result
  }
  for i in 1.. Bool {
  interval_profile(left) == interval_profile(right)
}

///|
pub fn transformation_catalog() -> Array[Transformation] {
  [
    {
      kind: Transpose,
      amount: 0,
      description: "move the phrase without changing intervals",
    },
    {
      kind: Invert,
      amount: 60,
      description: "reflect pitches around a register axis",
    },
    {
      kind: Retrograde,
      amount: 0,
      description: "reverse event order while preserving durations",
    },
    {
      kind: Stretch,
      amount: 2,
      description: "double event times for a slower exercise",
    },
    {
      kind: Compress,
      amount: 2,
      description: "halve event times for a faster exercise",
    },
    { kind: Rotate, amount: 1, description: "start from the next event" },
  ]
}

///|
pub fn transformation_examples(notes : Array[Note]) -> Array[String] {
  let result : Array[String] = []
  for transformation in transformation_catalog() {
    let output = apply_transformation(notes, transformation)
    result.push(
      "\{transformation_name(transformation.kind)}:\{sequence_fingerprint(output)}",
    )
  }
  result
}

///|
pub fn transformation_is_safe(transformation : Transformation) -> Bool {
  match transformation.kind {
    Transpose => transformation.amount >= -127 && transformation.amount <= 127
    Invert => transformation.amount >= 0 && transformation.amount <= 127
    Retrograde => true
    Stretch | Compress => transformation.amount > 0
    Rotate => true
  }
}

///|
pub fn safe_transformations() -> Array[Transformation] {
  let result : Array[Transformation] = []
  for transformation in transformation_catalog() {
    if transformation_is_safe(transformation) {
      result.push(transformation)
    }
  }
  result
}

///|
pub fn transformation_count() -> Int {
  safe_transformations().length()
}