from functools import wraps
import hashlib
import logging
from operator import itemgetter
import os
import re
from unicodedata import normalize as unicodedata_normalize
import unicodedata

from crispy_forms.layout import HTML, Button
from django import forms
from django.apps import apps
from django.conf import settings
from django.contrib import admin
from django.contrib.contenttypes.fields import (GenericForeignKey, GenericRel,
                                                GenericRelation)
from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import UploadedFile
from django.core.mail import get_connection
from django.db import models
from django.db.models import Q
from django.db.models.fields.files import FieldFile
from django.forms import BaseForm
from django.forms.widgets import SplitDateTimeWidget
from django.http.response import HttpResponse
from django.template import loader
from django.urls.base import reverse
from django.utils import six, timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
from easy_thumbnails import source_generators
from floppyforms.widgets import ClearableFileInput
from image_cropping.widgets import ImageCropWidget
import magic
from unipath.path import Path
import weasyprint

from sapl.crispy_layout_mixin import SaplFormHelper
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row

# (26/10/2018): O separador foi mudador de '/' para 'K'
# por conta dos leitores de códigos de barra, que trocavam
# a '/' por '&' ou ';'
SEPARADOR_HASH_PROPOSICAO = 'K'

def dont_break_out(value, max_part=50):
    _safe = value.split()

    def chunkstring(string):
        return re.findall('.{%d}' % max_part, string)

    def __map(a):
        if len(a) <= max_part:
            return a
        return '
' + '
'.join(chunkstring(a))

    _safe = map(__map, _safe)
    _safe = ' '.join(_safe)

    _safe = mark_safe(_safe)
    return value

def clear_thumbnails_cache(queryset, field):

    for r in queryset:
        assert hasattr(r, field), _(
            'Objeto da listagem não possui o campo informado')

        if not getattr(r, field):
            continue

        path = Path(getattr(r, field).path)

        if not path.exists():
            continue

        cache_files = path.parent.walk()

        for cf in cache_files:
            if cf != path:
                cf.remove()

def normalize(txt):
    return unicodedata_normalize(
        'NFKD', txt).encode('ASCII', 'ignore').decode('ASCII')

def get_settings_auth_user_model():
    return getattr(settings, 'AUTH_USER_MODEL', 'auth.User')

autor_label = '''

       Autor:
                {% if form.autor.value %}
                    {{form.autor.value}}
                {% endif %}

'''

autor_modal = '''

       <form>
           <input id="q" type="text" />
           <input id="pesquisar" type="submit" value="Pesquisar"
               class="btn btn-primary btn-sm"/>
       </form>

       <input type="submit" id="selecionar" value="Selecionar"
              hidden="true" />

'''

def montar_row_autor(name):
    autor_row = to_row(
        [(name, 0),
         (Button('pesquisar',
                 'Pesquisar Autor',
                 css_class='btn btn-secondary btn-sm'), 2),
         (Button('limpar',
                 'Limpar Autor',
                 css_class='btn btn-secondary btn-sm'), 10)])

    return autor_row

# TODO: Esta função é utilizada?

def montar_helper_autor(self):
    autor_row = montar_row_autor('nome')
    self.helper = SaplFormHelper()
    self.helper.layout = SaplFormLayout(*self.get_layout())

    # Adiciona o novo campo 'autor' e mecanismo de busca
    self.helper.layout[0][0].append(HTML(autor_label))
    self.helper.layout[0][0].append(HTML(autor_modal))
    self.helper.layout[0][1] = autor_row

    # Adiciona espaço entre o novo campo e os botões
    # self.helper.layout[0][4][1].append(HTML('

'))

    # Remove botões que estão fora do form
    self.helper.layout[1].pop()

    # Adiciona novos botões dentro do form
    self.helper.layout[0][4][0].insert(2, form_actions(more=[
        HTML('Cancelar')]))

class SaplGenericForeignKey(GenericForeignKey):

    def __init__(
        self,
            ct_field='content_type',
            fk_field='object_id',
            for_concrete_model=True,
            verbose_name=''):
        super().__init__(ct_field, fk_field, for_concrete_model)
        self.verbose_name = verbose_name

class SaplGenericRelation(GenericRelation):
    """
    Extenção da class GenericRelation para implmentar o atributo fields_search

    fields_search é uma tupla de tuplas de dois strings no padrão de construção
        de campos porém com [Field Lookups][ref_1]

        exemplo:
            [No Model Parlamentar em][ref_2] existe a implementação dessa
            classe no atributo autor. Parlamentar possui três informações
            relevantes para buscas realacionadas a Autor:

                - nome_completo;
                - nome_parlamentar; e
                - filiacao__partido__sigla

            que devem ser pesquisados, coincidentemente
            pelo FieldLookup __icontains

            portanto a estrutura de fields_search seria:
                fields_search=(
                    ('nome_completo', '__icontains'),
                    ('nome_parlamentar', '__icontains'),
                    ('filiacao__partido__sigla', '__icontains'),
                )

    [ref_1]: https://docs.djangoproject.com/el/1.10/topics/db/queries/
             #field-lookups
    [ref_2]: https://github.com/interlegis/sapl/blob/master/sapl/
             parlamentares/models.py
    """

    def __init__(self, to, fields_search=(), **kwargs):

        assert 'related_query_name' in kwargs, _(
            'SaplGenericRelation não pode ser instanciada sem '
            'related_query_name.')

        assert fields_search, _(
            'SaplGenericRelation não pode ser instanciada sem fields_search.')

        for field in fields_search:
            # descomente para ver todas os campos que são elementos de busca
            # print(kwargs['related_query_name'], field)

            assert isinstance(field, (tuple, list)), _(
                'fields_search deve ser um array de tuplas ou listas.')

            assert len(field) <= 3, _(
                'cada tupla de fields_search deve possuir até 3 strings')

            # TODO implementar assert para validar campos do Model e lookups

        self.fields_search = fields_search
        super().__init__(to, **kwargs)

class OverwriteStorage(FileSystemStorage):

    def save(self, name, content, max_length=None):

        prod_name = FileSystemStorage.save(
            self, name, content, max_length=max_length)

        FileSystemStorage.save(
            self, 'original__%s' % prod_name, content, max_length=max_length)

        return prod_name

class PortalFieldFile(FieldFile):

    @property
    def original_path(self):
        self._require_file()
        return self.storage.path('original__{}'.format(self.name))

    @property
    def original_name(self):
        self._require_file()
        return 'original__{}'.format(self.name)

    @property
    def url(self):
        try:
            self._require_file()
            # if settings.DEBUG:
            #    return self.storage.url(self.name)

            field_name_action = self.field.name.replace('_', '-')
            return '%s' % reverse(
                'sapl.api:%s-%s' % (self.instance._meta.model_name,
                                    field_name_action),
                kwargs={'pk': self.instance.pk})
        except:
            return ''

    def shorten_file_name(self):
        name = self.name

        if not name:
            return

        nl = name.split('/')

        if len(nl[-1]) < 128:
            return

        name = nl[-1][-20:]

        name = f'f_{name}'

        nl[-1] = name
        new_path = f"{settings.MEDIA_ROOT}/{'/'.join(nl)}"
        new_original_path = new_path.replace('sapl/', 'original__sapl/')
        new_original_path = new_original_path.replace('cmj/', 'original__cmj/')

        os.rename(self.path, new_path)
        os.rename(self.original_path, new_original_path)

        self.name = '/'.join(nl)
        self.instance.save()

    def delete(self, save=True):
        if not self:
            return
        # Only close the file if it's already open, which we know by the
        # presence of self._file
        if hasattr(self, '_file'):
            self.close()
            del self.file

        original_name = self.name.replace('sapl/', 'original__sapl/')
        original_name = original_name.replace('cmj/', 'original__cmj/')
        self.storage.delete(self.name)
        self.storage.delete(original_name)

        self.name = None
        setattr(self.instance, self.field.name, self.name)
        self._committed = False

        if save:
            self.instance.save()

class PortalFileField(models.FileField):
    attr_class = PortalFieldFile

class ImageThumbnailFileInput(ClearableFileInput):
    template_name = 'floppyforms/image_thumbnail.html'

class CustomImageCropWidget(ImageCropWidget):
    """
    Custom ImageCropWidget that doesn't show the initial value of the field.
    We use this trick, and place it right under the CropWidget so that
    it looks like the user is seeing the image and clearing the image.
    """
    template_with_initial = (
        # '%(initial_text)s: %(initial)s '
        '%(clear_template)s
%(input_text)s: %(input)s'
    )

class RangeWidgetOverride(forms.MultiWidget):

    def __init__(self, attrs=None):
        widgets = (forms.DateInput(format='%d/%m/%Y',
                                   attrs={'class': 'dateinput form-control',
                                          'placeholder': 'Inicial'}),
                   forms.DateInput(format='%d/%m/%Y',
                                   attrs={'class': 'dateinput form-control',
                                          'placeholder': 'Final'}))
        super(RangeWidgetOverride, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return [value.start, value.stop]
        return []

    def render(self, name, value, attrs=None, renderer=None):
        rendered_widgets = []
        for i, x in enumerate(self.widgets):
            rendered_widgets.append(
                x.render(
                    '%s_%d' % (name, i), value[i] if value else ''
                )
            )

        html = '
%s

%s
'\
            % tuple(rendered_widgets)
        return '
%s
' % html

class CustomSplitDateTimeWidget(SplitDateTimeWidget):

    def render(self, name, value, attrs=None, renderer=None):
        rendered_widgets = []
        for i, x in enumerate(self.widgets):
            x.attrs['class'] += ' form-control'
            rendered_widgets.append(
                x.render(
                    '%s_%d' % (name, i), self.decompress(
                        value)[i] if value else ''
                )
            )

        html = '
%s

%s
'\
            % tuple(rendered_widgets)
        return '
%s
' % html

def register_all_models_in_admin(module_name, exclude_list=[]):
    appname = module_name.split('.')
    appname = appname[1] if appname[0] == 'sapl' else appname[0]
    app = apps.get_app_config(appname)
    for model in app.get_models():
        if not admin.site.is_registered(model):
            admin.site.register(model)

def xstr(s):
    return '' if s is None else str(s)

def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

def get_base_url(request):
    # TODO substituir por Site.objects.get_current().domain
    # from django.contrib.sites.models import Site

    current_domain = request.get_host()
    protocol = 'https' if request.is_secure() else 'http'
    return "{0}://{1}".format(protocol, current_domain)

def create_barcode(value, width=170, height=50):
    '''
        creates a base64 encoded barcode PNG image
    '''
    from base64 import b64encode
    from reportlab.graphics.barcode import createBarcodeDrawing
    value_bytes = bytes(value, "ascii")
    barcode = createBarcodeDrawing('Code128',
                                   value=value_bytes,
                                   barWidth=width,
                                   height=height,
                                   fontSize=2,
                                   humanReadable=True)
    data = b64encode(barcode.asString('png'))
    return data.decode('utf-8')

YES_NO_CHOICES = [(True, _('Sim')), (False, _('Não'))]

def listify(function):

    @wraps(function)
    def f(*args, **kwargs):
        return list(function(*args, **kwargs))

    return f

LISTA_DE_UFS = [
    ('AC', 'Acre'),
    ('AL', 'Alagoas'),
    ('AP', 'Amapá'),
    ('AM', 'Amazonas'),
    ('BA', 'Bahia'),
    ('CE', 'Ceará'),
    ('DF', 'Distrito Federal'),
    ('ES', 'Espírito Santo'),
    ('GO', 'Goiás'),
    ('MA', 'Maranhão'),
    ('MT', 'Mato Grosso'),
    ('MS', 'Mato Grosso do Sul'),
    ('MG', 'Minas Gerais'),
    ('PR', 'Paraná'),
    ('PB', 'Paraíba'),
    ('PA', 'Pará'),
    ('PE', 'Pernambuco'),
    ('PI', 'Piauí'),
    ('RJ', 'Rio de Janeiro'),
    ('RN', 'Rio Grande do Norte'),
    ('RS', 'Rio Grande do Sul'),
    ('RO', 'Rondônia'),
    ('RR', 'Roraima'),
    ('SC', 'Santa Catarina'),
    ('SE', 'Sergipe'),
    ('SP', 'São Paulo'),
    ('TO', 'Tocantins'),
    ('EX', 'Exterior'),
]

RANGE_ANOS = [(year, year) for year in range(timezone.now().year + 1,
                                             1889, -1)]

RANGE_MESES = [
    (1, 'Janeiro'),
    (2, 'Fevereiro'),
    (3, 'Março'),
    (4, 'Abril'),
    (5, 'Maio'),
    (6, 'Junho'),
    (7, 'Julho'),
    (8, 'Agosto'),
    (9, 'Setembro'),
    (10, 'Outubro'),
    (11, 'Novembro'),
    (12, 'Dezembro'),
]

RANGE_DIAS_MES = [(n, n) for n in range(1, 32)]

def ANO_CHOICES():
    return [('', '---------')] + RANGE_ANOS

def choice_anos(model):
    try:
        anos_list = model.objects.all().distinct(
            'ano').order_by('-ano').values_list('ano', 'ano')
        return list(anos_list)
    except Exception:
        return []

def choice_anos_com_materias():
    from sapl.materia.models import MateriaLegislativa
    return choice_anos(MateriaLegislativa)

def choice_anos_com_normas():
    from sapl.norma.models import NormaJuridica
    return choice_anos(NormaJuridica)

def choice_anos_com_protocolo():
    from sapl.protocoloadm.models import Protocolo
    return choice_anos(Protocolo)

def choice_anos_com_documentoadministrativo():
    from sapl.protocoloadm.models import DocumentoAdministrativo
    return choice_anos(DocumentoAdministrativo)

def choice_anos_com_sessaoplenaria():
    try:
        from sapl.sessao.models import SessaoPlenaria
        y = SessaoPlenaria.objects.all().dates('data_inicio', 'year')

        y = list(map(lambda x: x.year, y))

        now = timezone.now()

        if now.year not in y:
            y.append(now.year)

        y.sort(reverse=True)

        y = map(lambda x: (x, x), y)

        return y
    except Exception:
        return []

def choice_force_optional(callable):
    """ Django-filter faz algo que tenha o mesmo sentido em ChoiceFilter,
        no entanto, as funções choice_anos_... podem ser usadas em formulários
        comuns de adição e/ou edição, com a particularidade de terem
        required=False.
        Neste caso para ser possível contar com a otimização de apenas mostrar anos
        que estejam na base de dados e ainda colocar o item opcional '---------',
        é necessário encapsular então, as funções choice_anos_... com a
        esta função choice_force_optional... isso ocorre e foi aplicado
        inicialmente no cadastro de documentos administrativos onde tem-se
        opcionalmente a possibilidade de colocar o ano do protocolo.
        Em ChoiceFilter choice_force_optional não deve ser usado pois duplicaria
        o item opcional '---------' já que ChoiceFilter já o adiciona, como dito
        anteriormente.
    """

    def _func():
        return [('', '---------')] + callable()

    return _func

FILTER_OVERRIDES_DATEFIELD = {
    'filter_class': django_filters.DateFromToRangeFilter,
    'extra': lambda f: {
        'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')),
        'widget': RangeWidgetOverride
    }
}

