from django.db import models
from snippets.countries import CountryField

# Support CountryField for South
from south.modelsinspector import add_introspection_rules
from django.db.transaction import managed
add_introspection_rules([], ["^snippets\.countries\.CountryField"])

# Create your models here.
class Player(models.Model):
    FIDE_TITLES = (
        ('8 GM', 'Grandmaster'),
        ('7 IM', 'International Master'),
        ('5 FM', 'FIDE Master'),
        ('3 CM', 'Candidate Master'),
        ('6 WGM', 'Woman Grandmaster'),
        ('4 WIM', 'Woman International Master'),
        ('2 WFM', 'Woman FIDE Master'),
        ('1 WCM', 'Woman Candidate Master'),
        ('0 nt', 'no title'),
    )
    DEVELOPMENT_COEFFICIENT = (
        (None, 'Auto'),
        (30, '30'),
        (15, '15'),
        (10, '10'),
    )

    name = models.CharField(max_length=200)
    country = CountryField()
    register_date = models.DateField('registration date', auto_now_add=True)
    initial_rating = models.IntegerField()
    rating = models.IntegerField()
    rating_dev_coef = models.IntegerField(choices=DEVELOPMENT_COEFFICIENT, default=None, null=True)
    last_rating_calculation = models.DateField(null=True, default=None)
    fide_id = models.BigIntegerField('FIDE ID')
    fide_title = models.CharField(max_length=10, choices=FIDE_TITLES, default='0 nt')

    def save(self, *args, **kwargs):
        if self.id == None:
            self.initial_rating = self.rating
        super(Player, self).save(*args, **kwargs) # Call the "real" save() method.

    def __unicode__(self):
        return self.name

class Tournament(models.Model):
    BYE_SCORES = (
        (0.5, '0.5'),
        (1.0, '1.0'),
    )

    name = models.CharField(max_length=200)
    country = CountryField()
    city = models.CharField(max_length=200)
    start_date = models.DateField()
    end_date = models.DateField()
    players = models.ManyToManyField(Player)
    bye_score = models.DecimalField(max_digits=2, decimal_places=1, default=1, choices=BYE_SCORES)

    def __unicode__(self):
        return self.name

    def players_count(self):
        return self.players.count()

class Round(models.Model):
    tournament = models.ForeignKey(Tournament)
    name = models.CharField(max_length=200)
    round_date = models.DateField()

    def __unicode__(self):
        return self.tournament.name + " - " + self.name

class Game(models.Model):
    GAME_STATUSES = (
        ('planned', 'Planned'),
        ('finished', 'Finished'),
        ('walkover', 'Walkover'),
    )

    PLAYER_SCORES = (
        (0, '0'),
        (0.5, '0.5'),
        (1.0, '1'),
    )

    PLAYER_COLOR = (
        ('W', 'White'),
        ('B', 'Black'),
    )

    round = models.ForeignKey(Round)
    player = models.ForeignKey(Player, related_name="player_a")
    player_color = models.CharField(max_length=1,
                                      choices=PLAYER_COLOR,
                                      default='W')
    player_score = models.DecimalField(max_digits=4, decimal_places=1, default=0, choices=PLAYER_SCORES)
    opponent_score = models.DecimalField(max_digits=4, decimal_places=1, default=0, choices=PLAYER_SCORES)
    opponent_color = models.CharField(max_length=1,
                                      choices=PLAYER_COLOR,
                                      default='B')
    opponent = models.ForeignKey(Player, related_name="player_b", null=True, blank=True)
    status = models.CharField(max_length=10,
                              choices=GAME_STATUSES,
                              default='planned')

    def __unicode__(self):
        return str(self.player) + " " + str(self.player_score) + ":" + str(self.opponent_score) + " " + str(self.opponent)

# Bind model to SQL VIEW
class Tournament_Player_Score(models.Model):
    id = models.CharField(max_length=20, primary_key=True)
    tournament = models.ForeignKey(Tournament)
    player = models.ForeignKey(Player)
    score = models.DecimalField(max_digits=4, decimal_places=1)
    rating = models.IntegerField()
    fide_title = models.CharField(max_length=10)
    name = models.CharField(max_length=200)

    class Meta:
        managed = False

    def __unicode__(self):
        return str(self.player) + " " + str(self.score)


import math
import random
import functools
import operator

import torch
from torch import nn
from torchvision import models
import torch.utils.model_zoo as model_zoo

from torch.nn import functional as F
from torch.autograd import Function
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence
from torch.autograd import Variable

from op import FusedLeakyReLU, fused_leaky_relu, upfirdn2d, conv2d_gradfix

from miscc.config import cfg

class PixelNorm(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, input):
        return input * torch.rsqrt(torch.mean(input ** 2, dim=1, keepdim=True) + 1e-8)

def make_kernel(k):
    k = torch.tensor(k, dtype=torch.float32)

    if k.ndim == 1:
        k = k[None, :] * k[:, None]

    k /= k.sum()

    return k

class Upsample(nn.Module):
    def __init__(self, kernel, factor=2):
        super().__init__()

        self.factor = factor
        kernel = make_kernel(kernel) * (factor ** 2)
        self.register_buffer("kernel", kernel)

        p = kernel.shape[0] - factor

        pad0 = (p + 1) // 2 + factor - 1
        pad1 = p // 2

        self.pad = (pad0, pad1)

    def forward(self, input):
        out = upfirdn2d(input, self.kernel, up=self.factor, down=1, pad=self.pad)

        return out

class Downsample(nn.Module):
    def __init__(self, kernel, factor=2):
        super().__init__()

        self.factor = factor
        kernel = make_kernel(kernel)
        self.register_buffer("kernel", kernel)

        p = kernel.shape[0] - factor

        pad0 = (p + 1) // 2
        pad1 = p // 2

        self.pad = (pad0, pad1)

    def forward(self, input):
        out = upfirdn2d(input, self.kernel, up=1, down=self.factor, pad=self.pad)

        return out

class Blur(nn.Module):
    def __init__(self, kernel, pad, upsample_factor=1):
        super().__init__()

        kernel = make_kernel(kernel)

        if upsample_factor > 1:
            kernel = kernel * (upsample_factor ** 2)

        self.register_buffer("kernel", kernel)

        self.pad = pad

    def forward(self, input):
        out = upfirdn2d(input, self.kernel, pad=self.pad)

        return out

class EqualConv2d(nn.Module):
    def __init__(
        self, in_channel, out_channel, kernel_size, stride=1, padding=0, bias=True
    ):
        super().__init__()

        self.weight = nn.Parameter(
            torch.randn(out_channel, in_channel, kernel_size, kernel_size)
        )
        self.scale = 1 / math.sqrt(in_channel * kernel_size ** 2)

        self.stride = stride
        self.padding = padding

        if bias:
            self.bias = nn.Parameter(torch.zeros(out_channel))

        else:
            self.bias = None

    def forward(self, input):
        out = conv2d_gradfix.conv2d(
            input,
            self.weight * self.scale,
            bias=self.bias,
            stride=self.stride,
            padding=self.padding,
        )

        return out

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.weight.shape[1]}, {self.weight.shape[0]},"
            f" {self.weight.shape[2]}, stride={self.stride}, padding={self.padding})"
        )

class EqualLinear(nn.Module):
    def __init__(
        self, in_dim, out_dim, bias=True, bias_init=0, lr_mul=1, activation=None
    ):
        super().__init__()

        self.weight = nn.Parameter(torch.randn(out_dim, in_dim).div_(lr_mul))

        if bias:
            self.bias = nn.Parameter(torch.zeros(out_dim).fill_(bias_init))

        else:
            self.bias = None

        self.activation = activation

        self.scale = (1 / math.sqrt(in_dim)) * lr_mul
        self.lr_mul = lr_mul

    def forward(self, input):
        if self.activation:
            out = F.linear(input, self.weight * self.scale)
            out = fused_leaky_relu(out, self.bias * self.lr_mul)

        else:
            out = F.linear(
                input, self.weight * self.scale, bias=self.bias * self.lr_mul
            )

        return out

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.weight.shape[1]}, {self.weight.shape[0]})"
        )

