"""
File: pcseg.py
Author: Jeff Martin
Date: 10/30/2021

Copyright © 2021 by Jeffrey Martin. All rights reserved.
Email: jmartin@jeffreymartincomposer.com
Website: https://jeffreymartincomposer.com

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 <https://www.gnu.org/licenses/>.
"""

from . import pcset, tables, transformations
from .pitch import PitchClass
import random

_rng = random.Random()
_rng.seed()


def adjacent_search(pcseg: list, corpus: list) -> list:
    """
    Performs an adjacent search on a corpus of pcsegs. Returns all pcsegs in the corpus that contain pcseg as an ordered sublist.
    :param pcseg: A pcseg to search for
    :param corpus: The corpus of pcsegs to search
    :return: A sublist of pcsegs from the corpus that have pcseg in them. If an entry in the original corpus contains pcseg more than once,
    it will only be included once in this sublist.
    If no entries have pcseg in them, returns an empty list.
    """
    new_corpus = []
    for entry in corpus:
        for i in range(0, len(entry) - len(pcseg)):
            found = True
            for j in range(0, len(pcseg)):
                if pcseg[j] != entry[i+j]:
                    found = False
                    break
            if found:
                new_corpus.append(entry)
                break
    return new_corpus


def are_combinatorial2(row1: list, row2: list) -> bool:
    """
    Determines if two rows are hexachordally combinatorial.
    :param row1: A row
    :param row2: A row
    :return: True or False
    *Compatible only with chromatic pcsegs
    """
    if len(row1) != 12 or len(row2) != 12:
        return False
    s1 = set(row1[:6] + row2[:6])
    s2 = set(row1[6:] + row2[6:])
    if len(s1) == len(s2) == 12:
        return True
    else:
        return False


def are_combinatorial3(row1: list, row2: list, row3: list) -> bool:
    """
    Determines if three rows are tetrachordally combinatorial.
    :param row1: A row
    :param row2: A row
    :param row3: A row
    :return: True or False
    *Compatible only with chromatic pcsegs
    """
    if len(row1) != 12 or len(row2) != 12:
        return False
    s1 = set(row1[:4] + row2[:4] + row3[:4])
    s2 = set(row1[4:8] + row2[4:8] + row3[4:8])
    s3 = set(row1[8:] + row2[8:] + row3[8:])
    if len(s1) == len(s2) == len(s3) == 12:
        return True
    else:
        return False


def are_combinatorial4(row1: list, row2: list, row3: list, row4: list) -> bool:
    """
    Determines if four rows are trichordally combinatorial.
    :param row1: A row
    :param row2: A row
    :param row3: A row
    :param row4: A row
    :return: True or False
    *Compatible only with chromatic pcsegs
    """
    if len(row1) != 12 or len(row2) != 12:
        return False
    s1 = set(row1[:3] + row2[:3] + row3[:3] + row4[:3])
    s2 = set(row1[3:6] + row2[3:6] + row3[3:6] + row4[3:6])
    s3 = set(row1[6:9] + row2[6:9] + row3[6:9] + row4[6:9])
    s4 = set(row1[9:] + row2[9:] + row3[9:] + row4[9:])
    if len(s1) == len(s2) == len(s3) == len(s4) == 12:
        return True
    else:
        return False


def bip_n(imb: list) -> list:
    """
    Gets the BIP_n of a list of set-classes generated by IMB_n.
    :param imb: The IMB_n list
    :return: The BIP_n
    *Compatible only with chromatic pcsegs
    """
    bip = []
    if len(imb) > 0:
        for sc in imb:
            name = sc.name_forte.split('-')[1]
            name = name.replace("Z", "")
            bip.append(int(name))
        bip.sort()
    return bip


def create_ormap(pcseg: list) -> dict:
    """
    Creates the ORMAP of a row.
    :param pcseg: A row
    :return: The ORMAP
    *Compatible with all PitchClass modulos    
    """
    omap = {}
    mod = pcseg[0].mod
    for i in range(len(pcseg)):
        omap[PitchClass(pcseg[i].pc, mod)] = i
    return omap


