# TODO: method to get midi note number
# TODO: validation: only accept note parameters which result in a valid MIDI note
# TODO: method to get vextab representation
# TODO: comparison between two notes: e.g., note1 < note2 = true/false

from .interval import Interval

class Note(object):
    """A single note, defined by a pitch, octave, and (optional) accidentals."""

    VALID_PITCHES = ('C', 'D', 'E', 'F', 'G', 'A', 'B')
    """List of valid pitch characters."""

    VALID_ACCIDENTALS = ('#', '##', 'b', 'bb')
    """List of valid accidental representors."""

    def __init__(self, pitch, octave, accidental=None):
        """Create a new Note.

        Args:
            pitch : str
                The pitch of the note. Should be one of :attr:`~music_essentials.note.Note.VALID_PITCHES`, but can
                be upper or lower case.
            octave : int
                The octave of the note. Should be in the range [-1, 9].

        Kwags:
            accidental : str (default None)
                The accidental to apply to the note. Should be one of :attr:`~music_essentials.note.Note.VALID_ACCIDENTALS`.

        Returns:
            Note
                A new note with the given pitch, octave, and accidental.

        Raises:
            ValueError
                If an invalid pitch, octave, or accidental is provided.
        
        Examples:
            >>> n = Note('A', 4, '##')
            >>> print(n)
            A4##
            >>> n = Note('d', 7)
            >>> print(n)
            D7
            >>> n = Note('x', 6)
            ValueError: Invalid pitch: x
        """
        if not isinstance(pitch, str):
            raise ValueError('Expected string for pitch, got: ' + str(pitch))
        if pitch.upper() not in Note.VALID_PITCHES:
            raise ValueError('Invalid pitch: ' + str(pitch))

        try:
            int(octave) # test if octave value is a number
        except:
            raise ValueError('Expected integer for octave, got: ' + str(octave))
        if '.' in str(octave): # check that the number doesn't have a decimal place
            raise ValueError('Expected integer for octave, got ' + str(octave))
        if (int(octave) < -1) or (int(octave) > 9):
            raise ValueError('Octave needs to be in the range [-1, 9], got: ' + str(octave))

        if accidental is not None:
            if accidental.lower() not in Note.VALID_ACCIDENTALS:
                raise ValueError('Invalid accidental: ' + str(accidental))

        self.pitch = pitch.upper()
        self.octave = int(octave)
        self.accidental = accidental

        if accidental is not None:
            self.accidental = self.accidental.lower()

    @classmethod
    def from_note_string(cls, note_string):
        """Create a new Note.

        Processes the note string then uses the constructor :attr:`~music_essentials.note.Note.__init__()`

        Args:
            note_string : str
                A string representing the note to create. Should be in the form:
                    ``<pitch><octave><accidental>``
                
                The pitch of the note should be one of :attr:`~music_essentials.note.Note.VALID_PITCHES`, but can
                be upper or lower case.

                The octave of the note should be in the range ``[-1, 9]``.

                The accidental is optional, but if used should be one of :attr:`~music_essentials.note.Note.VALID_ACCIDENTALS`.

        Returns:
            Note
                A new note with the given pitch, octave, and accidental.

        Raises:
            ValueError
                If an invalid pitch, octave, or accidental is provided.
        
        Examples:
            >>> n = Note.from_note_string('A4##')
            >>> print(n)
            A4##
            >>> n = Note.from_note_string('d7')
            >>> print(n)
            D7
            >>> n = Note.from_note_string('x6')
            ValueError: Invalid pitch: x
        """
        pitch = note_string[0]
        octave = note_string[1]
        accidental = note_string[2:]

        if len(accidental) == 0:
            accidental = None

        return cls(pitch, octave, accidental)

    def __add__(self, other):
        if not isinstance(other, Interval):
            raise TypeError('unsupported operand type(s) for +: \'Note\' and \'' + str(other.__class__.__name__) + '\'')

        raise NotImplementedError('Working on it!')

    def __str__(self):
        """Create a string representation of the note in the form ``<pitch><octave><accidental>``.

        Can be used as a note string argument for :attr:`~music_essentials.note.Note.from_note_string()`.
        
        Examples:
            >>> n = Note('B', 9, '#')
            >>> print(n)
            B9#
            >>> n = Note('g', 7)
            >>> print(n)
            G7
            >>> n = Note('D', 3, 'B')
            >>> print(n)
            D3b
        """
        s = self.pitch + str(self.octave)
        if self.accidental is not None:
            s += self.accidental
        
        return s


if __name__ == '__main__':
    n = Note('A', 4, accidental='b')
    print(n)

    n = Note.from_note_string('A4#')
    print(n)
@@ -235,17 +235,16 @@
 e%0A%0Afrom 
-.
 interval
@@ -4392,51 +4392,355 @@
     
-raise NotImplementedError('Working on it!')
+print('Adding ' + str(self) + ' and ' + str(other))%0A        %0A        note_pitch_idx = Note.VALID_PITCHES.index(self.pitch)%0A        pitch_diff = (other.size %25 7) - 1%0A        if (note_pitch_idx + pitch_diff) %3E (len(Note.VALID_PITCHES) - 1):%0A            pitch_diff = -7 + pitch_diff%0A        new_pitch = Note.VALID_PITCHES%5Bnote_pitch_idx + pitch_diff%5D
 %0A%0A  
@@ -5422,69 +5422,62 @@
 Note
-('A', 4, accidental='b')%0A    print(n
+.from_note_string('F4'
 )%0A
-%0A
     
-n = Note.from_note
+i = Interval.from_interval
 _str
@@ -5485,14 +5485,14 @@
 ng('
-A4#
+M7
 ')%0A
+%0A
     
@@ -5493,14 +5493,36 @@
 %0A%0A    print(
-n
+'Result: ' + str(n + i)
 )

Add calculation of new pitch to pitch + interval addition.