class ModulatedConv2d(nn.Module):
    def __init__(
        self,
        in_channel,
        out_channel,
        kernel_size,
        style_dim,
        demodulate=True,
        upsample=False,
        downsample=False,
        blur_kernel=[1, 3, 3, 1],
        fused=True,
    ):
        super().__init__()

        self.eps = 1e-8
        self.kernel_size = kernel_size
        self.in_channel = in_channel
        self.out_channel = out_channel
        self.upsample = upsample
        self.downsample = downsample

        if upsample:
            factor = 2
            p = (len(blur_kernel) - factor) - (kernel_size - 1)
            pad0 = (p + 1) // 2 + factor - 1
            pad1 = p // 2 + 1

            self.blur = Blur(blur_kernel, pad=(pad0, pad1), upsample_factor=factor)

        if downsample:
            factor = 2
            p = (len(blur_kernel) - factor) + (kernel_size - 1)
            pad0 = (p + 1) // 2
            pad1 = p // 2

            self.blur = Blur(blur_kernel, pad=(pad0, pad1))

        fan_in = in_channel * kernel_size ** 2
        self.scale = 1 / math.sqrt(fan_in)
        self.padding = kernel_size // 2

        self.weight = nn.Parameter(
            torch.randn(1, out_channel, in_channel, kernel_size, kernel_size)
        )

        self.modulation = EqualLinear(style_dim, in_channel, bias_init=1)

        self.demodulate = demodulate
        self.fused = fused

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.in_channel}, {self.out_channel}, {self.kernel_size}, "
            f"upsample={self.upsample}, downsample={self.downsample})"
        )

    def forward(self, input, style):
        batch, in_channel, height, width = input.shape

        if not self.fused:
            weight = self.scale * self.weight.squeeze(0)
            style = self.modulation(style)

            if self.demodulate:
                w = weight.unsqueeze(0) * style.view(batch, 1, in_channel, 1, 1)
                dcoefs = (w.square().sum((2, 3, 4)) + 1e-8).rsqrt()

            input = input * style.reshape(batch, in_channel, 1, 1)

            if self.upsample:
                weight = weight.transpose(0, 1)
                out = conv2d_gradfix.conv_transpose2d(
                    input, weight, padding=0, stride=2
                )
                out = self.blur(out)

            elif self.downsample:
                input = self.blur(input)
                out = conv2d_gradfix.conv2d(input, weight, padding=0, stride=2)

            else:
                out = conv2d_gradfix.conv2d(input, weight, padding=self.padding)

            if self.demodulate:
                out = out * dcoefs.view(batch, -1, 1, 1)

            return out

        style = self.modulation(style).view(batch, 1, in_channel, 1, 1)
        weight = self.scale * self.weight * style

        if self.demodulate:
            demod = torch.rsqrt(weight.pow(2).sum([2, 3, 4]) + 1e-8)
            weight = weight * demod.view(batch, self.out_channel, 1, 1, 1)

        weight = weight.view(
            batch * self.out_channel, in_channel, self.kernel_size, self.kernel_size
        )

        if self.upsample:
            input = input.view(1, batch * in_channel, height, width)
            weight = weight.view(
                batch, self.out_channel, in_channel, self.kernel_size, self.kernel_size
            )
            weight = weight.transpose(1, 2).reshape(
                batch * in_channel, self.out_channel, self.kernel_size, self.kernel_size
            )
            out = conv2d_gradfix.conv_transpose2d(
                input, weight, padding=0, stride=2, groups=batch
            )
            _, _, height, width = out.shape
            out = out.view(batch, self.out_channel, height, width)
            out = self.blur(out)

        elif self.downsample:
            input = self.blur(input)
            _, _, height, width = input.shape
            input = input.view(1, batch * in_channel, height, width)
            out = conv2d_gradfix.conv2d(
                input, weight, padding=0, stride=2, groups=batch
            )
            _, _, height, width = out.shape
            out = out.view(batch, self.out_channel, height, width)

        else:
            input = input.view(1, batch * in_channel, height, width)
            out = conv2d_gradfix.conv2d(
                input, weight, padding=self.padding, groups=batch
            )
            _, _, height, width = out.shape
            out = out.view(batch, self.out_channel, height, width)

        return out

class NoiseInjection(nn.Module):
    def __init__(self):
        super().__init__()

        self.weight = nn.Parameter(torch.zeros(1))

    def forward(self, image, noise=None):
        if noise is None:
            batch, _, height, width = image.shape
            noise = image.new_empty(batch, 1, height, width).normal_()

        return image + self.weight * noise

class ConstantInput(nn.Module):
    def __init__(self, channel, size=4):
        super().__init__()

        self.input = nn.Parameter(torch.randn(1, channel, size, size))

    def forward(self, input):
        batch = input.shape[0]
        out = self.input.repeat(batch, 1, 1, 1)

        return out

class StyledConv(nn.Module):
    def __init__(
        self,
        in_channel,
        out_channel,
        kernel_size,
        style_dim,
        upsample=False,
        blur_kernel=[1, 3, 3, 1],
        demodulate=True,
    ):
        super().__init__()

        self.conv = ModulatedConv2d(
            in_channel,
            out_channel,
            kernel_size,
            style_dim,
            upsample=upsample,
            blur_kernel=blur_kernel,
            demodulate=demodulate,
        )

        self.noise = NoiseInjection()
        # self.bias = nn.Parameter(torch.zeros(1, out_channel, 1, 1))
        # self.activate = ScaledLeakyReLU(0.2)
        self.activate = FusedLeakyReLU(out_channel)

    def forward(self, input, style, noise=None):
        out = self.conv(input, style)
        out = self.noise(out, noise=noise)
        # out = out + self.bias
        out = self.activate(out)

        return out

class ToRGB(nn.Module):
    def __init__(self, in_channel, style_dim, upsample=True, blur_kernel=[1, 3, 3, 1]):
        super().__init__()

        if upsample:
            self.upsample = Upsample(blur_kernel)

        self.conv = ModulatedConv2d(in_channel, 3, 1, style_dim, demodulate=False)
        self.bias = nn.Parameter(torch.zeros(1, 3, 1, 1))

    def forward(self, input, style, skip=None):
        out = self.conv(input, style)
        out = out + self.bias

        if skip is not None:
            skip = self.upsample(skip)

            out = out + skip

        return out

class Generator(nn.Module):
    def __init__(
        self,
        size,
        style_dim,
        n_mlp,
        channel_multiplier=2,
        blur_kernel=[1, 3, 3, 1],
        lr_mlp=0.01,
    ):
        super().__init__()

        self.size = size

        self.style_dim = style_dim
        layers = [PixelNorm()]

        for i in range(n_mlp):
            layers.append(
                EqualLinear(
                    style_dim, style_dim, lr_mul=lr_mlp, activation="fused_lrelu"
                )
            )

        self.style = nn.Sequential(*layers)

        self.channels = {
            4: 512,
            8: 512,
            16: 512,
            32: 512,
            64: 256 * channel_multiplier,
            128: 128 * channel_multiplier,
            256: 64 * channel_multiplier,
            512: 32 * channel_multiplier,
            1024: 16 * channel_multiplier,
        }

        self.input = ConstantInput(self.channels[4])
        self.conv1 = StyledConv(
            self.channels[4], self.channels[4], 3, style_dim, blur_kernel=blur_kernel
        )
        self.to_rgb1 = ToRGB(self.channels[4], style_dim, upsample=False)

        self.log_size = int(math.log(size, 2))
        self.num_layers = (self.log_size - 2) * 2 + 1

        self.convs = nn.ModuleList()
        self.upsamples = nn.ModuleList()
        self.to_rgbs = nn.ModuleList()
        self.noises = nn.Module()

        in_channel = self.channels[4]

        for layer_idx in range(self.num_layers):
            res = (layer_idx + 5) // 2
            shape = [1, 1, 2 ** res, 2 ** res]
            self.noises.register_buffer(f"noise_{layer_idx}", torch.randn(*shape))

        for i in range(3, self.log_size + 1):
            out_channel = self.channels[2 ** i]

            self.convs.append(
                StyledConv(
                    in_channel,
                    out_channel,
                    3,
                    style_dim,
                    upsample=True,
                    blur_kernel=blur_kernel,
                )
            )

            self.convs.append(
                StyledConv(
                    out_channel, out_channel, 3, style_dim, blur_kernel=blur_kernel
                )
            )

            self.to_rgbs.append(ToRGB(out_channel, style_dim))

            in_channel = out_channel

        self.n_latent = self.log_size * 2 - 2

    def make_noise(self):
        device = self.input.input.device

        noises = [torch.randn(1, 1, 2 ** 2, 2 ** 2, device=device)]

        for i in range(3, self.log_size + 1):
            for _ in range(2):
                noises.append(torch.randn(1, 1, 2 ** i, 2 ** i, device=device))

        return noises

    def mean_latent(self, n_latent):
        latent_in = torch.randn(
            n_latent, self.style_dim, device=self.input.input.device
        )
        latent = self.style(latent_in).mean(0, keepdim=True)

        return latent

    def get_latent(self, input):
        return self.style(input)

    def forward(
        self,
        styles,
        return_latents=False,
        inject_index=None,
        truncation=1,
        truncation_latent=None,
        input_is_latent=False,
        noise=None,
        randomize_noise=True,
    ):
        if not input_is_latent:
            styles = [self.style(s) for s in styles]

        if noise is None:
            if randomize_noise:
                noise = [None] * self.num_layers
            else:
                noise = [
                    getattr(self.noises, f"noise_{i}") for i in range(self.num_layers)
                ]

        if truncation < 1:
            style_t = []

            for style in styles:
                style_t.append(
                    truncation_latent + truncation * (style - truncation_latent)
                )

            styles = style_t

        if len(styles) < 2:
            inject_index = self.n_latent

            if styles[0].ndim < 3:
                latent = styles[0].unsqueeze(1).repeat(1, inject_index, 1)

            else:
                latent = styles[0]

        else:
            if inject_index is None:
                inject_index = random.randint(1, self.n_latent - 1)

            latent = styles[0].unsqueeze(1).repeat(1, inject_index, 1)
            latent2 = styles[1].unsqueeze(1).repeat(1, self.n_latent - inject_index, 1)

            latent = torch.cat([latent, latent2], 1)

        out = self.input(latent)
        out = self.conv1(out, latent[:, 0], noise=noise[0])

        skip = self.to_rgb1(out, latent[:, 1])

        i = 1
        for conv1, conv2, noise1, noise2, to_rgb in zip(
            self.convs[::2], self.convs[1::2], noise[1::2], noise[2::2], self.to_rgbs
        ):
            out = conv1(out, latent[:, i], noise=noise1)
            out = conv2(out, latent[:, i + 1], noise=noise2)
            skip = to_rgb(out, latent[:, i + 2], skip)

            i += 2

        image = skip

        if return_latents:
            return image, latent

        else:
            return image, None

class ConvLayer(nn.Sequential):
    def __init__(
        self,
        in_channel,
        out_channel,
        kernel_size,
        downsample=False,
        blur_kernel=[1, 3, 3, 1],
        bias=True,
        activate=True,
    ):
        layers = []

        if downsample:
            factor = 2
            p = (len(blur_kernel) - factor) + (kernel_size - 1)
            pad0 = (p + 1) // 2
            pad1 = p // 2

            layers.append(Blur(blur_kernel, pad=(pad0, pad1)))

            stride = 2
            self.padding = 0

        else:
            stride = 1
            self.padding = kernel_size // 2

        layers.append(
            EqualConv2d(
                in_channel,
                out_channel,
                kernel_size,
                padding=self.padding,
                stride=stride,
                bias=bias and not activate,
            )
        )

        if activate:
            layers.append(FusedLeakyReLU(out_channel, bias=bias))

        super().__init__(*layers)