def generate_pcseg12_from_interval_list(interval_list: list, starting_pc=None) -> list:
    """
    Generates a pcseg from an interval list.
    :param interval_list: The interval list
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: A pcseg
    *Compatible only with chromatic pcsegs
    """
    if starting_pc is None:
        pcseg = [PitchClass(_rng.randrange(12), 12)]
        for i in range(len(interval_list)):
            pcseg.append(PitchClass(pcseg[i].pc + interval_list[i], 12))
        return pcseg
    else:
        pcseg = [PitchClass(starting_pc, 12)]
        for i in range(len(interval_list)):
            pcseg.append(PitchClass(pcseg[i].pc + interval_list[i], 12))
        return pcseg


def generate_pcseg24_from_interval_list(interval_list: list, starting_pc=None) -> list:
    """
    Generates a pcseg from an interval list.
    :param interval_list: The interval list
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: A pcseg
    *Compatible only with microtonal pcsegs
    """
    if starting_pc is None:
        pcseg = [PitchClass(_rng.randrange(24), 24)]
        for i in range(len(interval_list)):
            pcseg.append(PitchClass(pcseg[i].pc + interval_list[i], 24))
        return pcseg
    else:
        pcseg = [PitchClass(starting_pc, 24)]
        for i in range(len(interval_list)):
            pcseg.append(PitchClass(pcseg[i].pc + interval_list[i], 24))
        return pcseg


def generate_random_all_interval_row(starting_pc=None) -> list:
    """
    Generates a random all-interval row.
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: An all-interval row
    *Compatible only with chromatic pcsegs
    """
    name_tables = tables.create_tables_eleven_interval()
    row = []

    # Establish the starting pitch of the row
    if starting_pc is not None:
        if type(starting_pc) == PitchClass:
            row.append(PitchClass(starting_pc.pc, 12))
        else:
            row.append(PitchClass(starting_pc, 12))
    else:
        row.append(PitchClass(_rng.randrange(12), 12))
    generator = name_tables["elevenIntervalRowGenerators"][_rng.randrange(
        len(name_tables["elevenIntervalRowGenerators"]))]

    # Randomly choose to invert the row generator
    if _rng.randrange(1) == 1:
        generator = [i * 11 % 12 for i in generator]

    # Build the row
    for i in range(1, 12):
        row.append(PitchClass(row[i - 1].pc + generator[i - 1], 12))
    return row


def generate_random_all_trichord_row(starting_pc=None) -> list:
    """
    Generates a random all-trichord row.
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: An all-trichord row
    *Compatible only with chromatic pcsegs
    """
    name_tables = tables.create_tables_all_trichord()
    if starting_pc is not None:
        if type(starting_pc) == PitchClass:
            starting_pc = starting_pc.pc
    else:
        starting_pc = 0
    row = [PitchClass(pc + starting_pc, 12) for pc in name_tables["allTrichordRows"][_rng.randrange(
        len(name_tables["allTrichordRows"]))]]

    # Randomly choose to invert the row generator
    if _rng.randrange(1) == 1:
        row = invert(row)

    return row


def generate_random_all_trichord_babbitt_row(starting_pc=None) -> list:
    """
    Generates a random all-trichord (Babbitt) row.
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: An all-trichord (Babbitt) row
    *Compatible only with chromatic pcsegs
    """
    name_tables = tables.create_tables_all_trichord_babbitt()
    if starting_pc is not None:
        if type(starting_pc) == PitchClass:
            starting_pc = starting_pc.pc
    else:
        starting_pc = 0
    row = [PitchClass(pc + starting_pc, 12) for pc in name_tables["allTrichordBabbittRows"][_rng.randrange(
        len(name_tables["allTrichordBabbittRows"]))]]

    # Randomly choose to invert the row generator
    if _rng.randrange(1) == 1:
        row = invert(row)

    return row


def generate_random_pcseg12(length: int, non_duplicative: bool = False, starting_pc=None) -> list:
    """
    Generates a random pcseg.
    :param length: The length of the pcseg
    :param non_duplicative: Whether or not duplicate pcs may occur (must be True to generate a row)
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: A random pcseg
    *Compatible only with chromatic pcsegs
    """
    pcseg = [PitchClass(_rng.randrange(12) if starting_pc is None else starting_pc, 12)]
    if non_duplicative and 0 < length <= 12:
        pcs = [i for i in range(12)]
        del pcs[pcseg[0].pc]
        for i in range(length - 1):
            j = _rng.randrange(len(pcs))
            pcseg.append(PitchClass(pcs[j], 12))
            del pcs[j]
    elif not non_duplicative and 0 < length:
        for i in range(length - 1):
            pcseg.append(PitchClass(_rng.randrange(12), 12))
    else:
        raise ValueError("Invalid length")
    return pcseg