class FilterOverridesMetaMixin:
    filter_overrides = {
        models.DateField: FILTER_OVERRIDES_DATEFIELD
    }

TIPOS_TEXTO_PERMITIDOS = (
    'application/vnd.oasis.opendocument.text',
    'application/x-vnd.oasis.opendocument.text',
    'application/pdf',
    'application/x-pdf',
    'application/zip',
    'application/acrobat',
    'applications/vnd.pdf',
    'text/pdf',
    'text/x-pdf',
    'text/plain',
    'application/txt',
    'browser/internal',
    'text/anytext',
    'widetext/plain',
    'widetext/paragraph',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/xml',
    'application/octet-stream',
    'text/xml',
    'text/html',
    'video/mp4'
)

TIPOS_IMG_PERMITIDOS = (
    'image/jpeg',
    'image/jpg',
    'image/jpe_',
    'image/pjpeg',
    'image/vnd.swiftview-jpeg',
    'application/jpg',
    'application/x-jpg',
    'image/pjpeg',
    'image/pipeg',
    'image/vnd.swiftview-jpeg',
    'image/x-xbitmap',
    'image/bmp',
    'image/x-bmp',
    'image/x-bitmap',
    'image/png',
    'application/png',
    'application/x-png',
)

def fabrica_validador_de_tipos_de_arquivo(lista, nome):

    def restringe_tipos_de_arquivo(value):
        if not os.path.splitext(value.path)[1][:1]:
            raise ValidationError(_(
                'Não é possível fazer upload de arquivos sem extensão.'))
        try:
            mime = magic.from_buffer(value.read(), mime=True)
            if mime not in lista:
                raise ValidationError(_('Tipo de arquivo não suportado'))
        except FileNotFoundError:
            raise ValidationError(_('Arquivo não encontrado'))

    # o nome é importante para as migrations
    restringe_tipos_de_arquivo.__name__ = nome
    return restringe_tipos_de_arquivo

restringe_tipos_de_arquivo_txt = fabrica_validador_de_tipos_de_arquivo(
    TIPOS_TEXTO_PERMITIDOS, 'restringe_tipos_de_arquivo_txt')
restringe_tipos_de_arquivo_img = fabrica_validador_de_tipos_de_arquivo(
    TIPOS_IMG_PERMITIDOS, 'restringe_tipos_de_arquivo_img')

def intervalos_tem_intersecao(a_inicio, a_fim, b_inicio, b_fim):
    maior_inicio = max(a_inicio, b_inicio)
    menor_fim = min(a_fim, b_fim)
    return maior_inicio <= menor_fim

