import os
import math


# Protocol settings: https://phreakerclub.com/447
class Protocol:
    """
    Generic OOK communication protocol:
        - name: the protocol's name
        - n_bits: number of bits composing a key
        - transposition_table: how 1s and 0s are translated to .sub format
        - pilot_period: preamble PREPENDED to each key in sub format, empty by default
        - stop_bit: APPENDED to each key in sub format, empty by default
        - frequency: protocol's frequency in Hz, 433920000 by default
        - repetition: number of times to repeat each key, 3 by default
        - key_range: range of keys to generate, None by default
    """

    def __init__(
        self,
        name,
        n_bits,
        transposition_table,
        pilot_period="",
        stop_bit="",
        frequency=433920000,
        repetition=3,
        key_range=None,
    ):
        self.name = name
        self.n_bits = n_bits
        self.transposition_table = transposition_table
        self.pilot_period = pilot_period
        self.stop_bit = stop_bit
        self.repetition = repetition
        self.key_range = key_range
        self.file_header = (
            "Filetype: Flipper SubGhz RAW File\n"
            + "Version: 1\n"
            + f"Frequency: {frequency}\n"
            + "Preset: FuriHalSubGhzPresetOok650Async\n"
            + "Protocol: RAW\n"
        )

    def key_to_sub(self, key):
        bin_str = f"{key:0{self.n_bits}b}"
        return self.key_bin_str_to_sub(bin_str)

    def key_bin_str_to_sub(self, bin_str, debruijn=False):
        sub = ""
        if not debruijn:
            sub = self.pilot_period
        line_len = 0  # keep lines under 2500 chars
        for bit in bin_str:
            if line_len > 2500:
                sub += "\nRAW_Data: "
                line_len = 0
            sub += self.transposition_table[bit]
            line_len += len(self.transposition_table[bit])
        if not debruijn:
            sub += self.stop_bit
        return sub

    def de_bruijn(self):
        """
        de Bruijn binary sequence
        credit: https://www.rosettacode.org/wiki/De_Bruijn_sequences#Python
        """
        alphabet = "01"
        k = 2
        a = [0] * k * (self.n_bits if self.pilot_period == "" else self.n_bits + 1)
        sequence = []

        def db(t, p):
            if t > self.n_bits:
                if self.n_bits % p == 0:
                    sequence.extend(a[1 : p + 1])
            else:
                a[t] = a[t - p]
                db(t + 1, p)
                for j in range(a[t - p] + 1, k):
                    a[t] = j
                    db(t + 1, t)

        db(1, 1)
        db_seq = "".join(alphabet[i] for i in sequence)
        return self.key_bin_str_to_sub(db_seq, True)

    def generate_sub_files(self, n_folders=6):
        """
        Generate sub files grouped by split nuber of keys
        Directory structure:
        - sub_files
            - protocol_name
                - split_factor
                    - split_factor_id.sub

        n_folder: number of folders to create,
                  folder [1..n_folders] will contain [2^1..2^n_folders] files,
                  each file containing [2^n_bits/2^1..2^n_bits/2^n_folders] keys.
        """
        if (
            self.n_bits > 12 and self.key_range is None
        ):  # take up too much space for github
            print(f"Skipping {self.name}, takes up too much space for github")
            return
        base_dir = f"sub_files/{self.name}"
        os.makedirs(base_dir, exist_ok=True)
        # If key_range is defined, generate those keys only
        if self.key_range is not None:
            filename = f"{base_dir}/bf_{self.key_range[0]}-{self.key_range[-1]}.sub"
            with open(filename, "w") as f:
                f.write(self.file_header)
                for key in self.key_range:
                    f.write(
                        "RAW_Data: " + self.key_to_sub(key) * self.repetition + "\n"
                    )
            return
        # Create debruijn.sub
        filename = f"{base_dir}/debruijn.sub"
        with open(filename, "w") as f:
            f.write(self.file_header)
            f.write("RAW_Data: " + self.de_bruijn() + "\n")
        # Generate sets of 2^0, 2^1, .., 2^n_folders .sub files
        splits = [
            int(pow(2, self.n_bits) / _) for _ in [pow(2, _) for _ in range(n_folders)]
        ]
        [os.makedirs(f"{base_dir}/{split}", exist_ok=True) for split in splits]
        split_files = [None] * len(splits)  # current file per split
        for key in range(pow(2, self.n_bits)):
            for idx, split in enumerate(splits):
                if key % split == 0:
                    # close previous file if open
                    if split_files[idx] is not None:
                        split_files[idx].close()
                    filename = f"{base_dir}/{split}/{math.floor(key/(split*2)):03d}_{key/split:03.0f}.sub"
                    split_files[idx] = open(filename, "w")
                    split_files[idx].write(self.file_header)
                split_files[idx].write(
                    "RAW_Data: " + self.key_to_sub(key) * self.repetition + "\n"
                )
        # close all files
        [f.close() for f in split_files if f is not None]