def generate_random_pcseg24(length: int, non_duplicative: bool = False, starting_pc=None) -> list:
    """
    Generates a random pcseg.
    :param length: The length of the pcseg
    :param non_duplicative: Whether or not duplicate pcs may occur (must be True to generate a row)
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: A random pcseg
    *Compatible only with microtonal pcsegs
    """
    pcseg = [PitchClass(_rng.randrange(24) if starting_pc is None else starting_pc, 24)]
    if non_duplicative and 0 < length <= 24:
        pcs = [i for i in range(24)]
        del pcs[pcseg[0].pc]
        for i in range(length - 1):
            j = _rng.randrange(len(pcs))
            pcseg.append(PitchClass(pcs[j], 24))
            del pcs[j]
    elif not non_duplicative and 0 < length:
        for i in range(length - 1):
            pcseg.append(PitchClass(_rng.randrange(24), 24))
    else:
        raise ValueError("Invalid length")
    return pcseg


def generate_random_pcseg_from_pcset(pcset: set) -> list:
    """
    Generates a random pcseg from a pcset.
    :param pcset: A pcset
    :return: A pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg = []
    if len(pcset) > 0:
        setseg = list(pcset)
        mod = setseg[0].mod
        for i in range(len(setseg)):
            j = _rng.randrange(len(setseg))
            pcseg.append(PitchClass(setseg[j].pc, mod))
            del setseg[j]
    return pcseg


def generate_random_ten_trichord_row(starting_pc=None) -> list:
    """
    Generates a random ten-trichord row.
    :param starting_pc: The starting pitch-class. If None, a random starting pitch-class is used.
    :return: A ten-trichord row
    *Compatible only with chromatic pcsegs
    """
    name_tables = tables.create_tables_ten_trichord()
    if starting_pc is not None:
        if type(starting_pc) == PitchClass:
            starting_pc = starting_pc.pc
    else:
        starting_pc = 0
    row = [PitchClass(pc + starting_pc, 12) for pc in name_tables["tenTrichordRows"][_rng.randrange(
        len(name_tables["tenTrichordRows"]))]]

    # Randomly choose to invert the row generator
    if _rng.randrange(1) == 1:
        row = invert(row)

    return row


def get_intervals(pcseg: list) -> list:
    """
    Gets the interval sequence of a pcseg.
    :param pcseg: The pcseg
    :return: The interval sequence
    *Compatible with all PitchClass modulos    
    """
    intervals = []
    for i in range(1, len(pcseg)):
        intervals.append(pcseg[i].pc - pcseg[i - 1].pc)
    return intervals


def get_row_class(row: list) -> list:
    """
    Gets all of the rows in a row-class.
    :param row: A row
    :return: The row-class, as a list of rows
    *Compatible with PitchClasses mod 12 and 24
    """
    row_class1 = []
    if len(row) == 12 and row[0].mod == 12:
        ros = transformations.get_otos12()
        mod = 12
    elif len(row) == 24 and row[0].mod == 24:
        ros = transformations.get_otos24()
        mod = 24
    else:
        return row_class1

    row_class = {}
    for t in ["", "I", "R", "RI"]:
        for i in range(mod):
            transform = ros[f"T{i}{t}"].transform(row)
            row_name = str(transform)
            if row_name not in row_class:
                row_class[row_name] = transform
    for row_name in row_class:
        row_class1.append(row_class[row_name])
    
    return row_class1


def get_row_dsym(row: list) -> int:
    """
    Gets the degree of symmetry of a row.
    :param row: A row
    :return: The degree of symmetry of the row
    *Compatible with PitchClasses mod 12 and 24
    """
    if len(row) == 12 and row[0].mod == 12:
        return 48 // len(get_row_class(row))
    elif len(row) == 24 and row[0].mod == 24:
        return 96 // len(get_row_class(row))
    else:
        return 0


def imb_n(pcseg: list, n: int) -> list:
    """
    Gets the IMB_n of a pcseg. The IMB_n is the segment of imbricated set-classes of cardinality n.
    :param pcseg: The pcseg
    :param n: The cardinality of imbrication
    :return: The IMB_n
    *Compatible with all PitchClass modulos
    """
    imb = set()
    scs = []
    if len(pcseg) > 0 and n > 0:
        mod = pcseg[0].mod
        for i in range(len(pcseg) + 1 - n):
            for j in range(i, i + n):
                imb.add(pcseg[j])
            scs.append(pcset.SetClass(imb, mod))
            imb.clear()
    return scs


def invert(pcseg: list) -> list:
    """
    Inverts a pcseg.
    :param pcseg: The pcseg
    :return: The inverted pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg2 = []
    if len(pcseg) > 0:
        mod = pcseg[0].mod
        for pc in pcseg:
            pcseg2.append(PitchClass(pc.pc * -1, mod))
    return pcseg2