class MateriaPesquisaOrderingFilter(django_filters.OrderingFilter):

    choices = (
        ('dataD', 'Ordem Decrescente - Data, Tipo, Ano, Numero '),
        ('tipoD', 'Ordem Decrescente - Tipo, Ano, Numero, Data'),
        ('dataC', 'Ordem Crescente - Data, Tipo, Ano, Numero'),
        ('tipoC', 'Ordem Crescente - Tipo, Ano, Numero, Data'),
    )
    order_by_mapping = {
        'dataD': ['-data_apresentacao', '-tipo__sigla', '-ano', '-numero'],
        'tipoD': ['-tipo__sigla', '-ano', '-numero', '-data_apresentacao'],
        'dataC': ['data_apresentacao', 'tipo__sigla', 'ano', 'numero'],
        'tipoC': ['tipo__sigla', 'ano', 'numero', 'data_apresentacao'],
    }

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = self.choices
        super(MateriaPesquisaOrderingFilter, self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        if value:
            _value = self.order_by_mapping[value[0]] if value else value
        else:
            _value = ['-data_apresentacao', '-id']

        return super().filter(qs, _value)

class NormaPesquisaOrderingFilter(django_filters.OrderingFilter):

    choices = (
        ('dataD', 'Ordem Decrescente - Data, Tipo, Ano, Numero '),
        ('tipoD', 'Ordem Decrescente - Tipo, Ano, Numero, Data'),
        ('dataC', 'Ordem Crescente - Data, Tipo, Ano, Numero'),
        ('tipoC', 'Ordem Crescente - Tipo, Ano, Numero, Data'),
    )
    order_by_mapping = {
        'dataD': ['-data', '-tipo', '-ano', '-numero'],
        'tipoD': ['-tipo', '-ano', '-numero', '-data'],
        'dataC': ['data', 'tipo', 'ano', 'numero'],
        'tipoC': ['tipo', 'ano', 'numero', 'data'],
    }

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = self.choices
        super(NormaPesquisaOrderingFilter, self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        if value:
            _value = self.order_by_mapping[value[0]] if value else value
        else:
            _value = self.order_by_mapping['dataD']
        return super().filter(qs, _value)

class FileFieldCheckMixin(BaseForm):

    def _check(self):
        cleaned_data = super(FileFieldCheckMixin, self).clean()
        errors = []
        for name, campo in self.fields.items():
            if isinstance(campo, forms.fields.FileField):
                error = self.errors.get(name)
                if error:
                    msgs = [e.replace('Certifique-se de que o arquivo',
                                      "Certifique-se de que o nome do "
                                      "arquivo no campo '{}'".format(
                                          campo.label))
                            for e in error]
                    for msg in msgs:
                        errors.append(msg)

                arquivo = self.cleaned_data.get(name)
                if arquivo and not isinstance(arquivo, UploadedFile):
                    if not os.path.exists(arquivo.path):
                        errors.append("Arquivo referenciado no campo "
                                      " '%s' inexistente! Marque a "
                                      "opção Limpar e Salve." % campo.label)
        if errors:
            raise ValidationError(errors)
        return cleaned_data

    def clean(self):
        """ Alias for _check() """
        return self._check()

class AnoNumeroOrderingFilter(django_filters.OrderingFilter):

    choices = (('DEC', 'Ordem Decrescente'),
               ('CRE', 'Ordem Crescente'),)

    order_by_mapping = {
        'DEC': ['-ano', '-data'],
        'CRE': ['ano', 'data'],
    }

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = self.choices
        super(AnoNumeroOrderingFilter, self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        _value = self.order_by_mapping[value[0]] if value else value
        return super().filter(qs, _value)

class DocumentoAdministrativoOrderingFilter(django_filters.OrderingFilter):

    choices = (('dataPUBd', 'Ordem Decrescente - Data de última atualização'),
               ('dataDOCd', 'Ordem Decrescente - Data, Tipo, Número'),
               ('dataDOCc', 'Ordem Crescente - Data, Tipo, Número'),
               ('dataVENd', 'Ordem Decrescente - Data de Vencimento, Tipo, Número'),
               ('dataVENc', 'Ordem Crescente - Data de Vencimento, Tipo, Número'),)

    order_by_mapping = {
        'dataPUBd': ['-data_ultima_atualizacao', '-id'],
        'dataDOCd': ['-data', '-tipo', '-numero', '-id'],
        'dataDOCc': ['data', 'tipo', 'numero', '-id'],
        'dataVENd': ['-data_vencimento', '-tipo', '-numero', '-id'],
        'dataVENc': ['data_vencimento', 'tipo', 'numero', '-id'],
    }

    def __init__(self, *args, **kwargs):
        kwargs['choices'] = self.choices
        super(DocumentoAdministrativoOrderingFilter,
              self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        if value:
            _value = self.order_by_mapping[value[0]] if value else value
        else:
            _value = self.order_by_mapping['dataPUBd']
        return super().filter(qs, _value)

def gerar_hash_arquivo(arquivo, pk, block_size=2 ** 20):

    arquivo = arquivo.replace('media/sapl/', 'media/original__sapl/')
    arquivo = arquivo.replace('media/cmj/', 'media/original__cmj/')

    md5 = hashlib.md5()
    arq = open(arquivo, 'rb')
    while True:
        data = arq.read(block_size)
        if not data:
            break
        md5.update(data)
    return 'P' + md5.hexdigest() + SEPARADOR_HASH_PROPOSICAO + pk

def hash_sha512(arquivo):

    f = open(arquivo, "rb") if isinstance(arquivo, str) else arquivo

    sha512_hash = hashlib.sha512()
    # Read and update hash string value in blocks of 4K
    for byte_block in iter(lambda: f.read(4096), b""):
        sha512_hash.update(byte_block)

    if isinstance(arquivo, str):
        f.close()

    return sha512_hash.hexdigest()

class ChoiceWithoutValidationField(forms.ChoiceField):

    def validate(self, value):
        if self.required and not value:
            raise ValidationError(
                self.error_messages['required'], code='required')

def models_with_gr_for_model(model):
    return list(map(
        lambda x: x.related_model,
        filter(
            lambda obj: obj.is_relation and
            hasattr(obj, 'field') and
            isinstance(obj, GenericRel),

            model._meta.get_fields(include_hidden=True))
    ))

def generic_relations_for_model(model):
    """
    Esta função retorna uma lista de tuplas de dois elementos, onde o primeiro
    elemento é um model qualquer que implementa SaplGenericRelation (SGR), o
    segundo elemento é uma lista de todas as SGR's que pode haver dentro do
    model retornado na primeira posição da tupla.

    Exemplo: No Sapl, o model Parlamentar tem apenas uma SGR para Autor.
                Se no Sapl existisse apenas essa SGR, o resultado dessa função
                seria:
                    [   #Uma Lista de tuplas
                        (   # cada tupla com dois elementos
                            sapl.parlamentares.models.Parlamentar,
                            [<sapl.utils.SaplGenericRelation: autor>]
                        ),
                    ]
    """
    return list(map(
        lambda x: (x,
                   list(filter(
                       lambda field: (
                           isinstance(
                               field, SaplGenericRelation) and
                           field.related_model == model),
                       x._meta.get_fields(include_hidden=True)))),
        models_with_gr_for_model(model)
    ))

def texto_upload_path(instance, filename, subpath='', pk_first=False):
    """
    update materia_materialegislativa set texto_original = regexp_replace(texto_original, '^sapl/public/', '', 'ig');

    O path gerado por essa função leva em conta a pk de instance.
    isso não é possível naturalmente em uma inclusão pois a implementação
    do django framework chama essa função antes do metodo save

    Por outro lado a forma como vinha sendo formada os paths para os arquivos
    são improdutivas e inconsistentes. Exemplo: usava se o valor de __str__
    do model Proposicao que retornava a descrição da proposição, não retorna
    mais, para uma pasta formar o path do texto_original.
    Ora, o resultado do __str__ citado é totalmente impróprio para ser o nome
    de uma pasta.

    Para colocar a pk no path, a solução encontrada foi implementar o método
    save nas classes que possuem atributo do tipo FileField, implementação esta
    que guarda o FileField em uma variável independente e temporária para savar
    o object sem o arquivo e, logo em seguida, salvá-lo novamente com o arquivo
    Ou seja, nas inclusões que já acomparem um arquivo, haverá dois saves,
    um para armazenar toda a informação e recuperar o pk, e outro logo em
    seguida para armazenar o arquivo.
    """

    filename = re.sub('\s', '_', normalize(filename.strip()).lower())

    from sapl.materia.models import Proposicao, MateriaLegislativa, \
        DocumentoAcessorio
    from sapl.protocoloadm.models import DocumentoAdministrativo, \
        DocumentoAcessorioAdministrativo

    from cmj.cerimonial.models import Visita, Visitante

    if isinstance(
        instance, (
            DocumentoAdministrativo,
            Proposicao,
            DocumentoAcessorioAdministrativo,
            DocumentoAcessorio,
            MateriaLegislativa,
            Visita, Visitante
        )
    ):
        prefix = 'private'
    else:
        prefix = 'public'

    str_path = ('./sapl/%(prefix)s/%(model_name)s/'
                '%(subpath)s/%(pk)s/%(filename)s')

    if pk_first:
        str_path = ('./sapl/%(prefix)s/%(model_name)s/'
                    '%(pk)s/%(subpath)s/%(filename)s')

    if subpath is None:
        subpath = '_'

    path = str_path % \
        {
            'prefix': prefix,
            'model_name': instance._meta.model_name,
            'pk': instance.pk,
            'subpath': subpath,
            'filename': filename
        }

    return path

def qs_override_django_filter(self):
    if not hasattr(self, '_qs'):
        valid = self.is_bound and self.form.is_valid()

        if self.is_bound and not valid:
            """if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
                raise forms.ValidationError(self.form.errors)
            elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:"""
            self._qs = self.queryset.none()
            return self._qs
            # else STRICTNESS.IGNORE...  ignoring

        # start with all the results and filter from there
        qs = self.queryset.all()
        for name, filter_ in six.iteritems(self.filters):
            value = None
            if valid:
                value = self.form.cleaned_data[name]
            else:
                raw_value = self.form[name].value()
                try:
                    value = self.form.fields[name].clean(raw_value)
                except forms.ValidationError:
                    """if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
                        raise
                    elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:"""
                    self._qs = self.queryset.none()
                    return self._qs
                    # else STRICTNESS.IGNORE...  ignoring

            if value is not None:  # valid & clean data
                qs = qs._next_is_sticky()
                qs = filter_.filter(qs, value)

        self._qs = qs

    return self._qs

def filiacao_data(parlamentar, data_inicio, data_fim=None):
    from sapl.parlamentares.models import Filiacao

    filiacoes_parlamentar = Filiacao.objects.filter(
        parlamentar=parlamentar)

    filiacoes = filiacoes_parlamentar.filter(Q(
        data__lte=data_inicio,
        data_desfiliacao__isnull=True) | Q(
        data__lte=data_inicio,
        data_desfiliacao__gte=data_inicio))

    if data_fim:
        filiacoes = filiacoes | filiacoes_parlamentar.filter(
            data__gte=data_inicio,
            data__lte=data_fim)

    data_referencia = data_fim if data_fim else data_inicio

    return ' | '.join([f.nome_partido_ano(data_referencia.year).sigla for f in filiacoes])

def parlamentares_ativos(data_inicio, data_fim=None):
    from sapl.parlamentares.models import Mandato, Parlamentar
    '''
    :param data_inicio: define a data de inicial do período desejado
    :param data_fim: define a data final do período desejado
    :return: queryset dos parlamentares ativos naquele período
    '''
    mandatos_ativos = Mandato.objects.filter(Q(
        data_inicio_mandato__lte=data_inicio,
        data_fim_mandato__isnull=True) | Q(
        data_inicio_mandato__lte=data_inicio,
        data_fim_mandato__gte=data_inicio))
    if data_fim:
        mandatos_ativos = mandatos_ativos | Mandato.objects.filter(
            data_inicio_mandato__gte=data_inicio,
            data_inicio_mandato__lte=data_fim)
    else:
        mandatos_ativos = mandatos_ativos | Mandato.objects.filter(
            data_inicio_mandato__gte=data_inicio)

    parlamentares_id = mandatos_ativos.values_list(
        'parlamentar_id',
        flat=True).distinct('parlamentar_id')

    return Parlamentar.objects.filter(id__in=parlamentares_id)

def show_results_filter_set(qr):
    query_params = set(qr.keys())
    if ((len(query_params) == 1 and 'iframe' in query_params) or
            len(query_params) == 0):
        return False

    return True

def sort_lista_chave(lista, chave):
    """
    :param lista: Uma list a ser ordenada .
    :param chave: Algum atributo (chave) que está presente na lista e qual
    deve ser usado para a ordenação da nova
    lista.
    :return: A lista ordenada pela chave passada.
    """
    lista_ordenada = sorted(lista, key=itemgetter(chave))
    return lista_ordenada

def get_mime_type_from_file_extension(filename):
    ext = filename.split('.')[-1]
    if ext == 'odt':
        mime = 'application/vnd.oasis.opendocument.text'
    else:
        mime = "application/%s" % (ext,)
    return mime

def ExtraiTag(texto, posicao):
    for i in range(posicao, len(texto)):
        if (texto[i] == '>'):
            return i + 1

def TrocaTag(texto, startTag, endTag, sizeStart, sizeEnd, styleName, subinitiTag, subendTag):
    textoSaida = ''
    insideTag = 0
    i = 0
    if texto is None or texto.strip() == '':
        return texto
    if '
' in texto:
        texto = texto.replace('
', '')
        texto = texto.replace('
', '')
    if '
' in texto:
        texto = texto.replace('
', '')
        texto = texto.replace('
', '')
    while (i < len(texto)):
        shard = texto[i:i + sizeStart]
        if (shard == startTag):
            i = ExtraiTag(texto, i)
            textoSaida += subinitiTag + styleName + '">'
            insideTag = 1
        else:
            if (insideTag == 1):
                if (texto[i:i + sizeEnd] == endTag):
                    textoSaida += subendTag
                    insideTag = 0
                    i += sizeEnd
                else:
                    textoSaida += texto[i]
                    i += 1
            else:
                textoSaida += texto[i]
                i += 1

    return textoSaida

def RemoveTag(texto):
    textoSaida = ''
    i = 0

    while (i < len(texto)):

        if (texto[i] == '<'):
            i = ExtraiTag(texto, i)

        else:
            textoSaida += texto[i]
            i += 1

    return textoSaida

def remover_acentos(string):
    return unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode()

def mail_service_configured(request=None):

    logger = logging.getLogger(__name__)

    # if settings.DEBUG:
    #    return False
    if settings.EMAIL_RUNNING is None:
        result = True
        try:
            connection = get_connection()
            connection.open()
        except Exception as e:
            logger.error(str(e))
            result = False
        finally:
            connection.close()
        settings.EMAIL_RUNNING = result

    return settings.EMAIL_RUNNING

def lista_anexados(principal, isMateriaLegislativa=True):
    anexados_total = []
    if isMateriaLegislativa:  # MateriaLegislativa
        from sapl.materia.models import Anexada
        anexados_iterator = Anexada.objects.filter(materia_principal=principal)
    else:  # DocAdm
        from sapl.protocoloadm.models import Anexado
        anexados_iterator = Anexado.objects.filter(
            documento_principal=principal)

    anexadas_temp = list(anexados_iterator)

    while anexadas_temp:
        anx = anexadas_temp.pop()
        if isMateriaLegislativa:
            if anx.materia_anexada not in anexados_total:
                anexados_total.append(anx.materia_anexada)
                anexados_anexado = Anexada.objects.filter(
                    materia_principal=anx.materia_anexada)
                anexadas_temp.extend(anexados_anexado)
        else:
            if anx.documento_anexado not in anexados_total:
                anexados_total.append(anx.documento_anexado)
                anexados_anexado = Anexado.objects.filter(
                    documento_principal=anx.documento_anexado)
                anexadas_temp.extend(anexados_anexado)
    if principal in anexados_total:
        anexados_total.remove(principal)
    return anexados_total

def verifica_afastamento_parlamentar(parlamentar, data_inicio, data_fim=None):
    from sapl.parlamentares.models import AfastamentoParlamentar
    if data_fim:
        existe_afastamento = AfastamentoParlamentar.objects.filter(Q(parlamentar=parlamentar) &
                                                                   ((Q(data_inicio__lte=data_inicio) &
                                                                     Q(data_fim__gte=data_fim)) |
                                                                    (Q(data_inicio__lte=data_inicio) &
                                                                       Q(data_fim__isnull=True))
                                                                    )).exists()
    else:
        existe_afastamento = AfastamentoParlamentar.objects.filter(parlamentar=parlamentar,
                                                                   data_inicio__lte=data_inicio,
                                                                   data_fim__gte=data_inicio).exists()

    return existe_afastamento

def from_date_to_datetime_utc(data):
    """

    :param data: datetime.date
    :return: datetime.timestamp com UTC
    """
    import pytz
    from datetime import datetime

    # from date to datetime
    dt_unware = datetime.combine(data, datetime.min.time())
    dt_utc = pytz.utc.localize(dt_unware)
    return dt_utc

def gerar_pdf_impressos(request, context, template_name):
    template = loader.get_template(template_name)
    html = template.render(context, request)
    pdf = weasyprint.HTML(
        string=html, base_url=request.build_absolute_uri()).write_pdf()

    response = HttpResponse(pdf, content_type='application/pdf')
    response['Content-Disposition'] = 'inline; filename="relatorio_impressos.pdf"'
    response['Content-Transfer-Encoding'] = 'binary'

    return response


"""Manages general listeners like on_ready or on_command_error"""

import aiosqlite

from discord.ext import commands

class Info(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @commands.Cog.listener()
    async def on_ready(self):
        # Some debug info
        print(f"JoshGone logged on as {self.bot.user}.")
        print(f"SQLite version is {aiosqlite.sqlite_version}.")

    @commands.Cog.listener()
    async def on_command_error(self, ctx, error):
        # Unpack the error for cleaner error messages
        if isinstance(error, commands.CommandInvokeError):
            error = error.__cause__ or error
        try:
            await ctx.send(f"Oops, an error occurred: `{error!r}`")
        except Exception:
            print(f"Error: {error!r}")
            raise

def setup(bot):
    return bot.add_cog(Info(bot))


import serial
import parameter as param
import numpy as np

class Serial_COM:
    def __init__(self, bit_rate, dec):
        self.dec = dec
        self.bit_rate = bit_rate
        self.val = []
        # self.ser = serial.Serial(param.com_port, bit_rate, timeout=1)
        # self.ser = serial.Serial(param.com_port, bit_rate, timeout=1)

    def get_mean_std(self, cycle, label):
        ser = serial.Serial(param.com_port, self.bit_rate, timeout=1)
        mean = 0
        std_dev = 0

        for i in range(cycle):
            if self.ser.readable():
                vals.append(int(self.ser.readline().decode(self.dec)[-3:]) / 10)
                if(param.VIS): print(f'{i}: {vals[-1]}')
        self.ser.close()

        mean = np.mean(vals)
        std_dev = np.std(vals)

        del vals
        ser.close()

        return mean, std_dev


#!/usr/bin/env python3
import argparse
import sys

import vyper
from vyper.codegen.ir_node import IRnode
from vyper.ir import compile_ir, optimizer
from vyper.ir.s_expressions import parse_s_exp

def _parse_cli_args():
    return _parse_args(sys.argv[1:])

def _parse_args(argv):
    parser = argparse.ArgumentParser(description="Vyper IR IR compiler")
    parser.add_argument("input_file", help="Vyper sourcecode to compile")
    parser.add_argument(
        "--version", action="version", version=f"{vyper.__version__}+commit{vyper.__commit__}"
    )
    parser.add_argument(
        "-f",
        help="Format to print csv list of ir,opt_ir,asm,bytecode",
        default="bytecode",
        dest="format",
    )
    parser.add_argument(
        "--show-gas-estimates", help="Show gas estimates in ir output mode.", action="store_true"
    )

    args = parser.parse_args(argv)
    output_formats = set(dict.fromkeys(args.format.split(",")))
    compiler_data = compile_to_ir(args.input_file, output_formats, args.show_gas_estimates)

    for key in ("ir", "opt_ir", "asm", "bytecode"):
        if key in compiler_data:
            print(compiler_data[key])

def compile_to_ir(input_file, output_formats, show_gas_estimates=False):
    with open(input_file) as fh:
        s_expressions = parse_s_exp(fh.read())

    if show_gas_estimates:
        IRnode.repr_show_gas = True

    compiler_data = {}
    ir = IRnode.from_list(s_expressions[0])
    ir = optimizer.optimize(ir)
    if "ir" in output_formats:
        compiler_data["ir"] = ir

    asm = compile_ir.compile_to_assembly(ir)
    if "asm" in output_formats:
        compiler_data["asm"] = asm

    if "bytecode" in output_formats:
        bytecode, _ = compile_ir.assembly_to_evm(asm)
        compiler_data["bytecode"] = "0x" + bytecode.hex()

    return compiler_data

if __name__ == "__main__":
    _parse_cli_args()


'''
Read a z3d
Plot the time series
Use datashader to handle large time series data
'''

# from glob import glob
from datetime import timedelta

import numpy as np
import pandas as pd
import holoviews as hv
from holoviews import renderer,Curve
from holoviews.operation.datashader import datashade, shade, dynspread
from bokeh.models.formatters import DatetimeTickFormatter
from bokeh.models import tools as btools
from bokeh.models import Button
from bokeh.models.widgets import Select, PreText
from bokeh.layouts import layout
# import panel as pn

from .z3dio import read_z3d
from . import timeio
# import .process

# plot settings
# brenderer = renderer('bokeh')
hv.config.image_rtol = 0.1
dynspread.max_px=20
dynspread.threshold=0.5
shade.cmap="#30a2da" # to match HV Bokeh default
shade.cmap="#3062aa"
# shade.cmap=hv.plotting.util.process_cmap('Blues')

# limit doc
__all__ = ['verbose_formatter','no_logo','plot_ts']

def verbose_formatter():
    '''
    Format bokeh time axis verbosely
    '''
    vf = DatetimeTickFormatter()
    vf.microseconds = ['%f us']
    vf.milliseconds = ['%S.%3N s']
    # vf.milliseconds = ['%H:%M:%S.%3N']
    vf.seconds = ['%H:%M:%S']
    vf.minsec = ['%H:%M:%S']
    vf.minutes = ['%m/%d %H:%M']
    vf.hourmin = ['%m/%d %H:%M']
    vf.hours = ['%m/%d %H:%M']
    vf.days = ['%m/%d', '%a%d']
    vf.months = ['%m/%Y', '%b %Y']
    vf.years = ['%Y']
    return vf

def no_logo(plot, element):
    plot.state.toolbar.logo = None

def plot_ts(filename,time_range=[None,None]):
    '''
    Plot the time series in z3d file filename
    return a holoviews dynspread object
    time_range must be tz-aware
    '''
    # print('reading...')
    z3d = read_z3d(filename)
    sampling_rate = z3d['metadata']['A/D Rate']
    if 'TX.SENSE' in z3d['metadata']:
        # NOTE: this only ids channel 1 TX files
        station_type = 'TX'
    else:
        station_type = 'RX'
    # These items were in schedule_info for RX
    try:
        station = z3d['metadata']['CH.STN']
    except KeyError:
        try:
            station = z3d['metadata']['RX.STN']
        except KeyError:
            station = ''
    try:
        station = str(int(station))
    except ValueError:
        station = str(station)
    channel = z3d['metadata']['Channel']
    component = z3d['metadata']['CH.CMP']
    if z3d['metadata']['CH.CMP'][0] == 'H':
        # mag field
        # antenna_number = z3d['metadata']['CH.ANTSN']
        pass
    elif z3d['metadata']['CH.CMP'][0] == 'E':
        # electric field
        # antenna_number = None
        pass
    elif z3d['metadata']['CH.CMP'][0] == 'R':
        # electric field, transmitter ref channel
        # antenna_number = None
        pass
    else:
        raise ValueError('CH.CMP not recognized as either E or H component')
    if z3d['num_records']==0:
        valid = False
    else:
        [dt_start,dt_end] = timeio.get_start_and_end_times_mountain(z3d,include_final_second=True,astype='timestamp')

    # print('getting times...')
    # dt_start_naive = dt_start.replace(tzinfo=None)
    # dt_end_naive = dt_end.replace(tzinfo=None)
    # dt_start_naive = pd.Timestamp(dt_start.replace(tzinfo=None))
    # dt_end_naive = pd.Timestamp(dt_end.replace(tzinfo=None))
    # period = timedelta(seconds=1)/sampling_rate
    # sampling_increment = timedelta(seconds=1./sampling_rate)
    # dt_times = pd.date_range(start=dt_start_naive, periods=len(z3d['data']),
    #                          freq=sampling_increment, tz=None,
    #                          normalize=False, name=None, closed=None)
    dt_times = pd.date_range(start=dt_start, end=dt_end,
                          periods=len(z3d['data']), tz=None,
                          normalize=False, name=None, closed=None)#.values

    # print('windowing data...')
    # print(type(dt_start))
    # print(type(time_range[0]))
    # window data to time_range
    if time_range[0]:
        dt_start = time_range[0]
    if time_range[1]:
        dt_end = time_range[1]
    window_slice = timeio.slice_to_window_series(dt_times,dt_start,dt_end)
    curve_data = z3d['data'][window_slice]
    dt_times = dt_times[window_slice]
    # ... or don't
    # curve_data = z3d['data']
    # print('windowed!')

    # holoviews seems to want timezone-naive type datetime64[ns]. Throws warning otherwise
    try:
        dt_times = dt_times.tz_localize(None).astype('datetime64[ns]')
        # time_range[0] = time_range[0].tz_localize(None)
        # time_range[1] = time_range[1].tz_localize(None)
        # hv_time_range = (time_range[0].tz_localize(None), time_range[1].tz_localize(None))
        hv_time_range = (dt_start.tz_localize(None), dt_end.tz_localize(None))
        # print(type(dt_times))
        # print(type(dt_times[0]))
        # print(dt_times[0])
        # print(type(dt_start))
        # print(dt_start)
        dt_start = np.datetime64(dt_start.tz_localize(None))
        # print(type(dt_start))
        # print(dt_start)
        dt_end = np.datetime64(dt_end.tz_localize(None))
    except Exception as e:
        print(e)
        traceback.print_exc()

    # print(len(curve_data))
    # print(len(dt_times))
    # print(hv_time_range)

    # print('plotting...')
    title_str = '{} Station: {}, '+\
        'Channel {:.0f} ({}), {} to {} Mountain'
    title_str = title_str.format(station_type, station,
                                 channel, component,
                                 np.datetime_as_string(dt_start, unit='s'),
                                 np.datetime_as_string(dt_end, unit='s'))
    ts_tools = ['save','pan','xwheel_zoom','box_zoom','undo','reset']
    signal = hv.Dimension('signal',label='Signal',unit='V')
    time = hv.Dimension('time',label='Time',range=hv_time_range)
    curve = Curve((dt_times, curve_data),
                  time,
                  signal,
                  label=title_str
                 )#.redim.range(time=time_range)
                     # kdims=['Time'],vdims=['Signal, V']
    # dscurve = datashade(curve, cmap=["blue"]).opts(width=800)
    # dscurve = datashade(curve,name=title_str).opts(width=800)
    dscurve = dynspread(datashade(curve).opts(#framewise=True,
                                              xformatter=verbose_formatter(),
                                              default_tools=ts_tools))
    dsfinal=dscurve.opts(plot=dict(hooks=[no_logo]))
    # print('done!')
    return dsfinal

class QuickPlot():
    # plot some time series!
    start = 0
    end = None

    ts_tools = ['save','pan','xwheel_zoom','box_zoom','undo','reset']

    def ds(self,curve):
        # apply datashading and dynspread to a curve
        if self.end:
            dscurve = hv.operation.datashader.dynspread(hv.operation.datashader.datashade(curve).opts(
                xformatter=verbose_formatter(),
                default_tools=self.ts_tools))
        else:
            dscurve = hv.operation.datashader.dynspread(hv.operation.datashader.datashade(curve).opts(
                default_tools=self.ts_tools))
        dsfinal=dscurve.opts(plot=dict(hooks=[no_logo]))
        return dsfinal

    def times(self,ts):
        if self.end:
            dt_times = pd.date_range(start=self.start, end=self.end,
                          periods=len(ts), tz=None,
                          normalize=False, name=None, closed=None)#.values
            dt_times = dt_times.tz_localize(None).astype('datetime64[ns]')
            hv_time_range = (self.start.tz_localize(None), self.end.tz_localize(None))
            #dt_start = np.datetime64(dt_start.tz_localize(None))
            #dt_end = np.datetime64(dt_end.tz_localize(None))
            x_dim = hv.Dimension('time',label='Time',range=hv_time_range)

        else:
            dt_times = np.arange(len(ts))
            x_dim = hv.Dimension('sample',label='Sample number',range=(0,len(ts)))
        return (dt_times,x_dim)

    def p1(self,ts,x_arr=None):
        signal = hv.Dimension('signal',label='Signal')

        if x_arr is not None:
            # User-specified x axis
            dt_times = x_arr
            x_dim = hv.Dimension('x',label='x')
        # assume 4096 Hz
        elif self.end:
            dt_times = pd.date_range(start=self.start, end=self.end,
                          periods=len(ts), tz=None,
                          normalize=False, name=None, closed=None)#.values
            dt_times = dt_times.tz_localize(None).astype('datetime64[ns]')
            hv_time_range = (self.start.tz_localize(None), self.end.tz_localize(None))
            #dt_start = np.datetime64(dt_start.tz_localize(None))
            #dt_end = np.datetime64(dt_end.tz_localize(None))
            x_dim = hv.Dimension('time',label='Time',range=hv_time_range)

        else:
            dt_times = np.arange(len(ts))
            x_dim = hv.Dimension('sample',label='Sample number',range=(0,len(ts)))

        curve = hv.Curve((dt_times, ts),x_dim,signal)
        p = self.ds(curve).opts(height=250,width=800)
        return p

    def p2(self,tx_ts,rx_ts,xlim=None,rxlim=None,txlim=None):
        assert len(tx_ts)==len(rx_ts), 'tx and rx time series must be the same length'

        tx_signal = hv.Dimension('tx_signal',label='TX Signal',unit='A')
        rx_signal = hv.Dimension('rx_signal',label='RX Signal',unit='V')

        # assume 4096 Hz
        if self.end:
            dt_times = pd.date_range(start=self.start, end=self.end,
                          periods=len(tx_ts), tz=None,
                          normalize=False, name=None, closed=None)#.values
            dt_times = dt_times.tz_localize(None).astype('datetime64[ns]')
            hv_time_range = (self.start.tz_localize(None), self.end.tz_localize(None))
            #dt_start = np.datetime64(dt_start.tz_localize(None))
            #dt_end = np.datetime64(dt_end.tz_localize(None))
            x_dim = hv.Dimension('time',label='Time',range=hv_time_range)

        else:
            dt_times = np.arange(len(tx_ts))
            x_dim = hv.Dimension('sample',label='Sample number')

        tx_curve = hv.Curve((dt_times, tx_ts),x_dim,tx_signal)
        rx_curve = hv.Curve((dt_times, rx_ts),x_dim,rx_signal)
        txp = self.ds(tx_curve).opts(height=200,width=800)
        rxp = self.ds(rx_curve).opts(height=200,width=800)
        if xlim:
            txp = txp.opts(xlim=xlim)
            rxp = rxp.opts(xlim=xlim)
        if rxlim:
            rxp = rxp.opts(ylim=rxlim)
        if txlim:
            txp = txp.opts(ylim=txlim)
        return (txp+rxp).cols(1)

def _old_plot_series(ts,ts2=None,sampling_rate=4096):
    '''
    Time series plots
    Plot one or two time series on the same x-axis
    '''
    fig,ax1=plt.subplots(figsize=(9,6))
    c1 = 'tab:blue'
    ax1.set_xlabel('seconds')
    time_in_ms = np.arange(len(ts))/float(sampling_rate)
    ax1.plot(time_in_ms,ts,color=c1)
    ax1.tick_params(axis='y',labelcolor=c1)
    ax1.set_ylabel('Signal 1',color=c1)
    if ts2 is not None:
        c2 = 'tab:red'
        ax2 = ax1.twinx()
        ax2.plot(time_in_ms,ts2,color=c2)
        ax2.set_ylabel('Signal 2',color=c2)
        ax2.tick_params(axis='y',labelcolor=c2)
    fig.tight_layout()
    # plt.show()
    if ts2 is not None:
        return(fig,ax1,ax2)
    else:
        return(fig,ax1)

def _old_plot_ts(txfilename,rxfilename,rx_station):
    tx=z3dio.read_z3d(txfilename)
    rx=z3dio.read_z3d(rxfilename)

    sampling_rate = round(rx['metadata']['A/D Rate'])
    tx_mountain_times = timeio.gps_week_seconds_to_mountain(tx['metadata']['GpsWeek'],tx['gps_times'])
    rx_mountain_times = timeio.gps_week_seconds_to_mountain(rx['metadata']['GpsWeek'],rx['gps_times'])
    [tx_start,tx_end] = timeio.get_start_and_end_times_mountain(tx)
    [rx_start,rx_end] = timeio.get_start_and_end_times_mountain(rx)

    rx_overlap_start_second = rx_mountain_times.index(tx_start)
    rx_start_index = rx_overlap_start_second*sampling_rate
    rx_end_index = rx_start_index + len(tx['data'])

    tx_start_naive = tx_start.replace(tzinfo=None)

    # form array of datetimes without pandas
    # TODO: stop assuming total overlap of tx and rx
    # TODO: stop assuming same sampling rate
    period = timedelta(seconds=1)/sampling_rate
    tx_times = tx_start_naive+np.arange(len(tx['data']))*period
    # plot with matplotlib
    fig,ax1=plt.subplots(figsize=(9,6))
    mpl_times = matplotlib.dates.date2num(tx_times)
    c1 = 'tab:blue'
    c2 = 'tab:red'
    ax1.set_xlabel('Date/Time, Mountain Time')
    ax1.plot_date(mpl_times,tx['data'],fmt='-',color=c1)
    ax1.tick_params(axis='y',labelcolor=c1)
    if 'TX.SENSE' in rx['metadata']:
        ax1.set_ylabel('Transmitter Current, A',color=c1)
    else:
        ax1.set_ylabel('Transmitter Signal, V',color=c1)
    ax2 = ax1.twinx()
    ax2.set_ylabel('Receiver Signal, V',color=c2)
    ax2.plot_date(mpl_times,rx['data'][rx_start_index:rx_end_index],fmt='-',color=c2)
    ax2.tick_params(axis='y',labelcolor=c2)
    # datetimeformat = "%Y-%m-%d %H:%M:%S"

    # show it!
    # plt.title(ntpath.basename(filename))
    title_string = 'TX: '+str(int(tx['metadata']['RX.STN']))+', RX: '+\
                   str(int(rx_station))+', '+str(tx['metadata']['Tx.Freq'])+\
                   ' Hz, '+rx['metadata']['CH.CMP']
    plt.title(title_string)
    fig.tight_layout()
    plt.show()


from libs.comnetpr.Graph import *
from libs.comnetpr.Graph import *
from libs.comnetpr.Algorithms import *
from mpl_toolkits.mplot3d import Axes3D
import pandas as pd
import matplotlib.pyplot as plt

graph = CreateGraph()
graph.readCSV('graph1.csv')
#graph.readCSV('internal.csv')

df = pd.DataFrame(graph.getNodeEdge()[1], columns=['Source', 'Destination', 'Cost'])

fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
ax.title.set_text('Source-Destination-Cost 3D')

x = df['Source']
y = df['Destination']
z = df['Cost']

ax.scatter(x, y, z)
ax.set_xlabel("Source")
ax.set_ylabel("Destination")
ax.set_zlabel("Cost")

plt.show()


from setuptools import setup
import sys, os

version = '0.2'

long_description = (
    open('README.txt').read()
    + '\n' +
    open('CHANGES.txt').read())

setup(name='typeinfo',
      version=version,
      description='A library to add optional type information to python objects.',
      long_description=long_description,
      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
      keywords='',
      author='TypeInfo Developers',
      author_email='b.leskes@gmail.com',
      url='',
      license='apache',
      py_modules=['typeinfo'],
      package_dir={ "": "src"},
      zip_safe=False,
      install_requires=[
      ])


import re

from PySide2.QtGui import QCursor, QIcon, QTextCursor
from PySide2.QtWidgets import QApplication, QFileDialog, QTabBar, QTableWidgetItem, QMenu, QTextEdit, QTableWidget, \
    QAbstractItemView, QMessageBox, QInputDialog, QDialog, QLabel, QLineEdit, QHeaderView, QWidget, QComboBox
from PySide2.QtCore import QPoint, QModelIndex, Qt

import ipaddress  # check input for IP
import netifaces # Get ifaces ip addr
import html # To escape text from snippets

from core.database import Database
from core.config import Config
from ui.custom_command import Custom_Command
from ui.new_scan import New_Scan
from ui.search import Search
from ui.set_variables import Set_Variables
from ui.about import About
from ui.credentials import Credentials_dialog
from utils.QNoteTextEdit import QNoteTextEdit

class View:
    def __init__(self, main_window, controller):
        self.ui = main_window
        self.controller = controller

        self.default_tabs = [{'name': 'Ports', 'object': type(QTableWidget())},
                             {'name': 'Nmap', 'object': type(QTextEdit())},
                             {'name': 'Credentials', 'object': type(QTableWidget())},
                             {'name': 'Notes', 'object': type(QNoteTextEdit())}]
        self.app_tabs = dict()

        self.setup_ui()
        self.connect_slots()

    def setup_ui(self):
        self.ui.ui.host_list.setColumnHidden(0, True)
        self.ui.ui.hosts_for_port_table.setColumnHidden(0, True)
        self.ui.ui.creds_table.setColumnHidden(0, True)
        self.ui.ui.log_table.setColumnHidden(0, True)
        self.ui.ui.job_table.setColumnHidden(0, True)
        self.ui.ui.job_table.setColumnHidden(1, True)
        self.ui.ui.job_table.setColumnHidden(2, True)
        self.ui.ui.job_table.setColumnHidden(6, True)
        self.ui.ui.hosts_for_port_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.ui.ui.creds_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.ui.ui.actionEnable_automatic_tools.setChecked(Config.get()['user_prefs']['enable_autorun'])
        self.ui.ui.actionAutosave_database_every_5_mins.setChecked(Config.get()['user_prefs']['autosave'])
        self.ui.ui.actionEnable_automatic_tools_on_import.setChecked(Config.get()['user_prefs']['enable_autorun_on_xml_import'])
        self.ui.ui.host_list.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.reset_ui_snippets()

    def reset_ui_snippets(self):
        for interface_name in Config.get()['user_prefs']['preferred_interfaces']:
            if interface_name in netifaces.interfaces() and netifaces.AF_INET in netifaces.ifaddresses(interface_name).keys():
                self.ui.ui.lhost.setText(netifaces.ifaddresses(interface_name)[netifaces.AF_INET][0]['addr'])
                break
        self.ui.ui.lport.setText(str(Config.get()['user_prefs']['preferred_lport']))
        self.setup_ui_snippets_body()

    def setup_ui_snippets_body(self):
        # Good luck.
        def create_subsection(level: int, content):
            final_subsection = ""
            for i, chunk in enumerate(content):
                current_string = ""
                if isinstance(chunk, str):
                    for char in set(self.ui.ui.escaped_chars.text()):
                        chunk = chunk.replace(char, '\\' + char)
                    for char in set(self.ui.ui.urlencoded_chars.text()):
                        chunk = chunk.replace(char, '%' + str(ord(char)))
                    chunk = html.escape(chunk)
                    if i != len(content)-1 and isinstance(content[i+1], list):
                        current_string += f'<h{level + 4}>{chunk}</h{level + 4}>'
                    else:
                        current_string += chunk + '
'
                elif isinstance(chunk, list):
                    if final_subsection and '<h' not in final_subsection:
                        final_subsection = f'
{final_subsection}
'
                    current_string += create_subsection(level+1, chunk)

                if '<h' in current_string:
                    if (i == len(content)-1 or isinstance(content[i+1], list)) and final_subsection and '<h' not in final_subsection and '<p' not in final_subsection:
                        final_subsection = f'
{final_subsection}
'
                    final_subsection += current_string
                else:
                    final_subsection += current_string
                    if (i == len(content)-1 or isinstance(content[i+1], list)) and final_subsection and '<h' not in final_subsection and '<p' not in final_subsection:
                        final_subsection = f'
{final_subsection}
'

            if final_subsection and '<h' not in final_subsection:
                final_subsection = f'
{final_subsection}
'

            # Clean-up
            final_subsection = re.sub('(
]+>)(
]+>)+', '\\1', final_subsection)
            final_subsection = re.sub('(
)(
)+', '\\1', final_subsection)
            final_subsection = re.sub('

', '
', final_subsection)

            return final_subsection

        self.ui.ui.snippets_tabs.clear()
        tabs = {}
        for i, tab in enumerate(Config.get()['snippets']):
            tabs[i] = QTextEdit()
            if not isinstance(Config.get()['snippets'][tab], list):
                continue
            section = ""
            for element in Config.get()['snippets'][tab]:
                if isinstance(element, str):
                    section += f'
{element}
'
                if isinstance(element, list):
                    section += create_subsection(0, element)

            section = section.replace("%%%LHOST%%%", self.ui.ui.lhost.text())
            section = section.replace("%%%LPORT%%%", self.ui.ui.lport.text())

            tabs[i].setHtml(section)
            tabs[i].moveCursor(QTextCursor.Start)
            self.ui.ui.snippets_tabs.addTab(tabs[i], tab)

    def connect_slots(self):
        self.ui.ui.actionNew.triggered.connect(self.controller.clear_db)
        self.ui.ui.actionOpen.triggered.connect(self.open_db)
        self.ui.ui.actionSave.triggered.connect(self.save_db)
        self.ui.ui.actionSaveAs.triggered.connect(self.save_as_db)
        self.ui.ui.actionExit.triggered.connect(self.ui.close)
        self.ui.ui.actionSet_variables.triggered.connect(self.set_variables)
        self.ui.ui.actionSearch_string.triggered.connect(self.dialog_search)
        self.ui.ui.actionReload_configuration_from_file.triggered.connect(self.controller.reload_conf)
        self.ui.ui.actionAbout.triggered.connect(self.about)
        self.ui.ui.actionAbout_Qt.triggered.connect(QApplication.aboutQt)
        self.ui.ui.ImportNmap.clicked.connect(self.dialog_import_xml)
        self.ui.ui.ScanHost.clicked.connect(self.dialog_scan_host)
        self.ui.ui.host_list.clicked.connect(self.update_right_panel)
        self.ui.ui.button_play.clicked.connect(self.clicked_button_play)
        self.ui.ui.button_pause.clicked.connect(self.clicked_button_pause)
        self.ui.ui.button_stop.clicked.connect(self.clicked_button_stop)
        self.ui.ui.job_table.clicked.connect(self.enable_buttons)
        self.ui.ui.host_list.doubleClicked.connect(self.copy_ip_in_clipboard_from_hostlist)
        self.ui.ui.host_list.customContextMenuRequested.connect(self.right_click_in_host_table)
        self.ui.ui.job_table.customContextMenuRequested.connect(self.right_click_in_job_table)
        self.ui.ui.log_table.customContextMenuRequested.connect(self.right_click_in_log_table)
        self.ui.ui.application_TabWidget.tabCloseRequested.connect(self.remove_tab)
        self.ui.ui.host_list_filter.textEdited.connect(self.apply_filter_on_host_list)
        self.ui.ui.creds_table_filter.textEdited.connect(self.apply_filter_on_creds_table)
        self.ui.ui.creds_table.customContextMenuRequested.connect(self.right_click_in_creds_table)
        self.ui.ui.creds_table_copy_selection_to_clipboard.clicked.connect(lambda: self.copy_selectedIndexes_to_clipboard(self.ui.ui.creds_table.selectedIndexes()))
        self.ui.ui.creds_table_copy_all_to_clipboard.clicked.connect(self.creds_table_copy_all_to_clipboard)
        self.ui.ui.actionEnable_automatic_tools.triggered.connect(lambda checked: Config.set(["user_prefs", "enable_autorun"], checked))
        self.ui.ui.actionEnable_automatic_tools_on_import.triggered.connect(lambda checked: Config.set(["user_prefs", "enable_autorun_on_xml_import"], checked))
        self.ui.ui.reset_lhost_lport.clicked.connect(self.reset_ui_snippets)
        self.ui.ui.lhost.textEdited.connect(self.setup_ui_snippets_body)
        self.ui.ui.lport.textEdited.connect(self.setup_ui_snippets_body)
        self.ui.ui.escaped_chars.textEdited.connect(self.setup_ui_snippets_body)
        self.ui.ui.urlencoded_chars.textEdited.connect(self.setup_ui_snippets_body)
        self.ui.ui.port_table.itemSelectionChanged.connect(self.change_filter_hosts_for_port)
        self.ui.ui.machine_list_copy_selection_to_clipboard.clicked.connect(lambda: self.copy_selectedIndexes_to_clipboard(self.ui.ui.hosts_for_port_table.selectedIndexes()))
        self.ui.ui.machine_list_copy_all_to_clipboard.clicked.connect(self.machine_list_copy_all_to_clipboard)
        self.ui.ui.hosts_for_port_table.customContextMenuRequested.connect(self.right_click_in_hosts_for_port_table)

    def about(self):
        about_dialog = About()
        about_dialog.setWindowTitle("About this program")
        about_dialog.ui.text.setText(f"
QtRecon {self.controller.APPLICATION_VERSION}

2023, licenced under Creative Commons CC-BY

Thanks to my friends, for all the advices they gave me and the beta-testing while developping this thing

https://github.com/bouligo/cuterecon/")
        about_dialog.ui.image.setPixmap("icons/icon.ico")
        about_dialog.ui.button.clicked.connect(about_dialog.close)
        about_dialog.exec()

    def dialog_search(self):
        search_dialog = Search()
        search_dialog.ui.search_button.clicked.connect(lambda: self.search(search_dialog))
        search_dialog.ui.host_list.currentRowChanged.connect(lambda: self.search_in_host(search_dialog))
        search_dialog.exec()

    def search(self, search_dialog: QDialog):
        search_dialog.ui.host_list.clear()
        search_dialog.ui.results.clear()
        search_dialog.ui.host_list.addItems(self.controller.search_string_in_db_hosts(search_dialog.ui.search_input.text()))

    def search_in_host(self, search_dialog: QDialog):
        if search_dialog.ui.host_list.currentItem() is None:
            return
        search_dialog.ui.results.setText('\n'.join(
            self.controller.search_string_in_db_hosts(search_dialog.ui.search_input.text(),
            search_dialog.ui.host_list.currentItem().text())))

    def set_variables(self):
        set_variables_dialog = Set_Variables()
        config_variables = Config.get()['user_variables']
        dialog_variables = {} #Storing Widgets to get inputs easily
        for i, variable in enumerate(config_variables):
            input_field = QLineEdit(config_variables[variable])
            dialog_variables[variable] = input_field
            set_variables_dialog.ui.formLayout.insertRow(i, QLabel(variable + ':'), input_field)

        if not set_variables_dialog.exec():
            return

        for variable in dialog_variables:
            Config.set(["user_variables", variable], dialog_variables[variable].text())

    def select_credentials_dialog(self, credentials: list) -> (str, str, str, str):
        select_credentials_dialog = Credentials_dialog()
        for i, credential in enumerate(credentials):
            select_credentials_dialog.ui.creds_table.insertRow(select_credentials_dialog.ui.creds_table.rowCount())
            select_credentials_dialog.ui.creds_table.setItem(i, 0, QTableWidgetItem(credential['type']))
            select_credentials_dialog.ui.creds_table.setItem(i, 1, QTableWidgetItem(credential['domain']))
            select_credentials_dialog.ui.creds_table.setItem(i, 2, QTableWidgetItem(credential['username']))
            select_credentials_dialog.ui.creds_table.setItem(i, 3, QTableWidgetItem(credential['password']))
        select_credentials_dialog.ui.creds_table.selectRow(0)
        select_credentials_dialog.exec()

        if select_credentials_dialog.result():
            # Returns type, domain, username and password
            return [cell.data() for cell in select_credentials_dialog.ui.creds_table.selectedIndexes()]
        else:
            return None, None, None, None

    def enable_buttons(self, index: QModelIndex):
        job_details = self.controller.get_job_details(index)
        if job_details['state'] == 'Running':
            self.ui.ui.button_play.setEnabled(False)
            self.ui.ui.button_pause.setEnabled(True)
            self.ui.ui.button_stop.setEnabled(True)
        elif job_details['state'] == 'NotRunning':
            self.ui.ui.button_play.setEnabled(True)
            self.ui.ui.button_pause.setEnabled(False)
            self.ui.ui.button_stop.setEnabled(True)
        else:
            self.ui.ui.button_play.setEnabled(False)
            self.ui.ui.button_pause.setEnabled(False)
            self.ui.ui.button_stop.setEnabled(False)

    def refresh_buttons(self):
        if self.ui.ui.job_table.selectedIndexes():
            self.enable_buttons(self.ui.ui.job_table.selectedIndexes()[0])

    def clicked_button_play(self):
        selected_row = self.ui.ui.job_table.selectedIndexes()
        job_details = self.controller.get_job_details(selected_row[0])  # Choosing first cell to get the job details
        self.controller.resume_job(job_details['id'])
        self.ui.ui.button_play.setEnabled(False)
        self.ui.ui.button_pause.setEnabled(True)

    def clicked_button_pause(self):
        selected_row = self.ui.ui.job_table.selectedIndexes()
        job_details = self.controller.get_job_details(selected_row[0])  # Choosing first cell to get the job details
        self.controller.pause_job(job_details['id'])
        self.ui.ui.button_play.setEnabled(True)
        self.ui.ui.button_pause.setEnabled(False)

    def clicked_button_stop(self):
        selected_row = self.ui.ui.job_table.selectedIndexes()
        job_details = self.controller.get_job_details(selected_row[0]) # Choosing first cell to get the job details
        self.controller.kill_job(job_details['id'])
        self.ui.ui.button_play.setEnabled(False)
        self.ui.ui.button_pause.setEnabled(False)
        self.ui.ui.button_stop.setEnabled(False)

    def apply_filter_on_host_list(self, search_input: str):
        self.controller.filter_hostlist(search_input)

    def apply_filter_on_creds_table(self, search_input: str):
        self.controller.filter_credstable(search_input)

    def copy_ip_in_clipboard_from_hostlist(self, index: QModelIndex):
        host = self.controller.get_selected_host(index)
        clipboard = QApplication.clipboard()
        clipboard.setText(host['ip'])
        self.ui.statusBar().showMessage('IP copied to clipboard !', 5000)

    def right_click_in_host_table(self, qpoint: QPoint):
        item = self.ui.ui.host_list.indexAt(qpoint)
        if item.row() == -1:
            return

        self.update_right_panel(item)
        host = self.controller.get_selected_host(item)

        top_menu = QMenu(None)
        set_ip_action = top_menu.addAction("Change IP")
        set_hostname_action = top_menu.addAction("Set / Change hostname")
        set_os_action = top_menu.addAction("Set / Change OS")
        set_pwned = top_menu.addAction("(Un-)Mark as pwned")
        set_color = top_menu.addMenu("Highlight with color ...")
        color_menu_dict = {}

        color_menu_dict['None'] = set_color.addAction('None')
        set_color.addSeparator()
        # list of colors from doc: https://doc.qt.io/qtforpython-5/PySide2/QtCore/Qt.html#PySide2.QtCore.PySide2.QtCore.Qt.GlobalColor
        for color in ["white", "red", "green", "cyan", "magenta", "yellow", "darkRed", "darkGreen", "blue", "darkBlue", "darkCyan", "darkMagenta", "darkYellow", "gray", "darkGray", "lightGray", "black"]:
            color_menu_dict[color] = set_color.addAction(color)

        top_menu.addSeparator()
        autorun_action = top_menu.addAction("Run all autorun configured programs")
        top_menu.addSeparator()
        remove_action = top_menu.addAction("Delete host and data")

        action = top_menu.exec_(QCursor.pos())
        if not action:
            return

        if action == remove_action:
            self.controller.delete_host(item)
            self.ui.ui.host_list.clearSelection()
            self.close_right_panel_tabs()
        elif action == set_ip_action:
            new_ip, confirmed = QInputDialog.getText(None, "Change IP", "Set the IP address to :", text = host['ip'])
            if confirmed:
                try:
                    ipaddress.IPv4Address(new_ip) # fails if not a valid IP address
                    if host['ip'] != new_ip:
                        self.controller.change_host_ip(item, new_ip)
                        self.close_right_panel_tabs()
                except ipaddress.AddressValueError:
                    QMessageBox.critical(None, "Cannot change IP", f"Cannot change ip address of \"{host['ip']}\" to \"{new_ip}\"")
        elif action == set_hostname_action:
            new_hostname, confirmed = QInputDialog.getText(None, "Set hostname", "Set the hostname to :", text = host['hostname'])
            if confirmed:
                self.controller.change_host_hostname(item, new_hostname)
                self.close_right_panel_tabs()
        elif action == set_os_action:
            new_os, confirmed = QInputDialog.getItem(None, "Set Operating System", "Set the OS to :", ["Windows", "Linux", "IOS", "Unknown"], editable=False)
            if confirmed:
                self.controller.change_host_os(item, new_os)
                self.close_right_panel_tabs()
        elif action == set_pwned:
            self.controller.set_host_pwned(item)
        elif action in color_menu_dict.values():
            if action.text() == 'None': # 'd better not trust this ? Use the key from color_menu_dict instead ?
                self.controller.set_host_highlight_color(item, "")
            else:
                 self.controller.set_host_highlight_color(item, action.text())
        elif action == autorun_action:
            self.controller.autorun([host['id']])

    def right_click_in_log_table(self, qpoint: QPoint):
        item = self.ui.ui.log_table.indexAt(qpoint)
        log = self.ui.ui.log_table.model().itemData(item)

        if not log:
            return

        top_menu = QMenu(None)
        clipboard_action = top_menu.addAction("Copy log into clipboard")
        clipboard_action.setIcon(QIcon.fromTheme("edit-copy"))

        action = top_menu.exec_(QCursor.pos())
        if not action:
            return

        if action == clipboard_action:
            clipboard = QApplication.clipboard()
            clipboard.setText(log['log'])

    def right_click_in_job_table(self, qpoint: QPoint):
        item = self.ui.ui.job_table.indexAt(qpoint)
        job = self.controller.get_job_details(item)

        if not job:
            return

        command = job['command']

        top_menu = QMenu(None)
        clipboard_action = top_menu.addAction("Copy command into clipboard")
        clipboard_action.setIcon(QIcon.fromTheme("edit-copy"))

        action = top_menu.exec_(QCursor.pos())
        if not action:
            return

        if action == clipboard_action:
            clipboard = QApplication.clipboard()
            clipboard.setText(command)

    def right_click_in_port_table(self, qpoint: QPoint):
        item = self.app_tabs['Ports'].itemAt(qpoint)
        if item is None:
            return

        host = self.ui.ui.host_list.model().itemData(self.ui.ui.host_list.currentIndex())
        port = self.app_tabs['Ports'].item(item.row(), 2).text()
        proto = self.app_tabs['Ports'].item(item.row(), 0).text()
        description = self.app_tabs['Ports'].item(item.row(), 3).text()

        top_menu, visible_actions = self.create_actions_from_port(proto, port)

        custom_command = top_menu.addAction("Custom command ...")
        copy_description = top_menu.addAction("Copy service description into clipboard")
        copy_description.setIcon(QIcon.fromTheme("edit-copy"))
        copy_uri = top_menu.addAction(f"Copy \"{host['ip']}:{port}\" into clipboard")
        copy_uri.setIcon(QIcon.fromTheme("edit-copy"))
        action = top_menu.exec_(QCursor.pos())

        if not action:
            return

        if action == copy_description:
            clipboard = QApplication.clipboard()
            clipboard.setText(description)

        if action == copy_uri:
            clipboard = QApplication.clipboard()
            clipboard.setText(f"{host['ip']}:{port}")

        if action == custom_command:
            self.launch_custom_command_dialog()

        for program in visible_actions:
            if program['text'] == action.text():
                self.controller.new_job(program)
                return

    def right_click_in_hosts_for_port_table(self, qpoint: QPoint):
        item = self.ui.ui.hosts_for_port_table.indexAt(qpoint) # But we're not using this here, as multiple selection can occur
        if item is None:
            return

        proto = self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),0).text()
        port = self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),1).text()
        hosts = dict()
        for index in self.ui.ui.hosts_for_port_table.selectedIndexes():
            itemData = self.ui.ui.hosts_for_port_table.model().itemData(index)
            if itemData['id'] not in hosts.keys():
                hosts[itemData['id']] = itemData

        top_menu, visible_actions = self.create_actions_from_port(proto, port)
        custom_command = top_menu.addAction("Custom command ...")
        action = top_menu.exec_(QCursor.pos())

        if not action:
            return

        if action == custom_command:
            self.launch_custom_command_dialog()

        for program in visible_actions:
            if program['text'] == action.text():
                for host in hosts:
                    self.controller.new_job(program, hosts[host]['id'], port)
                return

    def right_click_in_creds_table(self, qpoint: QPoint):
        if self.ui.ui.work.currentIndex() == 0: # Calling from a host
            widget_calling = self.app_tabs['Credentials']
        elif self.ui.ui.work.currentIndex() == 2:  # Calling from the credentials tab
            widget_calling = self.ui.ui.creds_table

        item = widget_calling.indexAt(qpoint)

        if item is None:
            return

        top_menu = QMenu()

        if item.row() >= 0 or item.column() >= 0:
            clipboard_action = top_menu.addAction("Copy selected cells into clipboard")
            clipboard_action.setIcon(QIcon.fromTheme("edit-copy"))
            delete_action = top_menu.addAction("Delete credentials")
            delete_action.setIcon(QIcon.fromTheme("edit-delete"))
            if self.ui.ui.work.currentIndex() == 0:
                top_menu.addSeparator()
        if self.ui.ui.work.currentIndex() == 0:
            insert_action = top_menu.addAction("Insert new credentials")
            insert_action.setIcon(QIcon.fromTheme("document-new"))

        action = top_menu.exec_(QCursor.pos())
        if not action:
            return

        if self.ui.ui.work.currentIndex() == 0 and action == insert_action:
            widget_calling.insertRow(widget_calling.rowCount())
            new_id = self.controller.create_creds()
            widget_calling.setItem(widget_calling.rowCount()-1, 0, QTableWidgetItem(str(new_id)))

            combo = QComboBox()
            combo.addItems(["password", "hash", "ssh_key", "other"])
            combo.setProperty('cred_id', str(new_id))
            widget_calling.setCellWidget(widget_calling.rowCount()-1, 1, combo)
            combo.currentTextChanged.connect(lambda new_type, qcombobox=combo: self.credentials_changed(qcombobox)) # signal in loop: https://stackoverflow.com/questions/46300229/connecting-multiples-signal-slot-in-a-for-loop-in-pyqt

        elif action == clipboard_action:
            self.copy_selectedIndexes_to_clipboard(widget_calling.selectedIndexes())

        elif action == delete_action:
            items = widget_calling.selectedIndexes()

            creds_ids = []
            rows_to_delete = []
            for item in items:
                if self.ui.ui.work.currentIndex() == 0:
                    creds_ids.append(widget_calling.item(item.row(), 0).text())
                    rows_to_delete.append(item.row())
                if self.ui.ui.work.currentIndex() == 2:
                    creds_ids.append(item.model().itemData(item)['id'])

            self.controller.remove_creds(list(set(creds_ids)))

            if self.ui.ui.work.currentIndex() == 0:
                for row in reversed(sorted(rows_to_delete)):
                    self.app_tabs['Credentials'].removeRow(int(row))
            if self.ui.ui.work.currentIndex() == 2:
                self.reset_right_panel()

    def launch_custom_command_dialog(self):
        new_scan_dialog = Custom_Command()
        new_scan_dialog.ui.detached.stateChanged.connect(lambda checked: new_scan_dialog.in_terminal.setEnabled(checked))

        if new_scan_dialog.exec():
            command = new_scan_dialog.ui.command.text()

            if command:
                program = {
                    'name': command.split()[0],
                    'binary': command.split()[0],
                    'args': command.split()[1:],
                    'detached': new_scan_dialog.ui.detached.checkState() == Qt.CheckState.Checked,
                    'in_terminal': new_scan_dialog.ui.in_terminal.checkState() == Qt.CheckState.Checked
                }
                self.controller.new_job(program)

    def create_actions_from_port(self, proto: str, port: str):
        port_associations = Config.get()['ports_associations']
        user_binaries = Config.get()['user_binaries']
        visible_actions = []  # Storing proposed actions here, rather than parsing AGAIN the conf ...

        top_menu = QMenu(None)

        for current_port in ["any", port]:
            if current_port != "any" and port not in port_associations[proto].keys():
                top_menu.addAction(f"No specific program declared for port {current_port}/{proto}").setEnabled(False)
            else:
                if current_port == "any" and "any" not in Config.get()['ports_associations'][proto]:
                    continue
                for program in port_associations[proto][current_port]:
                    _ = top_menu.addAction(user_binaries[program]['text'])
                    if 'icon' in user_binaries[program].keys():
                        _.setIcon(QIcon(user_binaries[program]['icon']))
                    elif 'in_terminal' in user_binaries[program].keys() and user_binaries[program]['in_terminal']:
                        _.setIcon(QIcon.fromTheme("utilities-terminal"))

                    visible_actions.append(user_binaries[program])

            top_menu.addSeparator()

        return top_menu, visible_actions

    def open_db(self):
        filename, _ = QFileDialog.getOpenFileName(caption='Restore saved session', dir='.', filter='*.sqlite')
        if filename:
            self.controller.open_db(filename)

    def save_db(self):
        if not Database.current_savefile:
            self.save_as_db()
        else:
            self.controller.save_db()

    def save_as_db(self):
        filename, _ = QFileDialog.getSaveFileName(caption='Save current session', dir='.', filter='*.sqlite')
        if filename:
            if not filename.endswith('.sqlite'):
                filename += '.sqlite'
            self.controller.save_db(filename)

    def dialog_import_xml(self):
        filename, _ = QFileDialog.getOpenFileName(caption='Import nmap XML output', dir='.', filter='*.xml')
        if filename:
            self.controller.log('INFO', f"Importing Nmap results from {filename}")
            new_hosts = self.controller.parse_nmap_data('xml', filename)

            if Config.get()['user_prefs']['enable_autorun_on_xml_import']:
                self.controller.autorun(new_hosts)

    def dialog_scan_host(self):
        new_scan_dialog = New_Scan()
        if new_scan_dialog.exec():
            target = new_scan_dialog.ui.target.text()
            nmap_speed = new_scan_dialog.ui.nmap_speed.text()
            ports = new_scan_dialog.ui.ports.text()
            if target:
                self.controller.new_scan(target, nmap_speed, ports,
                                         new_scan_dialog.ui.skip_host_discovery.checkState(),
                                         new_scan_dialog.ui.check_versions.checkState(),
                                         new_scan_dialog.ui.launch_scripts.checkState(),
                                         new_scan_dialog.ui.os_detection.checkState(),
                                         new_scan_dialog.ui.tcp_and_udp.checkState())

    def update_notes(self):
        current_notes = self.app_tabs['Notes'].toHtml()
        self.controller.update_notes_for_current_host(current_notes)

    def credentials_changed(self, item: QComboBox | QTableWidgetItem):
        columns_description = {0: "id", 1: "type", 2: "domain", 3: "username", 4: "password"}
        if type(item) == QComboBox: # Type of cred has changed
            self.controller.update_credentials(item.property('cred_id'), columns_description[1], item.currentText())
        elif type(item) == QTableWidgetItem: # Any cell other than the credential type has changed
            cred_id = self.app_tabs['Credentials'].item(item.row(), 0).text()
            self.controller.update_credentials(cred_id, columns_description[item.column()], item.text())

    def new_tab(self, title: str, job_id: int, content: str = "") -> QTextEdit:
        tab = QTextEdit(content)
        tab.setReadOnly(True)
        tab_index = self.ui.ui.application_TabWidget.addTab(tab, title)
        self.app_tabs[tab_index] = {'widget': tab, 'job_id': job_id}
        self.link_job_and_widget(job_id, tab)
        return tab

    def link_job_and_widget(self, job_id: int, widget: QTextEdit):
        if self.controller.get_job(job_id):
            job = self.controller.get_job(job_id)
            job.readyReadStandardOutput.connect(lambda: widget.setText(job.get_output_text().replace(chr(0), "")))
            job.readyReadStandardError.connect(lambda: widget.setText(job.get_output_text().replace(chr(0), "")))

    def close_right_panel_tabs(self):
        for i in range(self.ui.ui.application_TabWidget.count()):
            self.ui.ui.application_TabWidget.removeTab(0)
        self.app_tabs = {}

    def reset_right_panel(self):
        self.close_right_panel_tabs()

        for widget in self.default_tabs:
            if issubclass(widget['object'], QWidget):
                self.app_tabs[widget['name']] = widget['object']()
            else:
                self.app_tabs[widget['name']] = QWidget()
            self.ui.ui.application_TabWidget.addTab(self.app_tabs[widget['name']], widget['name'])

        # Setup ports view
        self.app_tabs['Ports'].setColumnCount(4)
        self.app_tabs['Ports'].setHorizontalHeaderLabels(['Protocol', 'Status','Port','Description'])
        self.app_tabs['Ports'].verticalHeader().setVisible(False)
        self.app_tabs['Ports'].horizontalHeader().setStretchLastSection(True)
        self.app_tabs['Ports'].setGridStyle(Qt.DotLine)
        self.app_tabs['Ports'].setSelectionBehavior(QAbstractItemView.SelectRows)
        self.app_tabs['Ports'].setSelectionMode(QAbstractItemView.SingleSelection)
        self.app_tabs['Ports'].setContextMenuPolicy(Qt.CustomContextMenu)
        self.app_tabs['Ports'].setEditTriggers(QAbstractItemView.NoEditTriggers)

        # Setup nmap tab
        self.app_tabs['Nmap'].setReadOnly(True)

        # Setup creds tab
        self.app_tabs['Credentials'].setColumnCount(5)
        self.app_tabs['Credentials'].setColumnHidden(0, True)
        self.app_tabs['Credentials'].setHorizontalHeaderItem(0, QTableWidgetItem('id'))
        self.app_tabs['Credentials'].setHorizontalHeaderItem(1, QTableWidgetItem('type'))
        self.app_tabs['Credentials'].setHorizontalHeaderItem(2, QTableWidgetItem('domain'))
        self.app_tabs['Credentials'].setHorizontalHeaderItem(3, QTableWidgetItem('username'))
        self.app_tabs['Credentials'].setHorizontalHeaderItem(4, QTableWidgetItem('password'))
        self.app_tabs['Credentials'].horizontalHeader().setStretchLastSection(True)
        self.app_tabs['Credentials'].itemChanged.connect(self.credentials_changed)
        self.app_tabs['Credentials'].setContextMenuPolicy(Qt.CustomContextMenu)
        self.app_tabs['Credentials'].customContextMenuRequested.connect(self.right_click_in_creds_table)

        # Setup notes signal / slot
        self.app_tabs['Notes'].textChanged.connect(self.update_notes)

        # Delete close button on default tabs
        self.ui.ui.application_TabWidget.tabBar().tabButton(0, QTabBar.RightSide).resize(0, 0)
        self.ui.ui.application_TabWidget.tabBar().tabButton(1, QTabBar.RightSide).resize(0, 0)
        self.ui.ui.application_TabWidget.tabBar().tabButton(2, QTabBar.RightSide).resize(0, 0)
        self.ui.ui.application_TabWidget.tabBar().tabButton(3, QTabBar.RightSide).resize(0, 0)

    def update_right_panel(self, host: QModelIndex):
        host_details = self.controller.get_selected_host(host)
        if not host_details:
            return

        current_index = self.ui.ui.application_TabWidget.currentIndex()

        self.reset_right_panel()

        ## Filling default tabs
        # Port listing
        self.app_tabs['Ports'].setRowCount(len(host_details['ports'])) # Number of row to add (counting number of ports in both tcp and udp)

        row_count = 0
        for port in host_details['ports']:
            self.app_tabs['Ports'].setItem(row_count, 0, QTableWidgetItem(port['proto']))
            self.app_tabs['Ports'].setItem(row_count, 1, QTableWidgetItem(port['status']))
            self.app_tabs['Ports'].setItem(row_count, 2, QTableWidgetItem(port['port']))
            self.app_tabs['Ports'].setItem(row_count, 3, QTableWidgetItem(port['description']))

            row_count += 1

        # Connect the right click on a row
        self.app_tabs['Ports'].customContextMenuRequested.connect(self.right_click_in_port_table)

        # Nmap output
        if 'nmap' in host_details.keys():
            self.app_tabs['Nmap'].setText(host_details['nmap'])

        # Credentials
        self.app_tabs['Credentials'].blockSignals(True)
        self.app_tabs['Credentials'].setRowCount(len(host_details['credentials']))
        for row in range(len(host_details['credentials'])):
            combo = QComboBox()
            combo.addItems(["password", "hash", "ssh_key", "other"])
            self.app_tabs['Credentials'].setCellWidget(row, 1, combo)

        for i, cred in enumerate(host_details['credentials']):
            self.app_tabs['Credentials'].setItem(i, 0, QTableWidgetItem(str(cred['id'])))
            self.app_tabs['Credentials'].cellWidget(i, 1).setCurrentText(str(cred['type']))
            self.app_tabs['Credentials'].cellWidget(i, 1).setProperty('cred_id', str(cred['id']))
            self.app_tabs['Credentials'].cellWidget(i, 1).currentTextChanged.connect(lambda new_type, qcombobox=self.app_tabs['Credentials'].cellWidget(i, 1): self.credentials_changed(qcombobox)) # signal in loop: https://stackoverflow.com/questions/46300229/connecting-multiples-signal-slot-in-a-for-loop-in-pyqt
            self.app_tabs['Credentials'].setItem(i, 2, QTableWidgetItem(str(cred['domain'])))
            self.app_tabs['Credentials'].setItem(i, 3, QTableWidgetItem(str(cred['username'])))
            self.app_tabs['Credentials'].setItem(i, 4, QTableWidgetItem(str(cred['password'])))

        self.app_tabs['Credentials'].blockSignals(False)

        # Notes
        if 'notes' in host_details.keys():
            self.app_tabs['Notes'].blockSignals(True) # Consumes less resources, and do not trigger the dirty marker on database
            try:
                self.app_tabs['Notes'].setHtml(host_details['notes'])
            except ValueError:
                self.app_tabs['Notes'].setHtml(host_details['notes'].replace(chr(0), ""))
            self.app_tabs['Notes'].blockSignals(False)

        ## Application tabs
        for app in host_details['external_tabs']:
            if app['text']:
                self.new_tab(app['title'], app['job_id'], app['text'].replace(chr(0), ""))
            else:
                self.new_tab(app['title'], app['job_id'], "")

        # Restoring tab focus if currentIndex < 4
        if current_index < 4:
            self.ui.ui.application_TabWidget.setCurrentIndex(current_index)

    def update_hosts_for_port_panel(self, ports: dict):
        self.ui.ui.port_table.setRowCount(len(ports['udp']) + len(ports['tcp']))

        row_count = 0
        for proto in ['udp', 'tcp']:
            for port in ports[proto]:
                self.ui.ui.port_table.setItem(row_count, 0, QTableWidgetItem(proto))
                self.ui.ui.port_table.setItem(row_count, 1, QTableWidgetItem(str(port)))
                row_count += 1

    def change_filter_hosts_for_port(self):
        proto = port = ""
        if self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),0):
            proto = self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),0).text()
        if self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),1):
            port = self.ui.ui.port_table.item(self.ui.ui.port_table.currentRow(),1).text()
        if proto and port:
            self.controller.filter_hosts_for_port_table(proto, port)

    def remove_tab(self, tab_index: int):
        # There is a job, It is running, User wants to confirm
        if Config.get()['user_prefs']['confirm_before_tab_removal']:
            question = "Tab content will be discarded. Are you sure ?"
            if self.controller.get_job(self.app_tabs[tab_index]['job_id']):
                job = self.controller.get_job(self.app_tabs[tab_index]['job_id'])
                if job.state() == 2:
                    question = "The currently running program will be killed, and tab content will be discarded. Are you sure ?"

            if QMessageBox.question(None, 'Confirmation', question) != QMessageBox.Yes:
                return

        self.controller.remove_tab(self.app_tabs[tab_index])

        # Removing and re-indexing internal view app_tabs
        del self.app_tabs[tab_index]
        if range(tab_index, len(self.app_tabs)):
            for i in range(tab_index, len(self.app_tabs)):
                self.app_tabs[i] = self.app_tabs[i+1]
            del self.app_tabs[i+1]

        self.ui.ui.application_TabWidget.removeTab(tab_index)

    def copy_selectedIndexes_to_clipboard(self, selectedIndexes: list):
        data = {}
        for item in selectedIndexes:
            if item.data() is None:
                continue
            if item.row() in data.keys():
                data[item.row()].append(item.data())
            else:
                data[item.row()] = [item.data()]

        clipboard_data = ""
        for key in data:
            clipboard_data += ';'.join(data[key]) + '\n'

        clipboard = QApplication.clipboard()
        clipboard.setText(clipboard_data.strip('\n'))
        self.ui.statusBar().showMessage('Data copied to clipboard !', 5000)

    def machine_list_copy_all_to_clipboard(self):
        self.ui.ui.hosts_for_port_table.selectAll()
        self.copy_selectedIndexes_to_clipboard(self.ui.ui.hosts_for_port_table.selectedIndexes())

    def creds_table_copy_all_to_clipboard(self):
        self.ui.ui.creds_table.selectAll()
        self.copy_selectedIndexes_to_clipboard(self.ui.ui.creds_table.selectedIndexes())