class ResBlock(nn.Module):
    def __init__(self, in_channel, out_channel, blur_kernel=[1, 3, 3, 1]):
        super().__init__()

        self.conv1 = ConvLayer(in_channel, in_channel, 3)
        self.conv2 = ConvLayer(in_channel, out_channel, 3, downsample=True)

        self.skip = ConvLayer(
            in_channel, out_channel, 1, downsample=True, activate=False, bias=False
        )

    def forward(self, input):
        out = self.conv1(input)
        out = self.conv2(out)

        skip = self.skip(input)
        out = (out + skip) / math.sqrt(2)

        return out

class Discriminator(nn.Module):
    def __init__(self, size, channel_multiplier=2, blur_kernel=[1, 3, 3, 1]):
        super().__init__()

        channels = {
            4: 512,
            8: 512,
            16: 512,
            32: 512,
            64: 256 * channel_multiplier,
            128: 128 * channel_multiplier,
            256: 64 * channel_multiplier,
            512: 32 * channel_multiplier,
            1024: 16 * channel_multiplier,
        }

        # TODO
        # should become an argument later on
        self.ef_dim = 256
        self.img_size = 256

        convs = [ConvLayer(3 + self.ef_dim, channels[size], 1)]

        log_size = int(math.log(size, 2))

        in_channel = channels[size]

        for i in range(log_size, 2, -1):
            out_channel = channels[2 ** (i - 1)]

            convs.append(ResBlock(in_channel, out_channel, blur_kernel))

            in_channel = out_channel

        self.convs = nn.Sequential(*convs)

        self.stddev_group = 4
        self.stddev_feat = 1

        self.final_conv = ConvLayer(in_channel + 1, channels[4], 3)
        self.final_linear = nn.Sequential(
            EqualLinear(channels[4] * 4 * 4, channels[4], activation="fused_lrelu"),
            EqualLinear(channels[4], 1),
        )
        self.sigmoid_layer = nn.Sigmoid()

    def forward(self, input, c_code=None):
        if c_code is not None:
            c_code = c_code.view(-1, self.ef_dim, 1, 1)
            c_code = c_code.repeat(1, 1, self.img_size, self.img_size)
            input = torch.cat((input, c_code), 1)
        out = self.convs(input)

        batch, channel, height, width = out.shape
        group = min(batch, self.stddev_group)

        stddev = out.view(
            group, -1, self.stddev_feat, channel // self.stddev_feat, height, width
        )
        stddev = torch.sqrt(stddev.var(0, unbiased=False) + 1e-8)
        stddev = stddev.mean([2, 3, 4], keepdims=True).squeeze(2)
        stddev = stddev.repeat(group, 1, height, width)
        out = torch.cat([out, stddev], 1)

        out = self.final_conv(out)

        out = out.view(batch, -1)
        out = self.final_linear(out)
        logits = self.sigmoid_layer(out)
        return out, logits

class RNN_ENCODER(nn.Module):
    def __init__(
        self,
        ntoken,
        ninput=300,
        drop_prob=0.5,
        nhidden=128,
        nlayers=1,
        bidirectional=True,
    ):
        super(RNN_ENCODER, self).__init__()
        self.n_steps = cfg.TEXT.WORDS_NUM
        self.ntoken = ntoken  # size of the dictionary
        self.ninput = ninput  # size of each embedding vector
        self.drop_prob = drop_prob  # probability of an element to be zeroed
        self.nlayers = nlayers  # Number of recurrent layers
        self.bidirectional = bidirectional
        self.rnn_type = cfg.RNN_TYPE
        if bidirectional:
            self.num_directions = 2
        else:
            self.num_directions = 1
        # number of features in the hidden state
        self.nhidden = nhidden // self.num_directions

        self.define_module()
        self.init_weights()

    def define_module(self):
        self.encoder = nn.Embedding(self.ntoken, self.ninput)
        self.drop = nn.Dropout(self.drop_prob)
        if self.rnn_type == "LSTM":
            # dropout: If non-zero, introduces a dropout layer on
            # the outputs of each RNN layer except the last layer
            self.rnn = nn.LSTM(
                self.ninput,
                self.nhidden,
                self.nlayers,
                batch_first=True,
                dropout=self.drop_prob,
                bidirectional=self.bidirectional,
            )
        elif self.rnn_type == "GRU":
            self.rnn = nn.GRU(
                self.ninput,
                self.nhidden,
                self.nlayers,
                batch_first=True,
                dropout=self.drop_prob,
                bidirectional=self.bidirectional,
            )
        else:
            raise NotImplementedError

    def init_weights(self):
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange, initrange)
        # Do not need to initialize RNN parameters, which have been initialized
        # http://pytorch.org/docs/master/_modules/torch/nn/modules/rnn.html#LSTM
        # self.decoder.weight.data.uniform_(-initrange, initrange)
        # self.decoder.bias.data.fill_(0)

    def init_hidden(self, bsz):
        device = "cuda:1"
        weight = next(self.parameters()).data
        if self.rnn_type == "LSTM":
            return (
                Variable(
                    weight.new(
                        self.nlayers * self.num_directions, bsz, self.nhidden
                    ).zero_()
                ).cuda(),
                Variable(
                    weight.new(
                        self.nlayers * self.num_directions, bsz, self.nhidden
                    ).zero_()
                ).cuda(),
            )
        else:
            return Variable(
                weight.new(
                    self.nlayers * self.num_directions, bsz, self.nhidden
                ).zero_()
            ).cuda()

    def forward(self, captions, cap_lens, hidden, mask=None):
        # input: torch.LongTensor of size batch x n_steps
        # --> emb: batch x n_steps x ninput
        emb = self.drop(self.encoder(captions))
        #
        # Returns: a PackedSequence object
        cap_lens = cap_lens.data.tolist()
        emb = pack_padded_sequence(emb, cap_lens, batch_first=True)
        # #hidden and memory (num_layers * num_directions, batch, hidden_size):
        # tensor containing the initial hidden state for each element in batch.
        # #output (batch, seq_len, hidden_size * num_directions)
        # #or a PackedSequence object:
        # tensor containing output features (h_t) from the last layer of RNN
        output, hidden = self.rnn(emb, hidden)
        # PackedSequence object
        # --> (batch, seq_len, hidden_size * num_directions)
        output = pad_packed_sequence(output, batch_first=True)[0]
        # output = self.drop(output)
        # --> batch x hidden_size*num_directions x seq_len
        words_emb = output.transpose(1, 2)
        # --> batch x num_directions*hidden_size
        if self.rnn_type == "LSTM":
            sent_emb = hidden[0].transpose(0, 1).contiguous()
        else:
            sent_emb = hidden.transpose(0, 1).contiguous()
        sent_emb = sent_emb.view(-1, self.nhidden * self.num_directions)
        return words_emb, sent_emb

# CNN Encoder
def conv1x1(in_planes, out_planes, bias=False):
    "1x1 convolution with padding"
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=1,
                     padding=0, bias=bias)

class CNN_ENCODER(nn.Module):
    def __init__(self, nef):
        super(CNN_ENCODER, self).__init__()
        if cfg.TRAIN.FLAG:
            self.nef = nef
        else:
            self.nef = 256  # define a uniform ranker

        model = models.inception_v3()
        url = 'https://download.pytorch.org/models/inception_v3_google-1a9a5a14.pth'
        model.load_state_dict(model_zoo.load_url(url))
        for param in model.parameters():
            param.requires_grad = False
        print('Load pretrained model from ', url)
        # print(model)

        self.define_module(model)
        self.init_trainable_weights()

    def define_module(self, model):
        self.Conv2d_1a_3x3 = model.Conv2d_1a_3x3
        self.Conv2d_2a_3x3 = model.Conv2d_2a_3x3
        self.Conv2d_2b_3x3 = model.Conv2d_2b_3x3
        self.Conv2d_3b_1x1 = model.Conv2d_3b_1x1
        self.Conv2d_4a_3x3 = model.Conv2d_4a_3x3
        self.Mixed_5b = model.Mixed_5b
        self.Mixed_5c = model.Mixed_5c
        self.Mixed_5d = model.Mixed_5d
        self.Mixed_6a = model.Mixed_6a
        self.Mixed_6b = model.Mixed_6b
        self.Mixed_6c = model.Mixed_6c
        self.Mixed_6d = model.Mixed_6d
        self.Mixed_6e = model.Mixed_6e
        self.Mixed_7a = model.Mixed_7a
        self.Mixed_7b = model.Mixed_7b
        self.Mixed_7c = model.Mixed_7c

        self.emb_features = conv1x1(768, self.nef)
        self.emb_cnn_code = nn.Linear(2048, self.nef)

    def init_trainable_weights(self):
        initrange = 0.1
        self.emb_features.weight.data.uniform_(-initrange, initrange)
        self.emb_cnn_code.weight.data.uniform_(-initrange, initrange)

    def forward(self, x):
        features = None
        # --> fixed-size input: batch x 3 x 299 x 299
        x = nn.functional.interpolate(x,size=(299, 299), mode='bilinear', align_corners=False)
        # 299 x 299 x 3
        x = self.Conv2d_1a_3x3(x)
        # 149 x 149 x 32
        x = self.Conv2d_2a_3x3(x)
        # 147 x 147 x 32
        x = self.Conv2d_2b_3x3(x)
        # 147 x 147 x 64
        x = F.max_pool2d(x, kernel_size=3, stride=2)
        # 73 x 73 x 64
        x = self.Conv2d_3b_1x1(x)
        # 73 x 73 x 80
        x = self.Conv2d_4a_3x3(x)
        # 71 x 71 x 192

        x = F.max_pool2d(x, kernel_size=3, stride=2)
        # 35 x 35 x 192
        x = self.Mixed_5b(x)
        # 35 x 35 x 256
        x = self.Mixed_5c(x)
        # 35 x 35 x 288
        x = self.Mixed_5d(x)
        # 35 x 35 x 288

        x = self.Mixed_6a(x)
        # 17 x 17 x 768
        x = self.Mixed_6b(x)
        # 17 x 17 x 768
        x = self.Mixed_6c(x)
        # 17 x 17 x 768
        x = self.Mixed_6d(x)
        # 17 x 17 x 768
        x = self.Mixed_6e(x)
        # 17 x 17 x 768

        # image region features
        features = x
        # 17 x 17 x 768

        x = self.Mixed_7a(x)
        # 8 x 8 x 1280
        x = self.Mixed_7b(x)
        # 8 x 8 x 2048
        x = self.Mixed_7c(x)
        # 8 x 8 x 2048
        x = F.avg_pool2d(x, kernel_size=8)
        # 1 x 1 x 2048
        # x = F.dropout(x, training=self.training)
        # 1 x 1 x 2048
        x = x.view(x.size(0), -1)
        # 2048

        # global image features
        cnn_code = self.emb_cnn_code(x)
        # 512
        if features is not None:
            features = self.emb_features(features)

        return features, cnn_code