def is_all_interval_row(pcseg: list) -> bool:
    """
    Determines if a pcseg is an all-interval row.
    :param pcseg: The pcseg
    :return: Whether or not the pcseg is an all-interval row
    *Compatible with PitchClasses mod 12 and 24
    """
    pcs = {pc.pc for pc in pcseg}
    if len(pcseg) == 12 and pcseg[0].mod == 12:
        if len(pcs) != len(pcseg):
            return False
        else:
            ints = {(pcseg[i].pc - pcseg[i-1].pc) % 12 for i in range(1, len(pcseg))}
            if len(ints) == 11:
                return True
    elif len(pcseg) == 24 and pcseg[0].mod == 24:
        if len(pcs) != len(pcseg):
            return False
        else:
            ints = {(pcseg[i].pc - pcseg[i-1].pc) % 24 for i in range(1, len(pcseg))}
            if len(ints) == 23:
                return True
    return False


def is_all_trichord_row(pcseg: list) -> bool:
    """
    Determines if a pcseg is an all-trichord row.
    :param pcseg: The pcseg
    :return: Whether or not the pcseg is an all-trichord row
    *Compatible only with chromatic pcsegs
    """
    if not is_row(pcseg):
        return False
    row1 = [pcseg[i % 12] for i in range(14)]
    imb3 = imb_n(row1, 3)
    if len(set(imb3)) == 12:
        return True
    else:
        return False


def is_all_trichord_babbitt_row(pcseg: list) -> bool:
    """
    Determines if a pcseg is an "all-trichord" row (after Babbitt). This kind of row
    contains ten unique imbricated trichords, none of which are set-classes [036] or [048].
    Rotation is not considered.
    :param pcseg: The pcseg
    :return: Whether or not the pcseg is an "all-trichord" row (after Babbitt)
    *Compatible only with chromatic pcsegs
    """
    if not is_row(pcseg):
        return False
    imb3 = imb_n(pcseg, 3)
    if len(set(imb3)) == 10:
        for sc in imb3:
            if sc.name_prime == "[036]" or sc.name_prime == "[048]":
                return False
        return True
    else:
        return False


def is_link_chord(pcseg: list) -> bool:
    """
    Determines if a row is a Link chord (an all-interval row containing the all-trichord hexachord as a subset).
    :param pcseg: A pcset
    :return: True or False
    *Compatible only with chromatic pcsegs
    """
    if is_all_interval_row(pcseg):
        imb = imb_n(pcseg, 6)
        for sc in imb:
            if sc.name_forte == "6-Z17":
                return True
    return False


def is_row(pcseg: list) -> bool:
    """
    Determines if a pcseg is a row.
    :param pcseg: The pcseg
    :return: Whether or not the pcseg is a row
    *Compatible with PitchClasses mod 12 and 24
    """
    pcs = set([pc.pc for pc in pcseg])
    if (len(pcseg) == 12 and pcseg[0].mod == 12) or (len(pcseg) == 24 and pcseg[0].mod == 24):
        if len(pcs) != len(pcseg):
            return False
        else:
            return True
    else:
        return False


def is_row_generator(rgen: list) -> bool:
    """
    Determines if a row generator is valid.
    :param rgen: A row generator
    :return: True if the row generator is valid; false otherwise
    *Compatible only with chromatic pcsegs
    """
    for i in range(len(list) - 1):
        rgen_sum = rgen[i]
        for j in range(i + 1, len(list)):
            rgen_sum += rgen[j]
            if rgen_sum % 12 == 0:
                return False
    return True


