# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""Defines sequence of notes objects for creating datasets.
"""

import collections
import copy
from magenta.protobuf import music_pb2

# Set the quantization cutoff.
# Note events before this cutoff are rounded down to nearest step. Notes
# above this cutoff are rounded up to nearest step. The cutoff is given as a
# fraction of a step.
# For example, with quantize_cutoff = 0.75 using 0-based indexing,
# if .75 < event <= 1.75, it will be quantized to step 1.
# If 1.75 < event <= 2.75 it will be quantized to step 2.
# A number close to 1.0 gives less wiggle room for notes that start early,
# and they will be snapped to the previous step.
QUANTIZE_CUTOFF = 0.5

# Shortcut to chord symbol text annotation type.
CHORD_SYMBOL = music_pb2.NoteSequence.TextAnnotation.CHORD_SYMBOL


class BadTimeSignatureException(Exception):
  pass


class MultipleTimeSignatureException(Exception):
  pass


class NegativeTimeException(Exception):
  pass


def is_power_of_2(x):
  return x and not x & (x - 1)


Note = collections.namedtuple(
    'Note', ['pitch', 'velocity', 'start', 'end', 'instrument', 'program'])
TimeSignature = collections.namedtuple('TimeSignature',
                                       ['numerator', 'denominator'])
ChordSymbol = collections.namedtuple('ChordSymbol', ['step', 'figure'])


class QuantizedSequence(object):
  """Holds notes and chords which have been quantized to time steps.

  Notes contain a pitch, velocity, start time, and end time. Notes
  are stored in tracks (which can be different instruments or the same
  instrument). There is also a time signature and key signature.

  Attributes:
    tracks: A dictionary mapping track number to list of Note tuples. Track
        number is taken from the instrument number of each NoteSequence note.
    chords: A list of ChordSymbol tuples.
    qpm: Quarters per minute. This is needed to recover tempo if converting back
        to MIDI.
    time_signature: This determines the length of a bar of music. This is just
        needed to compute the number of quantization steps per bar, though it
        can also communicate more high level aspects of the music
        (see https://en.wikipedia.org/wiki/Time_signature).
    steps_per_quarter: How many quantization steps per quarter note of music.
  """

  def __init__(self):
    self._reset()

  def _reset(self):
    self.tracks = {}
    self.chords = []
    self.qpm = 120.0
    self.time_signature = TimeSignature(4, 4)  # numerator, denominator
    self.steps_per_quarter = 4

  def steps_per_bar(self):
    """Calculates steps per bar.

    Returns:
      Steps per bar as a floating point number.
    """
    quarters_per_beat = 4.0 / self.time_signature.denominator
    quarters_per_bar = (quarters_per_beat * self.time_signature.numerator)
    steps_per_bar_float = (self.steps_per_quarter * quarters_per_bar)
    return steps_per_bar_float

  def from_note_sequence(self, note_sequence, steps_per_quarter):
    """Populate self with a music_pb2.NoteSequence proto.

    Notes and time signature are saved to self with notes' start and end times
    quantized. If there is no time signature 4/4 is assumed. If there is more
    than one time signature an exception is raised.

    The quarter notes per minute stored in `note_sequence` is used to normalize
    tempo. Regardless of how fast or slow quarter notes are played, a note that
    is played for 1 quarter note will last `steps_per_quarter` time steps in
    the quantized result.

    A note's start and end time are snapped to a nearby quantized step. See
    the comments above `QUANTIZE_CUTOFF` for details.
    Args:
      note_sequence: A music_pb2.NoteSequence protocol buffer.
      steps_per_quarter: Each quarter note of music will be divided into this
          many quantized time steps.

    Raises:
      MultipleTimeSignatureException: If there is a change in time signature
          in `note_sequence`.
      BadTimeSignatureException: If the time signature found in `note_sequence`
          has a denominator which is not a power of 2.
      NegativeTimeException: If a note or chord occurs at a negative time.
    """
    self._reset()

    self.steps_per_quarter = steps_per_quarter

    if note_sequence.time_signatures:
      self.time_signature = TimeSignature(
          note_sequence.time_signatures[0].numerator,
          note_sequence.time_signatures[0].denominator)
    for time_signature in note_sequence.time_signatures[1:]:
      if (time_signature.numerator != self.time_signature.numerator or
          time_signature.denominator != self.time_signature.denominator):
        raise MultipleTimeSignatureException(
            'NoteSequence has at least one time signature change.')

    if not is_power_of_2(self.time_signature.denominator):
      raise BadTimeSignatureException(
          'Denominator is not a power of 2. Time signature: %d/%d' %
          (self.time_signature.numerator, self.time_signature.denominator))

    self.qpm = note_sequence.tempos[0].qpm if note_sequence.tempos else 120.0

    # Compute quantization steps per second.
    steps_per_second = steps_per_quarter * self.qpm / 60.0

    quantize = lambda x: int(x + (1 - QUANTIZE_CUTOFF))

    for note in note_sequence.notes:
      # Quantize the start and end times of the note.
      start_step = quantize(note.start_time * steps_per_second)
      end_step = quantize(note.end_time * steps_per_second)
      if end_step == start_step:
        end_step += 1

      # Do not allow notes to start or end in negative time.
      if start_step < 0 or end_step < 0:
        raise NegativeTimeException(
            'Got negative note time: start_step = %s, end_step = %s' %
            (start_step, end_step))

      if note.instrument not in self.tracks:
        self.tracks[note.instrument] = []
      self.tracks[note.instrument].append(Note(pitch=note.pitch,
                                               velocity=note.velocity,
                                               start=start_step,
                                               end=end_step,
                                               instrument=note.instrument,
                                               program=note.program))

    # Also add chord symbol annotations to the quantized sequence.
    for annotation in note_sequence.text_annotations:
      if annotation.annotation_type == CHORD_SYMBOL:
        # Quantize the chord time, disallowing negative time.
        step = quantize(annotation.time * steps_per_second)
        if step < 0:
          raise NegativeTimeException(
              'Got negative chord time: step = %s' % step)
        self.chords.append(ChordSymbol(step=step, figure=annotation.text))

  def __eq__(self, other):
    if not isinstance(other, QuantizedSequence):
      return False
    for track in self.tracks:
      if (track not in other.tracks or
          set(self.tracks[track]) != set(other.tracks[track])):
        return False
    return (
        self.qpm == other.qpm and
        self.time_signature == other.time_signature and
        self.steps_per_quarter == other.steps_per_quarter and
        set(self.chords) == set(other.chords))

  def __deepcopy__(self, unused_memo=None):
    new_copy = type(self)()
    new_copy.tracks = copy.deepcopy(self.tracks)
    new_copy.chords = copy.deepcopy(self.chords)
    new_copy.qpm = self.qpm
    new_copy.time_signature = self.time_signature
    new_copy.steps_per_quarter = self.steps_per_quarter
    return new_copy