protocols = [
    Protocol(
        name="CAME-12bit-433",
        n_bits=12,
        transposition_table={"0": "-320 640 ", "1": "-640 320 "},
        pilot_period="-11520 320 ",
    ),
    Protocol(
        name="CAME-12bit-433-fast",
        n_bits=12,
        transposition_table={"0": "-250 500 ", "1": "-500 250 "},
        pilot_period="-9000 250 ",
    ),
    Protocol(
        name="CAME-12bit-868",
        n_bits=12,
        transposition_table={"0": "-320 640 ", "1": "-640 320 "},
        pilot_period="-11520 320 ",
        frequency=868350000,
    ),
    Protocol(
        name="CAME-12bit-868-fast",
        n_bits=12,
        transposition_table={"0": "-250 500 ", "1": "-500 250 "},
        pilot_period="-9000 250 ",
        frequency=868350000,
    ),
    Protocol(
        name="Chamberlain-9bit-315",
        n_bits=10,
        transposition_table={"0": "-1000 3000 ", "1": "-2000 2000 "},
        pilot_period="-39000 1000 ",
        stop_bit="-3000 1000 ",
        frequency=315000000,
    ),
    Protocol(
        name="Chamberlain-9bit-390",
        n_bits=10,
        transposition_table={"0": "-1000 3000 ", "1": "-2000 2000 "},
        pilot_period="-39000 1000 ",
        stop_bit="-3000 1000 ",
        frequency=390000000,
    ),
    Protocol(
        name="Linear-10bit-300",
        n_bits=10,
        transposition_table={"0": "500 -1500 ", "1": "1500 -500 "},
        stop_bit="1 -21500 ",
        frequency=300000000,
        repetition=5,
    ),
    Protocol(
        name="Linear-10bit-310",
        n_bits=10,
        transposition_table={"0": "500 -1500 ", "1": "1500 -500 "},
        stop_bit="1 -21500 ",
        frequency=310000000,
        repetition=5,
    ),
    Protocol(
        name="NICE-12bit-433",
        n_bits=12,
        transposition_table={"0": "-700 1400 ", "1": "-1400 700 "},
        pilot_period="-25200 700 ",
    ),
    Protocol(
        name="NICE-12bit-868",
        n_bits=12,
        transposition_table={"0": "-700 1400 ", "1": "-1400 700 "},
        pilot_period="-25200 700 ",
        frequency=868350000,
    ),
    Protocol(
        name="PT-2240-433",
        n_bits=24,
        transposition_table={"0": "450 -1350 ", "1": "1350 -450 "},
        pilot_period="450 -13950 ",
    ),
    Protocol(
        name="Spacca_pager-433",
        n_bits=24,
        transposition_table={"0": "320 -960 ", "1": "960 -320 "},
        pilot_period="320 -9920 ",
        frequency=433650000,
        key_range=range(0x11A01C, 0x11A0E4),  # 300 keys
    ),
    Protocol(
        name="Ansonic-434",
        n_bits=12,
        transposition_table={"0": "-1111 555 ", "1": "-555 1111 "},
        frequency=434075000,
        pilot_period="-19425 555 ",
    ),
    Protocol(
        name="Holtek-315",
        n_bits=12,
        transposition_table={"0": "-870 430 ", "1": "-430 870 "},
        frequency=315000000,
        pilot_period="-15480 430 ",
    ),
    Protocol(
        name="Holtek-433",
        n_bits=12,
        transposition_table={"0": "-870 430 ", "1": "-430 870 "},
        pilot_period="-15480 430 ",
    ),
    Protocol(
        name="Holtek-868",
        n_bits=12,
        transposition_table={"0": "-870 430 ", "1": "-430 870 "},
        frequency=868350000,
        pilot_period="-15480 430 ",
    ),
    Protocol(
        name="Holtek-915",
        n_bits=12,
        transposition_table={"0": "-870 430 ", "1": "-430 870 "},
        frequency=915000000,
        pilot_period="-15480 430 ",
    ),
]

for p in protocols:
    # TODO multithread this
    p.generate_sub_files()
    print(f"{p.name} done")