def is_ten_trichord_row(pcseg: list) -> bool:
    """
    Determines if a pcseg is a ten-trichord row. This kind of row
    contains ten unique imbricated trichords. Rotation is not considered.
    :param pcseg: The pcseg
    :return: Whether or not the pcseg is an "all-trichord" row (after Babbitt)
    *Compatible only with chromatic pcsegs
    """
    if not is_row(pcseg):
        return False
    imb3 = imb_n(pcseg, 3)
    if len(set(imb3)) == 10:
        return True
    else:
        return False


def make_pcseg12(*args) -> list:
    """
    Makes a pcseg.
    :param args: Pcs
    :return: A pcseg
    *Compatible only with chromatic pcsegs
    """
    if type(args[0]) == list:
        args = args[0]
    return [PitchClass(pc, 12) for pc in args]


def make_pcseg24(*args) -> list:
    """
    Makes a pcseg.
    :param args: Pcs
    :return: A pcseg
    *Compatible only with microtonal pcsegs
    """
    if type(args[0]) == list:
        args = args[0]
    return [PitchClass(pc, 24) for pc in args]


def multiply(pcseg: list, n: int) -> list:
    """
    Multiplies a pcseg.
    :param pcseg: The pcseg
    :param n: The multiplier
    :return: The multiplied pcseg
    *Compatible with all PitchClass modulos    
    """
    pcseg2 = []
    if len(pcseg) > 0:
        mod = pcseg[0].mod
        for pc in pcseg:
            pcseg2.append(PitchClass(pc.pc * n, mod))
    return pcseg2


def multiply_order(pcseg: list, n: int) -> list:
    """
    Multiplies a pcseg's order.
    :param pcseg: The pcseg
    :param n: The multiplier
    :return: The order-multiplied pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg2 = []
    if len(pcseg) > 0 and n > 0:
        mod = pcseg[0].mod
        for i in range(len(pcseg)):
            pcseg2.append(PitchClass(pcseg[(i * n) % len(pcseg)].pc, mod))
    return pcseg2


def ormap(row: list, ormap: dict) -> list:
    """
    Performs the ORMAP mapping on a row, given a provided ORMAP.
    :param row: A row
    :param ormap:
    :return: The ORMAP mapping for the row
    *Compatible with all PitchClass modulos
    """
    mapping = []
    for item in row:
        mapping.append(ormap[item])
    return mapping


def prot(pcseg: list) -> set:
    """
    Generates the protocol pairs for a pcseg.
    :param pcseg: A pcseg
    :return: A set of protocol pairs
    *Compatible with all PitchClass modulos
    """
    pp = set()
    for i in range(len(pcseg)):
        for j in range(i, len(pcseg)):
            pp.add((pcseg[i], pcseg[j]))
    return pp


def retrograde(pcseg: list) -> list:
    """
    Retrogrades a pcseg.
    :param pcseg: The pcseg
    :return: The retrograded pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg2 = []
    if len(pcseg) > 0:
        mod = pcseg[0].mod
        for i in range(len(pcseg) - 1, -1, -1):
            pcseg2.append(PitchClass(pcseg[i].pc, mod))
    return pcseg2


