# Copyright 2022 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 chord progressions and models inputs/outputs.

MajorMinorChordOneHotEncoding is an encoding.OneHotEncoding that specifies a
one-hot encoding for ChordProgression events, i.e. chord symbol strings. This
encoding has 25 classes, all 12 major and minor triads plus "no chord".

TriadChordOneHotEncoding is another encoding.OneHotEncoding that specifies a
one-hot encoding for ChordProgression events, i.e. chord symbol strings. This
encoding has 49 classes, all 12 major/minor/augmented/diminished triads plus
"no chord".
"""

from note_seq import chord_symbols_lib
from note_seq import constants
from note_seq import encoder_decoder

NOTES_PER_OCTAVE = constants.NOTES_PER_OCTAVE
NO_CHORD = constants.NO_CHORD

# Mapping from pitch class index to name.
_PITCH_CLASS_MAPPING = ['C', 'C#', 'D', 'Eb', 'E', 'F',
                        'F#', 'G', 'Ab', 'A', 'Bb', 'B']


class ChordEncodingError(Exception):
  pass


class MajorMinorChordOneHotEncoding(encoder_decoder.OneHotEncoding):
  """Encodes chords as root + major/minor, with zero index for "no chord".

  Encodes chords as follows:
    0:     "no chord"
    1-12:  chords with a major triad, where 1 is C major, 2 is C# major, etc.
    13-24: chords with a minor triad, where 13 is C minor, 14 is C# minor, etc.
  """

  @property
  def num_classes(self):
    return 2 * NOTES_PER_OCTAVE + 1

  @property
  def default_event(self):
    return NO_CHORD

  def encode_event(self, event):
    if event == NO_CHORD:
      return 0

    root = chord_symbols_lib.chord_symbol_root(event)
    quality = chord_symbols_lib.chord_symbol_quality(event)

    if quality == chord_symbols_lib.CHORD_QUALITY_MAJOR:
      return root + 1
    elif quality == chord_symbols_lib.CHORD_QUALITY_MINOR:
      return root + NOTES_PER_OCTAVE + 1
    else:
      raise ChordEncodingError('chord is neither major nor minor: %s' % event)

  def decode_event(self, index):
    if index == 0:
      return NO_CHORD
    elif index - 1 < 12:
      # major
      return _PITCH_CLASS_MAPPING[index - 1]
    else:
      # minor
      return _PITCH_CLASS_MAPPING[index - NOTES_PER_OCTAVE - 1] + 'm'


class TriadChordOneHotEncoding(encoder_decoder.OneHotEncoding):
  """Encodes chords as root + triad type, with zero index for "no chord".

  Encodes chords as follows:
    0:     "no chord"
    1-12:  chords with a major triad, where 1 is C major, 2 is C# major, etc.
    13-24: chords with a minor triad, where 13 is C minor, 14 is C# minor, etc.
    25-36: chords with an augmented triad, where 25 is C augmented, etc.
    37-48: chords with a diminished triad, where 37 is C diminished, etc.
  """

  @property
  def num_classes(self):
    return 4 * NOTES_PER_OCTAVE + 1

  @property
  def default_event(self):
    return NO_CHORD

  def encode_event(self, event):
    if event == NO_CHORD:
      return 0

    root = chord_symbols_lib.chord_symbol_root(event)
    quality = chord_symbols_lib.chord_symbol_quality(event)

    if quality == chord_symbols_lib.CHORD_QUALITY_MAJOR:
      return root + 1
    elif quality == chord_symbols_lib.CHORD_QUALITY_MINOR:
      return root + NOTES_PER_OCTAVE + 1
    elif quality == chord_symbols_lib.CHORD_QUALITY_AUGMENTED:
      return root + 2 * NOTES_PER_OCTAVE + 1
    elif quality == chord_symbols_lib.CHORD_QUALITY_DIMINISHED:
      return root + 3 * NOTES_PER_OCTAVE + 1
    else:
      raise ChordEncodingError('chord is not a standard triad: %s' % event)

  def decode_event(self, index):
    if index == 0:
      return NO_CHORD
    elif index - 1 < 12:
      # major
      return _PITCH_CLASS_MAPPING[index - 1]
    elif index - NOTES_PER_OCTAVE - 1 < 12:
      # minor
      return _PITCH_CLASS_MAPPING[index - NOTES_PER_OCTAVE - 1] + 'm'
    elif index - 2 * NOTES_PER_OCTAVE - 1 < 12:
      # augmented
      return _PITCH_CLASS_MAPPING[index - 2 * NOTES_PER_OCTAVE - 1] + 'aug'
    else:
      # diminished
      return _PITCH_CLASS_MAPPING[index - 3 * NOTES_PER_OCTAVE - 1] + 'dim'


class PitchChordsEncoderDecoder(encoder_decoder.EventSequenceEncoderDecoder):
  """An encoder/decoder for chords that encodes chord root, pitches, and bass.

  This class has no label encoding and can only be used to encode chords as
  model input vectors. It can be used to help generate another type of event
  sequence (e.g. melody) conditioned on chords.
  """

  @property
  def input_size(self):
    return 3 * NOTES_PER_OCTAVE + 1

  @property
  def num_classes(self):
    raise NotImplementedError

  @property
  def default_event_label(self):
    raise NotImplementedError

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

    Indices [0, 36]:
    [0]: Whether or not this chord is "no chord".
    [1, 12]: A one-hot encoding of the chord root pitch class.
    [13, 24]: Whether or not each pitch class is present in the chord.
    [25, 36]: A one-hot encoding of the chord bass pitch class.

    Args:
      events: A note_seq.ChordProgression object.
      position: An integer event position in the chord progression.

    Returns:
      An input vector, an self.input_size length list of floats.
    """
    chord = events[position]
    input_ = [0.0] * self.input_size

    if chord == NO_CHORD:
      input_[0] = 1.0
      return input_

    root = chord_symbols_lib.chord_symbol_root(chord)
    input_[1 + root] = 1.0

    pitches = chord_symbols_lib.chord_symbol_pitches(chord)
    for pitch in pitches:
      input_[1 + NOTES_PER_OCTAVE + pitch] = 1.0

    bass = chord_symbols_lib.chord_symbol_bass(chord)
    input_[1 + 2 * NOTES_PER_OCTAVE + bass] = 1.0

    return input_

  def events_to_label(self, events, position):
    raise NotImplementedError

  def class_index_to_event(self, class_index, events):
    raise NotImplementedError