from django.contrib import admin
from django.urls import path
from firstapp import views
from django.views.generic import TemplateView
from django.conf.urls import include, url

admin.autodiscover()

urlpatterns = [
    path('',views.great, name='great'),
    url(r'^admin/backups/', include('dbbackup_ui.urls')),
    url(r'^admin/', admin.site.urls),
    path('zoo/', views.index, name='home'),
    path('animal/', views.index_animal, name='animal'),
    path('area/', views.index_habitat_area, name='area'),
    path('user/', views.index_user, name='user'),
    path('ration/', views.index_feeding_ration, name='ration'),
    path('bird/', views.index_bird, name='bird'),
    path('reptile/', views.index_reptile, name='reptile'),
    #пути для животных
    path('animal/add/', views.view_animal.add_animal, name='add_animal'),
    path("animal/del/", views.view_animal.del_animal, name="del_animal"),
    path("animal/up/", views.view_animal.update_animal, name="update_animal"),
    #пути для зон обитания
    path('area/adde/', views.view_habitat_area.add_habitat_area, name='add_habitat_area'),
    path("area/dele/", views.view_habitat_area.del_habitat_area, name="del_habitat_area"),
    path("area/upe/", views.view_habitat_area.update_habitat_area, name="update_habitat_area"),
    #пути для пользователей
    path('user/addc/', views.view_user.add_user, name='add_author'),
    path("user/delc/", views.view_user.del_user, name="del_data"),
    path("user/upc/", views.view_user.update_user, name="update_data"),
    #пути для рационов
    path('ration/addo/', views.view_feeding_ration.add_feeding_ration, name='add_ration'),
    path("ration/delo/", views.view_feeding_ration.del_feeding_ration, name="del_ration"),
    path("ration/upo/", views.view_feeding_ration.update_feeding_ration, name="update_ration"),
    #пути для птиц
    path('bird/addb/', views.view_bird.add_bird, name='add_bird'),
    path("bird/delb/", views.view_bird.del_bird, name="del_bird"),
    path("bird/upb/", views.view_bird.update_bird, name="update_bird"),
    #пути для рептилий
    path('reptile/addr/', views.view_reptile.add_reptile, name='add_reptile'),
    path("reptile/delr/", views.view_reptile.del_reptile, name="del_reptile"),
    path("reptile/upr/", views.view_reptile.update_reptile, name="update_reptile"),
]
urlpatterns += [
    path('accounts/', include('django.contrib.auth.urls')),
]