def rotate(pcseg: list, n: int) -> list:
    """
    Rotates a pcseg.
    :param pcseg: The pcseg
    :param n: The index of rotation
    :return: The rotated pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg2 = []
    if len(pcseg) > 0:
        mod = pcseg[0].mod
        for i in range(len(pcseg)):
            pcseg2.append(PitchClass(pcseg[(i - n) % len(pcseg)].pc, mod))
    return pcseg2


def transform(pcseg: list, string: str) -> list:
    """
    Transforms a pcseg with a provided transformation string.
    - Tn: transpose
    - I: invert
    - Mn: multiply
    - R: retrograde
    - rn: rotate
    :param pcseg: The pcseg to transform
    :param string: The transformation string
    :return: The transformed pcseg
    """
    pcseg2 = list.copy(pcseg)
    i = len(string) - 1
    while i >= 0:
        num = 0
        place_exp = 0
        while str.isdigit(string[i]) and i > 0:
            num += int(string[i]) * 10 ** place_exp
            place_exp += 1
            i -= 1
        match string[i]:
            case 'T':
                pcseg2 = transpose(pcseg2, num)
            case 'I':
                pcseg2 = invert(pcseg2)
            case 'M':
                pcseg2 = multiply(pcseg2, num)
            case 'R':
                pcseg2.reverse()
            case 'r':
                pcseg2 = rotate(pcseg2, num)
        i -= 1
    return pcseg2


def transform_hstack(pcseg: list, transformation_list: list) -> list:
    """
    Generates a really long pcseg by horizontally stacking transformed versions of it.
    :param pcseg: The pcseg to stack
    :param transformation_list: A list of transformations to stack
    :return: The stacked and transformed pcseg
    """
    pcseg1 = []
    for t in transformation_list:
        pcseg1 += transform(pcseg, t)
    return pcseg1


def transpose(pcseg: list, n: int) -> list:
    """
    Transposes a pcseg.
    :param pcseg: The pcseg
    :param n: The index of transposition
    :return: The transposed pcseg
    *Compatible with all PitchClass modulos
    """
    pcseg2 = []
    if len(pcseg) > 0:
        mod = pcseg[0].mod
        for pc in pcseg:
            pcseg2.append(PitchClass(pc.pc + n, mod))
    return pcseg2


def transpositional_combination(pcseg1: list, pcseg2: list) -> list:
    """
    Transpositionally combines (TC) two pcsegs. This is Boulez's "multiplication."
    :param pcseg1: A pcseg
    :param pcseg2: A pcseg
    :return: The TC pcset
    *Compatible with all PitchClass modulos
    """
    pcseg3 = []
    if len(pcseg1) > 0 and len(pcseg2) > 0:
        mod = pcseg1[0].mod
        for pc2 in pcseg2:
            for pc1 in pcseg1:
                pcseg3.append(PitchClass(pc1.pc + pc2.pc, mod))
    return pcseg3


class InvarianceMatrix:
    """
    Represents an invariance matrix. Compatible with all PitchClass modulos for T and I
    matrices, and compatible with mod 12 and mod 24 for various M matrices
    (M5, M7 for mod 12; M5, M7, M11, M13, M17, M19 for mod 24)
    """
    def __init__(self, a: list, c: list, mx_type="T"):
        """
        Creates an invariance matrix.
        :param a: Pcseg A
        :param c: Pcseg B
        :param mx_type: The matrix type (T, I, M, or MI)
        """
        self._a = None
        self._b = None
        self._c = None
        self._mx_type = mx_type.upper()
        self._mx = None
        self._load_matrix(a, c)

    def __getitem__(self, i: int, j: int) -> PitchClass:
        """
        Gets the pc at the specified row and column.
        :param i: The row
        :param j: The column
        :return: The pc
        """
        return self._mx[i][j]

    def __repr__(self):
        """
        Gets a representation of the InvarianceMatrix object.
        :returns: A string representation of the InvarianceMatrix object
        """
        return f"<pctheory.pcseg.InvarianceMatrix object at {id(self)}>: {self._mx}"

    def __str__(self):
        """
        Converts the InvarianceMatrix object to string.
        :returns: A string conversion of the InvarianceMatrix object
        """
        chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B']
        lines = "    "
        for pc in self._a:
            lines += chars[pc.pc] + " "
        lines += "\n   "
        for i in range(2 * len(self._a)):
            lines += "-"
        lines += "\n"
        for i in range(len(self._mx) - 1):
            lines += chars[self._b[i].pc] + " | "
            for j in range(len(self._mx[i])):
                lines += chars[self._mx[i][j].pc] + " "
            lines += "\n"
        lines += chars[self._b[len(self._mx) - 1].pc] + " | "
        for j in range(len(self._mx[len(self._mx) - 1])):
            lines += chars[self._mx[len(self._mx) - 1][j].pc] + " "
        return lines

    @property
    def mx(self) -> list:
        """
        Gets the invariance matrix.
        :return: The invariance matrix
        """
        return self._mx

    @property
    def pcseg_a(self) -> list:
        """
        Gets the pcseg A.
        :return: The pcseg A
        """
        return self._a

    @property
    def pcseg_b(self) -> list:
        """
        Gets the pcseg B.
        :return: The pcseg B
        """
        return self._b

    @property
    def pcseg_c(self) -> list:
        """
        Gets the pcseg C.
        :return: The pcseg C
        """
        return self._c

    def get_column(self, j: int) -> list:
        """
        Gets a column of the matrix.
        :param j: The column index
        :return: The column
        """
        c = []
        for i in range(len(self._mx)):
            c.append(self._mx[i][j])
        return c

    def get_row(self, i: int) -> list:
        """
        Gets a row of the matrix.
        :param i: The row index
        :return: The row
        """
        return self._mx[i]

    def _load_matrix(self, a: list, c: list):
        """
        Loads the matrix.
        :param a: Pcseg A
        :param c: Pcseg C
        :return: None
        """
        if len(a) > 0 and len(c) > 0:
            ro = transformations.OTO()
            if a[0].mod == 12:
                INVERT = 11
                if self._mx_type == "T":
                    ro.oto = [0, 0, (1 * INVERT) % 12]
                elif self._mx_type == "M" or self._mx_type == "M5":
                    ro.oto = [0, 0, (5 * INVERT) % 12]
                elif self._mx_type == "MI" or self._mx_type == "M7":
                    ro.oto = [0, 0, (7 * INVERT) % 12]
                elif self._mx_type == "I" or self._mx_type == "M11":
                    ro.oto = [0, 0, (11 * INVERT) % 12]
                b = ro.transform(c)
            elif a[0].mod == 24:
                INVERT = 23
                if self._mx_type == "T":
                    ro.oto = [0, 0, (1 * INVERT) % 24]
                elif self._mx_type == "M5":
                    ro.oto = [0, 0, (5 * INVERT) % 24]
                elif self._mx_type == "M7":
                    ro.oto = [0, 0, (7 * INVERT) % 24]
                elif self._mx_type == "M11":
                    ro.oto = [0, 0, (11 * INVERT) % 24]
                elif self._mx_type == "M13":
                    ro.oto = [0, 0, (13 * INVERT) % 24]
                elif self._mx_type == "M17":
                    ro.oto = [0, 0, (17 * INVERT) % 24]
                elif self._mx_type == "M19":
                    ro.oto = [0, 0, (19 * INVERT) % 24]
                elif self._mx_type == "I" or self._mx_type == "M23":
                    ro.oto = [0, 0, (23 * INVERT) % 24]
                b = ro.transform(c)
            else:
                if self._mx_type == "T":
                    ro.oto = [0, 0, 1 * (a[0].mod - 1) % a[0].mod]
                elif self._mx_type == "I":
                    ro.oto = [0, 0, 11 * (a[0].mod - 1) % a[0].mod]
                b = ro.transform(c)

            self._mx = []
            for i in range(len(b)):
                mxrow = []
                for j in range(len(a)):
                    mxrow.append(PitchClass(b[i].pc + a[j].pc, a[0].mod))
                self._mx.append(mxrow)
            self._a = a.copy()
            self._b = b
            self._c = c.copy()

    def print(self, include: list = None):
        """
        Prints the invariance matrix.
        :param include: The pcs to include (if None, all pcs will be printed)
        :returns: None
        """
        chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B']
        lines = "    "
        for pc in self._a:
            lines += chars[pc.pc] + " "
        lines += "\n   "
        for i in range(2 * len(self._a)):
            lines += "-"
        lines += "\n"
        if include is None:
            for i in range(len(self._mx) - 1):
                lines += chars[self._b[i].pc] + " | "
                for j in range(len(self._mx[i])):
                    lines += chars[self._mx[i][j].pc] + " "
                lines += "\n"
            lines += chars[self._b[len(self._mx) - 1].pc] + " | "
            for j in range(len(self._mx[len(self._mx) - 1])):
                lines += chars[self._mx[len(self._mx) - 1][j].pc] + " "
        else:
            for i in range(len(self._mx) - 1):
                lines += chars[self._b[i].pc] + " | "
                for j in range(len(self._mx[i])):
                    if self._mx[i][j] in include:
                        lines += chars[self._mx[i][j].pc] + " "
                    else:
                        lines += "  "
                lines += "\n"
            lines += chars[self._b[len(self._mx) - 1].pc] + " | "
            for j in range(len(self._mx[len(self._mx) - 1])):
                if self._mx[len(self._mx) - 1][j] in include:
                    lines += chars[self._mx[len(self._mx) - 1][j].pc] + " "
                else:
                    lines += "  "
        print(lines)


class TwelveToneMatrix:
    """
    Represents a twelve-tone matrix. Compatible only with mod 12 PitchClasses.
    """
    def __init__(self, row=None):
        """
        Creates a twelve-tone matrix.
        :param row: A row to import
        """
        self._labels_left = []
        self._labels_right = []
        self._labels_top = []
        self._labels_bottom = []
        self._mx = []
        self._row = None
        if row is not None:
            self.import_row(row)

    def __getitem__(self, i: int, j: int) -> PitchClass:
        """
        Gets the pc at the specified row and column.
        :param i: The row
        :param j: The column
        :return: The pc
        """
        return self._mx[i][j]

    def __repr__(self):
        """
        Generates a string representation of the TwelveToneMatrix that can be printed.
        :return: A string representation of the TwelveToneMatrix
        """
        lines = "     "
        for i in range(12):
            lines += f"{self._labels_top[i]: <5} "
        lines += "\n"
        for i in range(12):
            lines += f"{self._labels_left[i]: <4}"
            for j in range(12):
                lines += f"  {str(self._mx[i][j])}   "
            lines += f"{self._labels_right[i]: >4}"
            lines += "\n"
        lines += "     "
        for i in range(12):
            lines += f"{self._labels_bottom[i]: <5} "
        return lines

    def __str__(self):
        """
        Generates a string representation of the TwelveToneMatrix that can be printed.
        :return: A string representation of the TwelveToneMatrix
        """
        lines = "     "
        for i in range(12):
            lines += f"{self._labels_top[i]: <5} "
        lines += "\n"
        for i in range(12):
            lines += f"{self._labels_left[i]: <4}"
            for j in range(12):
                lines += f"  {str(self._mx[i][j])}   "
            lines += f"{self._labels_right[i]: >4}"
            lines += "\n"
        lines += "     "
        for i in range(12):
            lines += f"{self._labels_bottom[i]: <5} "
        return lines

    @property
    def labels_bottom(self) -> list:
        """
        Gets the bottom matrix labels.
        :return: The bottom matrix labels
        """
        return self._labels_bottom

    @property
    def labels_left(self) -> list:
        """
        Gets the left matrix labels.
        :return: The left matrix labels
        """
        return self._labels_left

    @property
    def labels_right(self) -> list:
        """
        Gets the right matrix labels.
        :return: The right matrix labels
        """
        return self._labels_right

    @property
    def labels_top(self) -> list:
        """
        Gets the top matrix labels.
        :return: The top matrix labels
        """
        return self._labels_top

    @property
    def matrix(self) -> list:
        """
        Gets the matrix.
        :return: The matrix
        """
        return self._mx

    @property
    def row(self) -> list:
        """
        Gets the row.
        :return: The row
        """
        return self._row

    def get_column(self, j: int) -> list:
        """
        Gets a column of the matrix.
        :param j: The column index
        :return: The column
        """
        pcseg = []
        for i in range(len(self._mx)):
            pcseg.append(PitchClass(self._mx[i][j].pc, 12))
        return pcseg

    def get_row(self, i: int) -> list:
        """
        Gets a row of the matrix.
        :param i: The row index
        :return: The row
        """
        return self._mx[i].copy()

    def import_row(self, row: list):
        """
        Imports a row.
        :param row: The row to import
        :return:
        """
        # We need the starting pitch to be 0 for the P0 form
        self._row = transpose(row, -row[0].pc)
        inv = invert(self._row)

        for i in range(len(self._row)):
            self._mx.append(transpose(self._row, inv[i].pc))

        for i in range(len(self._row)):
            p_lbl = self._mx[i][0] - self._mx[0][0]
            i_lbl = self._mx[0][i] - self._mx[0][0]
            r_lbl = self._mx[i][len(self._row) - 1] - self._mx[0][len(self._row) - 1]
            ri_lbl = self._mx[len(self._row) - 1][i] - self._mx[len(self._row) - 1][0]
            self._labels_bottom.append(f"T{ri_lbl.pc}RI")
            self._labels_left.append(f"T{p_lbl.pc}")
            self._labels_right.append(f"T{r_lbl.pc}R")
            self._labels_top.append(f"T{i_lbl.pc}I")
