# Copyright 2023 The Magenta 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.

"""Classes for converting between pianoroll input and model input/output."""

from note_seq import encoder_decoder
import numpy as np


class PianorollEncoderDecoder(encoder_decoder.EventSequenceEncoderDecoder):
  """An EventSequenceEncoderDecoder that produces a pianoroll encoding.

  Inputs are binary arrays with active pitches (with some offset) at each step
  set to 1 and inactive pitches set to 0.

  Events are PianorollSequence events, which are tuples of active pitches
  (with some offset) at each step.
  """

  def __init__(self, input_size=88):
    """Initialize a PianorollEncoderDecoder object.

    Args:
      input_size: The size of the input vector.
    """
    self._input_size = input_size

  @property
  def input_size(self):
    return self._input_size

  @property
  def num_classes(self):
    return 2 ** self.input_size

  @property
  def default_event_label(self):
    return 0

  def _event_to_label(self, event):
    label = 0
    for pitch in event:
      label += 2**pitch
    return label

  def _event_to_input(self, event):
    input_ = np.zeros(self.input_size, np.float32)
    input_[list(event)] = 1
    return input_

  def events_to_input(self, events, position):
    """Returns the input vector for the given position in the event sequence.

    Args:
      events: A list-like sequence of PianorollSequence events.
      position: An integer event position in the event sequence.

    Returns:
      An input vector, a list of floats.
    """
    return self._event_to_input(events[position])

  def events_to_label(self, events, position):
    """Returns the label for the given position in the event sequence.

    Args:
      events: A list-like sequence of PianorollSequence events.
      position: An integer event position in the event sequence.

    Returns:
      A label, an integer.
    """
    return self._event_to_label(events[position])

  def class_index_to_event(self, class_index, events):
    """Returns the event for the given class index.

    This is the reverse process of the self.events_to_label method.

    Args:
      class_index: An integer in the range [0, self.num_classes).
      events: A list-like sequence of events. This object is not used in this
          implementation.

    Returns:
      An PianorollSequence event value.
    """
    assert class_index < self.num_classes
    event = []
    for i in range(self.input_size):
      if class_index % 2:
        event.append(i)
      class_index >>= 1
    assert class_index == 0
    return tuple(event)

  def extend_event_sequences(self, event_sequences, softmax):
    """Extends the event sequences by adding the new samples.

    Args:
      event_sequences: A collection of PianorollSequences to append `samples`
         to.
      softmax: A collection of binary arrays with active pitches set to 1 and
         inactive pitches set to 0, which will be added to the corresponding
         `pianoroll_seqs`.
    Raises:
      ValueError: if inputs are not of equal length.
    """
    pianoroll_seqs = event_sequences
    samples = softmax
    if len(pianoroll_seqs) != len(samples):
      raise ValueError(
          '`pianoroll_seqs` and `samples` must have equal lengths.')
    for pianoroll_seq, sample in zip(pianoroll_seqs, samples):
      event = tuple(np.where(sample)[0])
      pianoroll_seq.append(event)