import pygame
from node import *
from point import Point

# COLORS
EMPTY_COLOR = (255, 255, 255)
DESTINATION = (255, 255, 0) # Yellow
WALL_COLOR = (0, 0, 0) # Black
BACKGROUND_COLOR = (100, 100, 100)

PINK = (255, 102, 255)
BLUE = (102, 255, 255)
PATH_COLOR = (255, 255, 0) # Yellow

# SIZES
# This sets the WIDTH and HEIGHT of each grid location
WIDTH = 20
HEIGHT = 20

# This sets the margin between each cell
MARGIN = 5

GRID_SIZE = 20
# Set the HEIGHT and WIDTH of the screen
WINDOW_SIZE = [505, 505]

# Key points

# Loop until the user clicks the close button.
done = False

# A star pathfinding
openlist = []
closedlist = []

# Create a 2 dimensional array. A two dimensional
# array is simply a list of lists.
grid = []
for row in range(GRID_SIZE):
    # Add an empty array that will hold each cell
    # in this row
    grid.append([])
    for col in range(GRID_SIZE):
        # grid[row].append(0)  # Append a cell
        grid[row].append(Node(True, Point(row, col)))

# Set start and end points

grid[5][5].type = NodeType.START
start = grid[5][5]
grid[10][10].type = NodeType.END
end = grid[10][10]

for row in range(GRID_SIZE):
    for col in range(GRID_SIZE):
        if grid[row][col].type == NodeType.EMPTY:
            grid[row][col].calc_values(start.position, end.position)

# Initialize pygame
pygame.init()
screen = pygame.display.set_mode(WINDOW_SIZE)

# Set title of screen
pygame.display.set_caption("Array Backed Grid")

# Used to manage how fast the screen updates
clock = pygame.time.Clock()

# -------- Main Program Loop -----------
while not done:
    for event in pygame.event.get():  # User did something
        if event.type == pygame.QUIT:  # If user clicked close
            done = True  # Flag that we are done so we exit this loop
        elif event.type == pygame.MOUSEBUTTONDOWN:
            # User clicks the mouse. Get the position
            pos = pygame.mouse.get_pos()
            # Change the x/y screen coordinates to grid coordinates
            col = pos[0] // (WIDTH + MARGIN)
            row = pos[1] // (HEIGHT + MARGIN)
            # Set that location to one
            # grid[row][col].walkable = True
            # pos is the mouse coordinates, row col is the grid coordinates
            print("Click: ", pos, "({0}, {1})".format(row, col), "Type: ", grid[row][col].type)
            grid[row][col].display_cost()

    # Set the screen background
    screen.fill(BACKGROUND_COLOR)

    # Draw the grid
    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):

            tile = grid[row][col].type

            if tile == NodeType.EMPTY:
                color = EMPTY_COLOR

            elif tile == NodeType.WALL:
                color = WALL_COLOR

            elif tile == NodeType.START or tile == NodeType.END:
                color = DESTINATION

            pygame.draw.rect(screen,
                             color,
                             [(MARGIN + WIDTH) * col + MARGIN,
                              (MARGIN + HEIGHT) * row + MARGIN,
                              WIDTH,
                              HEIGHT])

    # Limit to 60 frames per second
    clock.tick(60)

    # Go ahead and update the screen with what we've drawn.
    pygame.display.flip()

# Be IDLE friendly. If you forget this line, the program will 'hang'
# on exit.
pygame.quit()


from rest_framework import serializers
from slackchat.models import Reaction

class ReactionSerializer(serializers.ModelSerializer):
    user = serializers.SerializerMethodField()

    def get_user(self, obj):
        return obj.user.api_id

    class Meta:
        model = Reaction
        fields = (
            'timestamp',
            'reaction',
            'user'
        )


from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from qcportal.optimization import OptimizationDataset
from qcportal.record_models import PriorityEnum
from .testing_helpers import load_test_data

if TYPE_CHECKING:
    from qcportal import PortalClient

@pytest.fixture(scope="function")
def optimization_ds(submitter_client: PortalClient):
    ds = submitter_client.add_dataset(
        "optimization",
        "Test dataset",
        "Test Description",
        "a Tagline",
        ["tag1", "tag2"],
        "new_group",
        {"prov_key_1": "prov_value_1"},
        True,
        "def_tag",
        PriorityEnum.low,
        {"meta_key_1": "meta_value_1"},
        "group1",
    )

    assert ds.owner_user is not None
    assert ds.owner_user == ds._client.username
    assert ds.owner_group == "group1"

    yield ds

@pytest.mark.parametrize("find_existing", [True, False])
def test_optimization_dataset_client_submit(optimization_ds: OptimizationDataset, find_existing: bool):
    input_spec_1, molecule_1, _ = load_test_data("opt_psi4_benzene")

    optimization_ds.add_entry(name="test_molecule", initial_molecule=molecule_1)
    optimization_ds.add_specification("test_spec", input_spec_1, "test_specification")

    optimization_ds.submit()
    assert optimization_ds.status()["test_spec"]["waiting"] == 1

    optimization_ds.submit()
    assert optimization_ds.status()["test_spec"]["waiting"] == 1

    # Should only be one record
    record_id = 0
    for e, s, r in optimization_ds.iterate_records():
        assert r.owner_user == optimization_ds.owner_user
        assert r.owner_group == optimization_ds.owner_group
        record_id = r.id

    # delete & re-add entry, then resubmit
    optimization_ds.delete_entries(["test_molecule"])
    assert optimization_ds.status() == {}

    # record still on the server?
    r = optimization_ds._client.get_records(record_id)
    assert r.owner_user == optimization_ds.owner_user

    # now resubmit
    optimization_ds.add_entry(name="test_molecule", initial_molecule=molecule_1)
    optimization_ds.submit(find_existing=find_existing)
    assert optimization_ds.status()["test_spec"]["waiting"] == 1

    for e, s, r in optimization_ds.iterate_records():
        assert r.owner_user == optimization_ds.owner_user
        assert r.owner_group == optimization_ds.owner_group

        if find_existing:
            assert r.id == record_id
        else:
            assert r.id != record_id


import logging
from multiprocessing import Process, Queue, Value

def log_debug(module_name, message):
    global logging_queue
    logging_queue.put({'level': 0, 'module_name': module_name, 'message': message})

def log_info(module_name, message):
    global logging_queue
    logging_queue.put({'level': 1, 'module_name': module_name, 'message': message})

def log_warning(module_name, message):
    global logging_queue
    logging_queue.put({'level': 2, 'module_name': module_name, 'message': message})

def log_error(module_name, message):
    global logging_queue
    logging_queue.put({'level': 3, 'module_name': module_name, 'message': message})

def log_critical(module_name, message):
    global logging_queue
    logging_queue.put({'level': 4, 'module_name': module_name, 'message': message})

def log_start(level):
    global logger
    global logging_queue
    global logger_running
    global logger_process

    FORMAT = '%(asctime)-15s [%(level)s][%(module_name)s] %(message)s'
    logging.basicConfig(format=FORMAT)
    logger = logging.getLogger('pdf-pt-learner')
    logger.setLevel(level)
    logging_queue = Queue()
    logger_running = Value('b', True)

    logger_process = Process(target=log_worker)
    logger_process.daemon = True
    logger_process.start()

def log_stop():
    global logger_running
    global logger_process

    logger_running.value = False
    logger_process.join()

def log_worker():
    global logger
    global logger_running
    global logging_queue

    while True:
        try:
            msg = logging_queue.get(True, 5)
        except:
            if logger_running.value:
                continue
            else:
                return
        if msg['level'] == 0:
            logger.debug(msg['message'], extra={'level': 'D', 'module_name': msg['module_name']})
        elif msg['level'] == 1:
            logger.info(msg['message'], extra={'level': 'I', 'module_name': msg['module_name']})
        elif msg['level'] == 2:
            logger.warning(msg['message'], extra={'level': 'W', 'module_name': msg['module_name']})
        elif msg['level'] == 3:
            logger.error(msg['message'], extra={'level': 'E', 'module_name': msg['module_name']})
        elif msg['level'] == 4:
            logger.critical(msg['message'], extra={'level': 'C', 'module_name': msg['module_name']})

def test_logging():
    name = 'Tester'

    log_start(logging.DEBUG)

    log_debug(name, 'This is a debug message')
    log_info(name, 'This is an info message')
    log_warning(name, 'This is a warning message')
    log_error(name, 'This is an error message')
    log_critical(name, 'This is a critical message')

    log_stop()

if __name__ == '__main__':
    test_logging()


import re
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from urllib.parse import urlparse
from bs4 import BeautifulSoup
from .ntlm import ntlmdecode
from rich.console import Console
from rich.table import Table
import pkg_resources

# Dealing with SSL Warnings
try:
    import requests.packages.urllib3

    requests.packages.urllib3.disable_warnings()
except Exception:
    pass

