#!/usr/bin/env python3

# Copyright (C) 2021 Sergio García-Cuevas González.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Behringer TD-3 pattern file decoder.
"""

NOTE = "."
TIE = "o"
REST = "-"

TONE_C = "C"
TONE_CSHARP = "C#"
TONE_D = "D"
TONE_DSHARP = "D#"
TONE_E = "E"
TONE_F = "F"
TONE_FSHARP = "F#"
TONE_G = "G"
TONE_GSHARP = "G#"
TONE_A = "A"
TONE_ASHARP = "A#"
TONE_B = "B"
TONE_CPLUS = "C+"

TRANSPOSE_DOWN = "D"
TRANSPOSE_UP = "U"
DONT_TRANSPOSE = ""

class Pattern:
    """
    TD-3 pattern.
    """

    __DATA_OFFSET = 36
    __PITCHES_OFFSET = 0
    __PITCHES_LENGTH = 16
    __ACCENTS_OFFSET = 16
    __ACCENTS_LENGTH = 16
    __SLIDES_OFFSET = 32
    __SLIDES_LENGTH = 16
    __LENGTH_OFFSET = 49
    __TRIPLETS_OFFSET = 48
    __NOTES_OFFSET = 51
    __RESTS_OFFSET = 53
    __ACCENT = "A"
    __SLIDE = "S"
    __ACCENT_AND_SLIDE = "AS"
    __NO_MODIFIERS = ""

    def __init__(self, pattern_file_data):
        """
        Create a new pattern object from binary contents of a pattern file.
        """
        data = self.__words_to_bytes(pattern_file_data[self.__DATA_OFFSET:])
        self.__length = data[self.__LENGTH_OFFSET]
        if data[self.__TRIPLETS_OFFSET] > 0:
            self.__triplets = True
        else:
            self.__triplets = False
        self.__steps = self.__decode_steps(data)
        pitches = iter(data[self.__PITCHES_OFFSET:self.__PITCHES_OFFSET+self.__PITCHES_LENGTH])
        accents = iter(data[self.__ACCENTS_OFFSET:self.__ACCENTS_OFFSET+self.__ACCENTS_LENGTH])
        slides = iter(data[self.__SLIDES_OFFSET:self.__SLIDES_OFFSET+self.__SLIDES_LENGTH])
        self.__pitches = []
        self.__transposes = []
        self.__accents = []
        self.__slides = []
        for step in self.__steps:
            if step == NOTE:
                pitch, transpose = self.__decode_pitch(next(pitches))
                self.__pitches.append(pitch)
                self.__transposes.append(transpose)
                self.__accents.append(next(accents) > 0)
                self.__slides.append(next(slides) > 0)
            else:
                self.__pitches.append(None)
                self.__transposes.append(None)
                self.__accents.append(None)
                self.__slides.append(None)

    def accents(self):
        """
        Return a list of accents:
        * True on accented NOTE steps;
        * False on unaccented NOTE steps;
        * None on TIE and REST steps.
        """
        return self.__accents

    def length(self):
        """
        Return the number of steps of self.
        """
        return self.__length

    def pitches(self):
        """
        Return a list of:
        * pitch (TONE_C, TONE_CSHARP, ..., TONE_CPLUS) on NOTE steps;
        * None on TIE and REST steps.
        """
        return self.__pitches

    def slides(self):
        """
        Return a list of slides:
        * True on NOTE steps with slide;
        * False on NOTE steps without slide;
        * None on REST and TIE steps.
        """
        return self.__slides

    def steps(self):
        """
        Return a list of step types:
        * NOTE;
        * TIE;
        * REST.
        """
        return self.__steps

    def transposes(self):
        """
        Return a list of:
        * TRANSPOSE_UP on NOTE steps transposed one octave up;
        * TRANSPOSE_DOWN on NOTE steps transposed one octave down;
        * NO_TRANSPOSE on NOTE steps not transposed;
        * None on TIE and REST steps.
        """
        return self.__transposes

    def triplets(self):
        """
        Return True if self is in triplets mode;
        otherwise return False.
        """
        return self.__triplets

    def __str__(self):
        """
        Return a string representation of self.
        """
        length_line = f"Length: {self.length()}"
        triplets_line = f"Triplets: {self.triplets()}"
        pitches_line = self.__format_row(self.pitches())
        transposes_line = self.__format_row(self.transposes())
        modifiers = {(True, True): self.__ACCENT_AND_SLIDE,
                     (True, False): self.__ACCENT,
                     (False, True): self.__SLIDE,
                     (False, False): self.__NO_MODIFIERS,
                     (None, None): None}
        modifiers_line = self.__format_row([modifiers[accent, slide]
                                           for accent, slide
                                           in zip(self.accents(),
                                                  self.slides())])
        steps_line = self.__format_row(self.steps())
        return "\n".join([length_line,
                          triplets_line,
                          pitches_line,
                          transposes_line,
                          modifiers_line,
                          steps_line])

    def __decode_pitch(self, value):
        """
        Return the tone and transposition modifier
        corresponding to a numeric value.
        """
        pitches = {0x0C: (TONE_C, TRANSPOSE_DOWN),
                   0x0D: (TONE_CSHARP, TRANSPOSE_DOWN),
                   0x0E: (TONE_D, TRANSPOSE_DOWN),
                   0x0F: (TONE_DSHARP, TRANSPOSE_DOWN),
                   0x10: (TONE_E, TRANSPOSE_DOWN),
                   0x11: (TONE_F, TRANSPOSE_DOWN),
                   0x12: (TONE_FSHARP, TRANSPOSE_DOWN),
                   0x13: (TONE_G, TRANSPOSE_DOWN),
                   0x14: (TONE_GSHARP, TRANSPOSE_DOWN),
                   0x15: (TONE_A, TRANSPOSE_DOWN),
                   0x16: (TONE_ASHARP, TRANSPOSE_DOWN),
                   0x17: (TONE_B, TRANSPOSE_DOWN),
                   0x18: (TONE_C, DONT_TRANSPOSE),
                   0x19: (TONE_CSHARP, DONT_TRANSPOSE),
                   0x1A: (TONE_D, DONT_TRANSPOSE),
                   0x1B: (TONE_DSHARP, DONT_TRANSPOSE),
                   0x1C: (TONE_E, DONT_TRANSPOSE),
                   0x1D: (TONE_F, DONT_TRANSPOSE),
                   0x1E: (TONE_FSHARP, DONT_TRANSPOSE),
                   0x1F: (TONE_G, DONT_TRANSPOSE),
                   0x20: (TONE_GSHARP, DONT_TRANSPOSE),
                   0x21: (TONE_A, DONT_TRANSPOSE),
                   0x22: (TONE_ASHARP, DONT_TRANSPOSE),
                   0x23: (TONE_B, DONT_TRANSPOSE),
                   0x24: (TONE_C, TRANSPOSE_UP),
                   0x25: (TONE_CSHARP, TRANSPOSE_UP),
                   0x26: (TONE_D, TRANSPOSE_UP),
                   0x27: (TONE_DSHARP, TRANSPOSE_UP),
                   0x28: (TONE_E, TRANSPOSE_UP),
                   0x29: (TONE_F, TRANSPOSE_UP),
                   0x2A: (TONE_FSHARP, TRANSPOSE_UP),
                   0x2B: (TONE_G, TRANSPOSE_UP),
                   0x2C: (TONE_GSHARP, TRANSPOSE_UP),
                   0x2D: (TONE_A, TRANSPOSE_UP),
                   0x2E: (TONE_ASHARP, TRANSPOSE_UP),
                   0x2F: (TONE_B, TRANSPOSE_UP),
                   0xB0: (TONE_CPLUS, TRANSPOSE_UP)}
        return pitches[value]

    def __decode_steps(self, data):
        """
        Return the time information of the pattern
        as a list of NOTE, TIE and REST values.
        """
        notes = data[self.__NOTES_OFFSET + 1] * 256 + data[self.__NOTES_OFFSET]
        rests = data[self.__RESTS_OFFSET + 1] * 256 + data[self.__RESTS_OFFSET]
        steps = []
        for bit in range(0, self.length()):
            if self.__read_bit(rests, bit):
                steps.append(REST)
            elif self.__read_bit(notes, bit):
                steps.append(NOTE)
            else:
                steps.append(TIE)
        return steps

    def __format_row(self, row):
        """
        Format a row.
        Used by __str__(self).
        """
        formatted_row = []
        if self.triplets():
            steps_per_division = 3
        else:
            steps_per_division = 4
        for step in range(0, len(row)):
            if (step % steps_per_division) == 0:
                formatted_row.append("|")
            if row[step] is None:
                formatted_row.append("  ")
            else:
                formatted_row.append("%-2s" %row[step])
        formatted_row.append("|")
        return "".join(formatted_row)

    def __read_bit(self, value, bit):
        """
        Read a bit from an unsigned integer value.
        """
        bit_value = value & (1 << bit)
        if bit_value > 0:
            return True
        else:
            return False

    def __words_to_bytes(self, data):
        """
        Convert 16-bit pattern data words to bytes.
        """
        high_nybbles = data[0::2]
        low_nybbles = data[1::2]
        return [high_nybble * 16 + low_nybble
                for high_nybble, low_nybble in zip(high_nybbles, low_nybbles)]

if __name__ == "__main__":
    import argparse
    argument_parser = argparse.ArgumentParser(description="TD-3 pattern file printer")
    argument_parser.add_argument("pattern_file",
                                 metavar="PATTERN_FILE",
                                 help="Pattern file",
                                 type=argparse.FileType("rb"))
    arguments = argument_parser.parse_args()
    pattern = Pattern(arguments.pattern_file.read())
    print(pattern)
