# 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 drum tracks and models inputs/outputs."""

from note_seq import encoder_decoder

# Default list of 9 drum types, where each type is represented by a list of
# MIDI pitches for drum sounds belonging to that type. This default list
# attempts to map all GM1 and GM2 drums onto a much smaller standard drum kit
# based on drum sound and function.
DEFAULT_DRUM_TYPE_PITCHES = [
    # kick drum
    [36, 35],

    # snare drum
    [38, 27, 28, 31, 32, 33, 34, 37, 39, 40, 56, 65, 66, 75, 85],

    # closed hi-hat
    [42, 44, 54, 68, 69, 70, 71, 73, 78, 80, 22],

    # open hi-hat
    [46, 67, 72, 74, 79, 81, 26],

    # low tom
    [45, 29, 41, 43, 61, 64, 84],

    # mid tom
    [48, 47, 60, 63, 77, 86, 87],

    # high tom
    [50, 30, 62, 76, 83],

    # crash cymbal
    [49, 52, 55, 57, 58],

    # ride cymbal
    [51, 53, 59, 82]
]


class DrumsEncodingError(Exception):
  pass


class MultiDrumOneHotEncoding(encoder_decoder.OneHotEncoding):
  """Encodes drum events as binary where each bit is a different drum type.

  Each event consists of multiple simultaneous drum "pitches". This encoding
  converts each pitch to a drum type, e.g. bass drum, hi-hat, etc. Each drum
  type is mapped to a single bit of a binary integer representation, where the
  bit has value 0 if the drum type is not present, and 1 if it is present.

  If multiple "pitches" corresponding to the same drum type (e.g. two different
  ride cymbals) are present, the encoding is the same as if only one of them
  were present.
  """

  def __init__(self, drum_type_pitches=None, ignore_unknown_drums=True):
    """Initializes the MultiDrumOneHotEncoding.

    Args:
      drum_type_pitches: A Python list of the MIDI pitch values for each drum
          type. If None, `DEFAULT_DRUM_TYPE_PITCHES` will be used.
      ignore_unknown_drums: If True, unknown drum pitches will not be encoded.
          If False, a DrumsEncodingError will be raised when unknown drum
          pitches are encountered.
    """
    if drum_type_pitches is None:
      drum_type_pitches = DEFAULT_DRUM_TYPE_PITCHES
    self._drum_map = dict(enumerate(drum_type_pitches))
    self._inverse_drum_map = dict((pitch, index)
                                  for index, pitches in self._drum_map.items()
                                  for pitch in pitches)
    self._ignore_unknown_drums = ignore_unknown_drums

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

  @property
  def default_event(self):
    return frozenset()

  def encode_event(self, event):
    drum_type_indices = set()
    for pitch in event:
      if pitch in self._inverse_drum_map:
        drum_type_indices.add(self._inverse_drum_map[pitch])
      elif not self._ignore_unknown_drums:
        raise DrumsEncodingError('unknown drum pitch: %d' % pitch)
    return sum(2 ** i for i in drum_type_indices)

  def decode_event(self, index):
    bits = reversed(str(bin(index)))
    # Use the first "pitch" for each drum type.
    return frozenset(self._drum_map[i][0]
                     for i, b in enumerate(bits)
                     if b == '1')