def requests_retry_session(
    retries=1,
    backoff_factor=0.3,
    status_forcelist=(500, 502, 504),
    session=None,
):
    session = session or requests.Session()
    retry = Retry(
        total=retries,
        read=retries,
        connect=retries,
        backoff_factor=backoff_factor,
        status_forcelist=status_forcelist,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

# Using to find ADFS endpoints
def adfs_find(target):

    # Reading in potential subdomains
    resource = pkg_resources.resource_filename(__name__, "subs.txt")
    sd = [line.strip() for line in open(resource)]

    for i in sd:

        # Crafting our potential URL
        url = f"https://{i}.{target}/FederationMetadata/2007-06/FederationMetadata.xml"

        try:
            # Issuing request to URL
            response = requests_retry_session().get(
                url, timeout=1, allow_redirects=False, verify=False
            )

        except requests.exceptions.RequestException:
            pass

        else:
            # Doing checks to make sure we get SAML XML data back from the endpoint
            if "Content-Type" in response.headers:
                if response.headers["Content-Type"] == "application/samlmetadata+xml":
                    if response.status_code == 200:

                        # If everything checks out, parse out path and return the URL
                        url = f"https://{urlparse(url).hostname}"
                        return url

# Getting the version (year) displayed on the login page
def adfs_find_version(adfs_endpoint):

    # Crafting our URL
    url = f"{adfs_endpoint}/adfs/ls/?wa=wsignin1.0"

    try:
        # Issuing request to URL
        response = requests_retry_session().get(
            url, timeout=1, allow_redirects=False, verify=False
        )

    except requests.exceptions.RequestException:
        pass

    else:

        # Making sure everything checks out
        if response.status_code == 200:
            try:

                # Parsing the page content
                soup = BeautifulSoup(response.text, "html.parser")

                # Extracting raw value from copyright field
                version_raw = soup.find("span", {"id": "copyright"}).get_text()

            # If something happens, just return UNKNOWN
            except Exception:
                version = "UNKOWN"
                return version

            else:

                # Pulling everything besides the year
                version = version_raw.strip("© ").strip("Microsoft")
                return version

# Looking for federated services associated with endpoints
# May not work depending of configuration
def adfs_find_services(adfs_endpoint):

    # Crafting our URL
    url = f"{adfs_endpoint}/adfs/ls/idpinitiatedsignon.aspx"
    services = []

    try:

        # Issuing request
        response = requests_retry_session().get(
            url, timeout=1, allow_redirects=False, verify=False
        )

    except requests.exceptions.RequestException:
        pass

    else:

        # Making sure we got something back
        if response.status_code == 200:
            try:

                # Parsing the response
                soup = BeautifulSoup(response.text, "html.parser")

                # First parsing out if the information we are looking for is even availible
                check_services = soup.find(
                    "div", {"id": "idp_SignInThisSiteStatusLabel"}
                ).get_text()

            except AttributeError:
                services.append("Not able to enumerate services.")
                return services

            else:

                # If sign in is required to list federated services, return an "error" message to the array
                if "Sign in to this site." in check_services:
                    services.append("Not able to enumerate services.")
                    return services

                else:

                    # If we can read the list, pull everything from the dropdown menu
                    service = soup.find("select", {"name": "RelyingParty"})

                    # Parse out all listed federated services
                    for i in service.find_all("option"):

                        # Add services to our earlier defined array
                        services.append(i.text)

                    return services

# Function to check if the ADFS self service password reset endpoint is availible
def find_adfs_pwreset(adfs_endpoint):

    # Crafint our URL
    url = f"{adfs_endpoint}/adfs/portal/updatepassword"

    try:
        # Issuing request
        response = requests_retry_session().get(
            url, timeout=1, allow_redirects=False, verify=False
        )

    except requests.exceptions.RequestException:
        pass

    else:

        # Checking that we got something back and the page didn't return an error
        if response.status_code == 200 and "Update Password" in response.text:
            return True
        else:
            return False

def adfs_ntlm_pathfind(adfs_endpoint):

    # Defining potential NTLM authentication paths
    endpoints = [
        "/adfs/services/trust/2005/windowstransport",
        "/adfs/services/trust/2005/usernamemixed",
        "/adfs/services/trust/13/usernamemixed",
        "/adfs/services/trust/13/windowstransport",
    ]

    # Defining an empty array to add endpoints to
    valid_endpoints = []

    # Issue a request to each potential endpoint
    for e in endpoints:
        try:

            # Crafint our URL and issuing request
            url = f"{adfs_endpoint}{e}"
            response = requests_retry_session().get(
                url, timeout=1, allow_redirects=False, verify=False
            )

        except requests.exceptions.RequestException:
            pass

        else:

            # If we got a 401, NTLM auth is there
            if (
                response.status_code == 401
                and "NTLM" in response.headers["WWW-Authenticate"]
            ):
                valid_endpoints.append(url)

    return valid_endpoints

# Parsing data from found NTLM authentication endpoints
def adfs_ntlm_parse(adfs_ntlm_paths):

    try:
        ntlm_header = {
            "Authorization": "NTLM TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw=="
        }
        response = requests_retry_session().post(
            adfs_ntlm_paths[0], headers=ntlm_header, verify=False, allow_redirects=True
        )
    except requests.exceptions.RequestException:
        pass

    try:

        # Parsing what we need
        if (
            response.status_code == 401
            and "NTLM" in response.headers["WWW-Authenticate"]
        ):
            ntlm_info = ntlmdecode(response.headers["WWW-Authenticate"])
            ntlm_data = ntlm_info["NetBIOS_Domain_Name"]
            # ntlm_data.append(ntlm_info["NetBIOS_Domain_Name"])
            # ntlm_data.append(ntlm_info["FQDN"])
            # ntlm_data.append(ntlm_info["DNS_Domain_name"])
            return ntlm_data

    # Bad error handling
    except Exception as a:
        print(f"Error occured: {a}")

def adfs_display(
    adfs_endpoint,
    adfs_version,
    adfs_services,
    adfs_pwreset,
    adfs_ntlm_paths,
    adfs_ntlm_data,
):
    console = Console()
    table_adfs = Table(show_header=False, pad_edge=True)
    table_adfs.add_column("Context")
    table_adfs.add_column("Info")

    table_adfs.add_row("URL", f"{adfs_endpoint}")
    table_adfs.add_row("VERSION", f"{adfs_version}")

    table_adfs.add_row("SSPWR", f"{adfs_pwreset}")

    if len(adfs_ntlm_paths) != 0:
        paths = "\n".join(item for item in adfs_ntlm_paths)
        table_adfs.add_row("URLS", f"{paths}")

    if adfs_ntlm_data is not None:
        table_adfs.add_row("DOMAIN", f"{adfs_ntlm_data}")

    if len(adfs_services) > 10:
        print("Service list > 10")
        print("Y to display all.")
        print("N to display 10.")
        reply = str(input("(Y/n): ")).lower().strip()
        if reply[:1] == "y" or reply[:1] == "":
            services = "\n".join(item for item in adfs_services)
            table_adfs.add_row("SERVICES", f"{services}")
        elif reply[:1] == "n":
            services = "\n".join(item for item in adfs_services[:10])
            table_adfs.add_row("SERVICES", f"{services}")
    else:
        services = "\n".join(item for item in adfs_services)
        table_adfs.add_row("SERVICES", f"{services}")

    console.print(table_adfs)


from __future__ import annotations

import json
import pathlib
import re
import sys

from functools import reduce
from itertools import product
from strictyaml import load, Map, MapPattern, Str, Seq, YAML, YAMLError, YAMLValidationError
from typing import Any, Pattern

from modules.input import UserInput
from modules.stats import Stats
from modules.titletools import Regex
from modules.utils import download, eprint, Font, printwrap

class Config:
    def __init__(self,
                 clone_list_metadata_download_location: str,
                 clone_list_metadata_download_location_key: str,
                 program_download_location: str,
                 program_download_location_key: str,
                 config_file: str,
                 ignore_tags_key: str,
                 disc_rename_key: str,
                 promote_editions_key: str,
                 demote_editions_key: str,
                 modern_editions_key: str,
                 languages_key: str,
                 region_order_key: str,
                 video_order_key: str,
                 clone_lists_key: str,
                 metadata_key: str,
                 user_config_key: str,
                 user_language_order_key: str,
                 user_region_order_key: str,
                 user_video_order_key: str,
                 user_list_prefix_key: str,
                 user_list_suffix_key: str,
                 user_override_exclude_key: str,
                 user_override_include_key: str,
                 user_filter_key: str,
                 user_gui_settings_key: str,
                 system_settings_path: str,
                 sanitized_characters: tuple[str, ...],
                 reserved_filenames: tuple[str, ...],
                 version_major: str,
                 version_minor: str,
                 user_input: Any,
                 first_run_gui: bool = False) -> None:
        """ Creates an object that contains internal and user config data. Interactively
        browsable if you `print()` the object.

        Args:
            - `clone_list_metadata_download_location (str)` A URL that points to a folder
              containing clone lists and metadata files, so local copies can be updated.
              Only used if internal-config.json is missing.

            - `clone_list_metadata_download_location_key (str)` The key in
              internal-config.json that contains the URL that hosts clone lists and
              metadata files, so local copies can be updated.

            - `program_download_location (str)` A URL that points to where the Retool
              program can be downloaded. Not used, but if it was, it would only be used if
              internal-config.json was missing.

            - `program_download_location_key (str)` The key in internal-config.json that
              contains the URL that hosts the Retool program. Not used.

            - `config_file (str)` The location of internal-config.json.

            - `ignore_tags_key (str)` The key to look up in internal-config.json that
              contains tags that should be ignored so like titles can be grouped together.

            - `disc_rename_key (str)` The key in internal-config.json that specifies how
              multiple '(Disc)' tags should be renamed so they can be normalized and
              compared.

            - `promote_editions_key (str)` The key in internal-config.json that specifies
              which tags indicate a title should be promoted over another.

            - `demote_editions_key (str)` The key in internal-config.json that specifies
              which tags indicate a title should be demoted below another.

            - `modern_editions_key (str)` The key in internal-config.json that specifies
              which tags indicate a title is ripped from a modern release of a game, like
              those from Virtual Console.

            - `languages_key (str)` The key in internal-config.json that specifies the
              available languages and their associated language codes.

            - `region_order_key (str)` The key in internal-config.json that specifies the
              default region order and the implied language for each region.

            - `video_order_key (str)` The key in internal-config.json that specifies the
              default order for video standards like MPAL, NTSC, PAL, PAL 60Hz, and SECAM.

            - `clone_lists_key (str)` The key in internal-config.json that specifies where
              the local clone lists are located.

            - `metadata_key (str)` The key in internal-config.json that specifies where the
              local metadata files are located.

            - `user_config_key (str)` The key in internal-config.json that specifies where
              user-config.yaml is located.

            - `user_language_key (str)` The key in user-config.yaml that specifies the
              language order as defined by the user.

            - `user_region_order_key (str)` The key in user-config.yaml that specifies the
              region order as defined by the user.

            - `user_video_order_key (str)` The key in user-config.yaml that specifies the
              order for video standards like MPAL, NTSC, PAL, PAL 60Hz, and SECAM as defined
              by the user.

            - `user_list_prefix_key (str)` The key in user-config.yaml that specifies the
              prefix used when the user specifies `--listnames`.

            - `user_list_suffix_key (str)` The key in user-config.yaml that specifies the
              suffix used when the user specifies `--listnames`.

            - `user_gui_settings_key (str)` They key in user-config.yaml that specifies
              settings used by the GUI.

            - `system_settings_path (str)` The location of the system config files.

            - `sanitized_characters (tuple[str, ...])` Characters that can't be used in
              filenames.

            - `reserved_filenames (tuple[str, ...])` Filenames that can't be used in
              certain operating systems.

            - `version_major (str)` The major version of Retool. Combined with the minor
              version to make up the full version string.

            - `version_minor (str)` The minor version of Retool. Combined with the major
              version to make up the full version string.

            - `user_input (Any)` In the CLI version, all the arguments passed in by the user.
              In the GUI version, constructed from UI elements the user has enabled or
              interacted with.

            - `first_run_gui (bool, optional)` When using the GUI, is set to `True` to
              prevent Retool from prompting the user through the CLI to generate
              user-config.yaml, as this happens automatically through GUI use anyway.
              Defaults to `False`.
        """

        # Determine if STDOUT is being redirected or not
        self.stdout = False

        if not sys.stdout.isatty():
            self.stdout = True

        # Create the stats object
        self.stats = Stats()

        # Download the internal config file if it's missing
        def download_required_files(download_files: tuple[str, str]) -> None:
            """ Downloads the files Retool requires to operate.

            Args:
                - `download_files (tuple[str])` A tuple of the files to download.
            """

            required_files: str = ''.join([f'\n* {Font.bold}{x}{Font.warning}' for x in download_files])
            download_config: str = ''
            missing_file: bool = False

            for download_file in download_files:
                if not pathlib.Path(download_file).is_file():
                    missing_file = True

            if missing_file:
                while (
                    not download_config
                    or not (
                        download_config == 'y'
                        or download_config == 'n')):
                            printwrap(
                                f'{Font.warning_bold}Warning: {Font.warning} One or more '
                                'of the following files are missing, which Retool needs '
                                'to operate:', 'no_indent')
                            eprint(f'{required_files}')

                            eprint(f'\nWould you like to download them? (y/n) > {Font.end}')

                            download_config = input()

                if download_config.lower() == 'y':
                    eprint('')
                    for download_file in download_files:
                        eprint(f'* Downloading {Font.bold}{download_file}{Font.end}... ', sep=' ', end='', flush=True)
                        failed = download(f'{clone_list_metadata_download_location}/{download_file}', str(pathlib.Path(download_file)))

                        if not failed:
                            eprint('done.')

                    eprint('\n')
                else:
                    eprint('\nExiting...\n')
                    sys.exit()

                # Check that the files are there now for Retool to start
                download_required_files((config_file, 'datafile.dtd'))

        self.config_file: str = config_file

        download_required_files((config_file, 'datafile.dtd'))

        # Import the contents of the internal config file
        self.clone_list_metadata_download_location: str = clone_list_metadata_download_location
        self.program_download_location: str = program_download_location
        self.languages: dict[str, str] = {}
        self.languages_implied: dict[str, tuple[str, ...]] = {}
        self.languages_filter: dict[str, list[str]] = {}
        self.region_order_default: list[str] = []
        self.video_order_default: list[str] = []
        self.tags_ignore: list[str] = []
        self.tags_disc_rename: dict[str, str] = {}
        self.tags_promote_editions: tuple[Pattern[str]]
        self.tags_demote_editions: tuple[Pattern[str]]
        self.tags_modern_editions: tuple[Pattern[str]]
        self.user_config_file: str = ''

        try:
            with open(pathlib.Path(config_file), 'r', encoding='utf-8') as input_file:
                self.config_file_content: dict[str, Any] = json.load(input_file)
        except OSError as e:
            eprint(f'\n{Font.error_bold}* Error: {Font.end}{str(e)}\n')
            raise

        def import_key(key_name: str, dict_key_name: str, iter_type: str = '', tags_ignore: bool = False) -> None:
            """ Takes the data from a specific key in internal-config.json, then assigns
            it to a self.attribute.

            Args:
                - `key_name (str)` The key to find in internal-config.json.

                - `dict_key_name (str)` The attribute to assign to the Retool config
                  object.
                - `iter_type (str, optional)` Whether the resulting attribute value should
                  be a `list`, `tuple`, or neither. Defaults to `''`.

                - `tags_ignore (bool, optional)` Whether the values in the attribute should
                  be added to Retool's ignored tags list. Defaults to `False`.
            """

            if key_name in self.config_file_content:
                if iter_type == 'tuple':
                    setattr(self, dict_key_name, tuple(self.config_file_content[key_name]))
                elif iter_type == 'list':
                    setattr(self, dict_key_name, list(self.config_file_content[key_name]))
                else:
                    setattr(self, dict_key_name, self.config_file_content[key_name])

                if tags_ignore and iter_type != 'list':
                    list(map(self.tags_ignore.append, self.config_file_content[key_name]))

        import_key(languages_key, 'languages')
        import_key(disc_rename_key, 'tags_disc_rename')
        import_key(ignore_tags_key, 'tags_ignore', 'list', tags_ignore=True)
        import_key(promote_editions_key, 'tags_promote_editions', 'tuple', tags_ignore=True)
        import_key(demote_editions_key, 'tags_demote_editions', 'tuple', tags_ignore=True)
        import_key(modern_editions_key, 'tags_modern_editions', 'tuple', tags_ignore=True)
        import_key(user_config_key, 'user_config_file')

        if clone_list_metadata_download_location_key in self.config_file_content:
            self.clone_list_metadata_download_location = self.config_file_content[clone_list_metadata_download_location_key]

        if program_download_location_key in self.config_file_content:
            self.program_download_location = self.config_file_content[program_download_location_key]

        if region_order_key in self.config_file_content:
            self.region_order_default = [region for region in self.config_file_content[region_order_key]]

            # Set up the implied languages
            for key, values in self.config_file_content[region_order_key].items():
                language_filter_list: list[str] = []

                if values['impliedLanguage']:
                    if values['impliedLanguage'] in self.languages:
                        language_filter_list.append(self.languages[values['impliedLanguage']])
                        self.languages_implied[key] = (self.languages[values['impliedLanguage']][:2],)

                self.languages_filter[key] = language_filter_list

        if video_order_key in self.config_file_content:
            self.video_order_default = self.config_file_content[video_order_key]

        if 'localDir' in self.config_file_content[clone_lists_key]:
            self.path_clone_list = self.config_file_content[clone_lists_key]['localDir']

        if 'localDir' in self.config_file_content[metadata_key]:
            self.path_metadata = self.config_file_content[metadata_key]['localDir']

        def key_missing(section_key: str) -> None:
            """ Generates a warning if a specific key is missing from
            internal-config.json.

            Args:
                - `section_key (str)` The section to check for in internal-config.json.
            """

            if section_key not in self.config_file_content:
                printwrap(
                            f'{Font.warning}* The {Font.bold}{section_key}{Font.warning}'
                            f' key is missing from {Font.bold}{config_file}'
                            f'{Font.warning}. Clone matching won\'t be accurate.'
                            f'{Font.end}')

        set(map(lambda s: key_missing(s), [languages_key,
                                            region_order_key,
                                            ignore_tags_key,
                                            disc_rename_key,
                                            promote_editions_key,
                                            demote_editions_key,
                                            modern_editions_key]))

        # No-Intro uses a + sign for 2- and 3-in-1 compilations in their GBA DAT
        # to delineate languages between the first and subsequent titles. For
        # example: (En+En,Fr,De). We need to take this into account when
        # detecting languages in titles. Here we use a Cartesian product of all
        # languages to find these combinations. Thankfully there's only one
        # triple language title so far, so we manually add (En+En+En) instead of
        # searching for all triple language combinations and slowing things
        # down.

        no_intro_languages: list[str] = [x for x in self.languages.values()]
        compilation_languages: list[str] = []

        for language_combo in product(no_intro_languages, repeat=2):
            compilation_languages.append('\+'.join(language_combo))

        no_intro_languages.extend(compilation_languages)
        no_intro_languages.append('En\\+En\\+En')

        LANGUAGES = '|'.join(no_intro_languages)

        # Build out required regular expressions
        self.regex = Regex(LANGUAGES)

        # Generate required files if they're missing
        if user_input.user_config:
            user_config_file = user_input.user_config
        else:
            user_config_file = self.user_config_file

        generate_config(user_config_file,
                        tuple(map(lambda s: s, self.languages)),
                        tuple(self.region_order_default),
                        tuple(self.video_order_default),
                        system_settings_path,
                        first_run_gui=first_run_gui)

        # Import the user config file
        self.region_order_languages_user: list[str] = []
        self.language_order_user: list[str] = []
        self.languages_user_found: bool = False
        self.region_order_user: list[str] = []
        self.video_order_user: list[str] = []
        self.user_prefix: str = ''
        self.user_suffix: str = ''
        self.user_gui_settings: tuple[Any, ...] = ()

        schema = Map(
                {user_language_order_key: Seq(Str())|Str(),
                 user_region_order_key: Seq(Str())|Str(),
                 user_video_order_key: Seq(Str())|Str(),
                 user_list_prefix_key: Seq(Str())|Str(),
                 user_list_suffix_key: Seq(Str())|Str(),
                 user_override_exclude_key: Seq(Str())|Str(),
                 user_override_include_key: Seq(Str())|Str(),
                 user_filter_key: Seq(Str())|Str(),
                 user_gui_settings_key: Seq(Str()|MapPattern(Str(), Str()))|Str()})

        user_config: YAML

        try:
            with open(pathlib.Path(user_config_file), 'r', encoding='utf-8') as user_config_import:
                user_config = load(str(user_config_import.read()), schema)

        except OSError as e:
            eprint(f'\n{Font.error_bold}* Error: {Font.end}{str(e)}\n')
            raise

        except YAMLValidationError as e:
            # Check for the filters key that was added in v2.01.0, and add it if not found
            if '\'filters\' not found' in str(e):
                try:
                    with open(pathlib.Path(user_config_file), 'r', encoding='utf-8') as user_config_import:
                        add_filters_key: list[str] = user_config_import.readlines()
                        add_filters_key.append('\n\nfilters:')

                    with open(pathlib.Path(user_config_file), 'w', encoding='utf-8') as user_config_import:
                        user_config_import.writelines(add_filters_key)
                except Exception as e2:
                    eprint(f'\n{Font.error_bold}* Error: {Font.end}{str(e2)}\n')
                    raise

                try:
                    with open(pathlib.Path(user_config_file), 'r', encoding='utf-8') as user_config_import:
                        user_config = load(str(user_config_import.read()), schema)
                except Exception as e2:
                    eprint(f'\n{Font.error_bold}* Error: {Font.end}{str(e2)}\n')
                    raise
            else:
                eprint(f'\n{Font.error_bold}* YAML validation error: {Font.end}{str(e)}\n')
                raise

        except YAMLError as e:
            eprint(f'\n{Font.error_bold}* YAML error: {Font.end}{str(e)}\n')
            raise

        self.language_order_user = list(user_config.data[user_language_order_key])
        self.region_order_user = list(user_config.data[user_region_order_key])
        self.video_order_user = list(user_config.data[user_video_order_key])
        self.user_prefix = ''.join(user_config.data[user_list_prefix_key])
        self.user_suffix = ''.join(user_config.data[user_list_suffix_key])

        if user_config.data[user_gui_settings_key]:
            self.user_gui_settings = user_config.data[user_gui_settings_key]

        # Get the language order as determined by user regions and user languages, change
        # the user languages list to be regex strings instead of language names
        for region in self.region_order_user:
            self.region_order_languages_user.extend(self.languages_filter[region])

        # Make sure language entries are unique
        self.region_order_languages_user = reduce(lambda x,y: x + [y] if not y in x else x, self.region_order_languages_user, [])

        language_list: list[str] = []

        if not self.language_order_user:
            language_list = self.region_order_languages_user
        else:
            self.languages_user_found = True

            for language in self.language_order_user:
                if language in self.languages:
                    language_list.append(self.languages[language])

        self.language_order_user = language_list

        # Compensate for No-Intro using "United Kingdom", and Redump using "UK"
        if 'UK' in self.region_order_user:
            uk_index: int = self.region_order_user.index('UK')
            self.region_order_user[uk_index + 1:uk_index + 1] = ['United Kingdom']

        uk_index = self.region_order_default.index('UK')
        self.region_order_default[uk_index + 1:uk_index + 1] = ['United Kingdom']

        # Add "Export" to the default region order regex as a pseudo-region, which is reinterpreted as "World" later
        self.regex.region_order_default = re.compile('\\s\\((?:\\w*,\\s)*(?:' + '|'.join(self.region_order_default + ['Export']) + ')(?:,\\s\\w*)*\\)')

        if 'UK' in self.languages_implied:
            self.languages_implied['United Kingdom'] = self.languages_implied['UK']
            self.languages_filter['United Kingdom'] = self.languages_filter['UK']

        # Import global overrides
        self.system_settings_path = system_settings_path

        self.global_exclude: list[str] = user_config.data[user_override_exclude_key]
        self.global_include: list[str] = user_config.data[user_override_include_key]

        # Import global post filters
        self.global_filter: list[str] = user_config.data[user_filter_key]

        # Populate system settings later when the DAT file has been loaded
        self.system_clone_list: str = ''
        self.system_metadata_file: str = ''
        self.system_name: str = ''
        self.system_output: str = ''
        self.system_languages_user_found: bool = False
        self.system_language_order_user: list[str|dict[str,str]] = []
        self.system_region_order_user: list[str|dict[str,str]] = []
        self.system_video_order_user: list[str|dict[str,str]] = []
        self.system_exclude: list[str] = []
        self.system_include: list[str] = []
        self.system_filter: list[str|dict[str,str]] = []
        self.system_user_prefix: str = ''
        self.system_user_suffix: str = ''
        self.system_user_path_settings: tuple[Any, ...] = ()
        self.system_exclusions_options: tuple[Any, ...] = ()

        # Store the Retool version
        self.version_major: str = version_major
        self.version_minor: str = version_minor

        # Store invalid input
        self.sanitized_characters: tuple[str, ...] = sanitized_characters
        self.reserved_filenames: tuple[str, ...] = reserved_filenames

        # Store the user input object
        self.user_input: Any = user_input

    def __str__(self) -> str:
        from modules.utils import format_value

        return_attributes: list[list[str]] = []

        for attribute in dir(self):
            if not attribute.startswith('__'):
                if len(str(getattr(self, attribute))) > 80:
                    return_attributes.append([f'  ├ .{attribute}:', f'(...) {format_value(str(type(getattr(self, attribute)).__name__))}'])
                else:
                    return_attributes.append([f'  ├ .{attribute}:', format_value(getattr(self, attribute))])

        final_item: list[str] = return_attributes[-1].copy()

        for i, s in enumerate(final_item):
            return_attributes[-1][i] = s.replace('├', '└')

        # Column alignment
        def class_output() -> None:
            eprint(f'\n  ○ Config object')
            col_width: int = max(len(word) for row in return_attributes for word in row) - 20
            for row in return_attributes:
                eprint(''.join(str(word).ljust(col_width) for word in row))

        key: str = 'blank'

        class_output()

        while key != 'q':
            eprint('\nEnter a dictionary key to expand (e.g. tags_disc_rename), q to continue, or hit enter to list: ')
            key = input()

            eprint(key)

            if hasattr(self, key):
                if (
                    isinstance(getattr(self, key), Regex)
                    or isinstance(getattr(self, key), UserInput)):
                        eprint(f'\n{vars(getattr(self, key))}')
                else:
                    eprint(f'\n{format_value(getattr(self, key))}')
            elif key == '':
                class_output()
            else:
                if key != 'q':
                    eprint(f'\nUnknown key "{key}".')
                else:
                    break

        return ''

class UserOverrides():
    """ Creates an object for overrides """

    def __init__(self, exclude: list[str] = [], include: list[str] = []):
        self.exclude: list[str] = exclude
        self.include: list[str] = include
        self.file = ''

def generate_config(
    user_config_file: str,
    languages: tuple[str, ...],
    regions: tuple[str, ...],
    video_standards: tuple[str, ...],
    overrides_path: str,
    global_overrides: UserOverrides = UserOverrides(),
    system_overrides: UserOverrides = UserOverrides(),
    global_filters: list[str] = [],
    system_filters: list[str] = [],
    list_prefix: str = '',
    list_suffix: str = '',
    gui_settings: set[str] = set(),
    overwrite: bool = False,
    system_config: bool = False,
    system_paths: dict[str, str]  = {},
    override_status: dict[str, bool] = {},
    first_run_gui: bool = False
    ) -> None:
    """ When run from the CLI, creates required config files if they're missing. When run
    from the GUI, additionally manages the editing of those files.

    Args:
        - `user_config_file (str)` The location of user-config.yaml.

        - `languages (tuple[str, ...])` All languages Retool is processing.

        - `regions (tuple[str, ...])` All regions Retool is processing.

        - `video_standards (tuple[str, ...])` All video standards Retool is processing.

        - `overrides_path (str)` Where the overrides are stored for individual systems.

        - `global_overrides (dict[str, list[str]], optional)` Include and exclude overrides
          applied at the global level. Defaults to `{}`.

        - `system_overrides (dict[str, list[str]], optional)` Include and exclude overrides
          applied at the system level. Defaults to `{}`.

        - `list_prefix (str, optional)` A prefix to add to each title that's exported
          using `--listnames`. Defaults to `''`.

        - `list_suffix (str, optional)` A suffix to add to each title that's exported
          using `--listnames`. Defaults to `''`.

        - `gui_settings (set[str], optional)` Settings only written and used by Retool's
          GUI. Defaults to `set()`.

        - `overwrite (bool, optional)` Whether or not to overwrite an existing
          user-config.yaml. Only used when the GUI is writing the file. Defaults to `False`.

        - `system_config (bool)` Whether a system config is in play. Defaults to `False`.

        - `system_paths (dict[str, str])` The paths the user has set for the system config,
          including the output folder, clone list file, and metadata file.

        - `overide_status (dict[str, bool])` Which system settings tabs have the "Override
          global settings" checkbox enabled.

        - `first_run_gui (bool, optional)` When using the GUI, is set to `True` to prevent
          Retool from prompting the user through the CLI to generate user-config.yaml, as
          this happens automatically through GUI use anyway. Defaults to `False`.
    """

    new_user_config: bool = False

    if not pathlib.Path(user_config_file).exists() or overwrite:
        try:
            def add_entry(string: str, is_comment: bool = False) -> None:
                    """ Adds an entry to a YAML object.

                    Args:
                        - `string (str)` The entry to add to the list.

                        - `is_comment (bool, optional)` Whether or not the entry
                          should be commented out. Defaults to `False`.
                    """

                    if is_comment:
                        output_file.writelines(f'\n# - {string}')
                    else:
                        output_file.writelines(f'\n- {string}')

            with open(pathlib.Path(user_config_file), 'w', encoding='utf-8') as output_file:
                output_file.writelines(
                    '---')

                if system_config:
                    clone_list_path: str = '# clonelists\your-clone-list.json'
                    metadata_file_path: str = '# metadata/your-metadata-file.json'
                    output_path: str = '# C:\path'

                    if system_paths:
                        if system_paths['clone list']:
                            clone_list_path = system_paths['clone list']
                        if system_paths['metadata file']:
                            metadata_file_path = system_paths['metadata file']
                        if system_paths['output']:
                            output_path = system_paths['output']

                    output_file.writelines(
                        f'\n# This file contains the system settings for {pathlib.Path(user_config_file).stem}.'
                        '\n#'
                        '\n# It might override settings in config/user-config.yaml specifically for that'
                        '\n# DAT.'
                        '\n#'
                        '\n# =============================================='
                        '\n# CLONE LIST, METADATA FILE, AND OUTPUT LOCATION'
                        '\n# =============================================='
                        '\npaths:'
                        f'\n- override: {str(override_status["paths"]).lower()}'
                        f'\n- clone list: {clone_list_path}'
                        f'\n- metadata file: {metadata_file_path}'
                        f'\n- output: {output_path}'
                        '\n#'
                    )

                output_file.writelines(
                    '\n# =============='
                    '\n# LANGUAGE ORDER'
                    '\n# =============='
                    '\n# If the -l option is used, only include titles with the following languages.'
                    '\n# Comment out languages you don\'t want. Order is important.'
                    '\nlanguage order:'
                )

                if system_config:
                    output_file.writelines(
                        f'\n- override: {str(override_status["languages"]).lower()}'
                    )

                if not overwrite:
                    for language in languages:
                        add_entry(language, is_comment=True)
                else:
                    for language in languages:
                        if 'Comment|' in language:
                            add_entry(language[8:], is_comment=True)
                        else:
                            add_entry(language)

                output_file.writelines(
                    '\n\n# ============'
                    '\n# REGION ORDER'
                    '\n# ============'
                    '\n# Only include titles with the following regions. Comment out the regions you'
                    '\n# don\'t want. Order is important.'
                    '\nregion order:')

                if system_config:
                    output_file.writelines(
                         f'\n- override: {str(override_status["regions"]).lower()}'
                    )

                for region in regions:
                    if not overwrite:
                        add_entry(region)
                    else:
                        if 'Comment|' in region:
                            add_entry(region[8:], is_comment=True)
                        else:
                            add_entry(region)

                output_file.writelines(
                    '\n\n# ==========='
                    '\n# VIDEO ORDER'
                    '\n# ==========='
                    '\n# Priority for titles with a video tag in their name. Do not comment out any'
                    '\n# lines.'
                    '\nvideo order:'
                )

                if system_config:
                    output_file.writelines(
                        f'\n- override: {str(override_status["video"]).lower()}'
                    )

                for video_standard in video_standards:
                    add_entry(video_standard)

                output_file.writelines(
                    '\n\n# ============================'
                    '\n# LIST NAMES PREFIX AND SUFFIX'
                    '\n# ============================'
                    '\n# If the --listnames option is used, you can optionally add a prefix and'
                    '\n# suffix to each title.'
                    '\n#'
                    '\n# If you start a prefix with http://, https://, or ftp://, each line in the'
                    '\n# list will be URL encoded.'
                    '\n#'
                    '\n# The text must be inside double quotes. You must escape other double quotes'
                    '\n# and backslashes inside the quotes like so: \\", \\\\'
                    '\nlist prefix:')

                if list_prefix != '':
                    add_entry(f'"{list_prefix}"')
                else:
                    output_file.writelines('\n# - "This text will be at the start of each line"')

                output_file.writelines(f'\n\nlist suffix:')

                if list_suffix != '':
                    add_entry(f'"{list_suffix}"')
                else:
                    output_file.writelines('\n# - "This text will be at the end of each line"')

                output_file.writelines(
                    '\n\n# ===================================='
                    '\n# GLOBAL EXCLUDE AND INCLUDE OVERRIDES'
                    '\n# ===================================='
                    '\n# Override Retool and force exclude or include specific titles by adding your own'
                    '\n# text to match against. Items in the list are case sensitive. See the'
                    '\n# documentation for more information, and pay particular attention to how system'
                    '\n# overrides interact with global overrides.'
                    '\n#'
                    '\n# The formatting is as follows:'
                    '\n#'
                    '\n# * Plain text indicates a partial string match.'
                    '\n# * A prefix of / indicates a regular expression match.'
                    '\n# * A prefix of | indicates a full string match.'
                    '\n# * Additionally, wrap a string in <> to also remove any match\'s related clones.'
                    '\n#'
                    '\n# The text must be inside double quotes. You must escape double quotes and'
                    '\n# backslashes like so: \\", \\\\'
                    '\n#'
                    '\n# Comment out lines you don\'t want.'
                    '\nexclude:')

                if system_config:
                    if (
                        system_overrides == UserOverrides()
                        or system_overrides.exclude == []):
                        output_file.writelines('\n# - "[b]"')
                        output_file.writelines('\n# - "/.*?\\(Virtual*"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), system_overrides.exclude))

                    output_file.writelines('\n\ninclude:')

                    if (
                        system_overrides == UserOverrides()
                        or system_overrides.include == []):
                        output_file.writelines('\n# - "|My favorite title (Japan)"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), system_overrides.include))
                else:
                    if (
                        global_overrides == UserOverrides()
                        or global_overrides.exclude == []):
                        output_file.writelines('\n# - "[b]"')
                        output_file.writelines('\n# - "/.*?\\(Virtual*"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), global_overrides.exclude))

                    output_file.writelines('\n\ninclude:')

                    if (
                        global_overrides == UserOverrides()
                        or global_overrides.include == []):
                        output_file.writelines('\n# - "|My favorite title (Japan)"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), global_overrides.include))

                output_file.writelines(
                    '\n\n# ============'
                    '\n# POST FILTERS'
                    '\n# ============'
                    '\n# After Retool has finished processing, remove all titles except the ones that'
                    '\n# match the text listed here. Items in the list are case sensitive. See the'
                    '\n# documentation for more information.'
                    '\n#'
                    '\n# The formatting is as follows:'
                    '\n#'
                    '\n# * Plain text indicates a partial string match.'
                    '\n# * A prefix of / indicates a regular expression match.'
                    '\n# * A prefix of | indicates a full string match.'
                    '\n#'
                    '\n# The text must be inside double quotes. You must escape double quotes and'
                    '\n# backslashes like so: \\", \\\\'
                    '\n#'
                    '\n# Comment out lines you don\'t want'
                    '\nfilters:')

                if system_config:
                    output_file.writelines(f'\n- override: {str(override_status["post_filters"]).lower()}')

                    if not system_filters:
                        output_file.writelines('\n# - "/.*?\\(Virtual*"')
                        output_file.writelines('\n# - "|My favorite title (Japan)"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), system_filters))

                else:
                    if not global_filters:
                        output_file.writelines('\n# - "/.*?\\(Virtual*"')
                        output_file.writelines('\n# - "|My favorite title (Japan)"')
                    else:
                        list(map(lambda s: output_file.writelines(f'\n- "{s}"'), global_filters))

                if system_config:
                    output_file.writelines(
                        '\n\n# ======================'
                        '\n# EXCLUSIONS AND OPTIONS'
                        '\n# ======================'
                        '\n# You should use the GUI to generate these options, even if you'
                        '\n# intend to use the CLI. Add a DAT, go the the System settings'
                        '\n# tab, and then change the exclusions and options to populate'
                        '\n# this section.'
                        '\nexclusions and options:'
                        f'\n- override exclusions: {str(override_status["exclusions"]).lower()}'
                        f'\n- override options: {str(override_status["options"]).lower()}')
                else:
                    output_file.writelines(
                    '\n\n# ============'
                    '\n# GUI SETTINGS'
                    '\n# ============'
                    '\n# GUI settings only, not used by the CLI.'
                    '\ngui settings:')

                if gui_settings != set():
                    for setting in sorted(gui_settings):
                        add_entry(setting)

                new_user_config = True

        except OSError as e:
            eprint(f'\n{Font.error_bold}* Error: {Font.end}{str(e)}\n')
            raise

    pathlib.Path(overrides_path).mkdir(parents=True, exist_ok=True)

    if (
        overwrite == False
        and not first_run_gui):

        if new_user_config:
            file_list: list[str]= []

            if new_user_config:
                file_list.append(f'* {Font.warning_bold}config/user-config.yaml{Font.warning}')

            file_list_str: str = '\n'.join(file_list)

            printwrap(f'{Font.warning}It\'s likely this is the first time '
                      'you\'ve run Retool. The following system files were '
                      f'missing and have been created:', 'no_indent')

            eprint(f'\n{file_list_str}\n')

            if new_user_config:
                printwrap(f'You might want to edit {Font.warning_bold}'
                          f'config/user-config.yaml{Font.warning} to define a custom '
                          f'region order, or to filter specific languages.{Font.end}',
                          'no_indent')
                eprint('')

            printwrap(f'{Font.success}You can now run Retool normally.{Font.end}', 'no_indent')

            sys.exit()