__author__  = "William Batista Aguiar Motta"
__email__   = "william.b.motta@aluno.unb.br"
__license__ = "GPL"

"""
    Tool Box
"""
from matricula_web_web_crawler import *

"""
    Operações em disco.
"""
def salvar_lista_disciplinas_arquivo_csv(nome_arquivo, lista_disciplinas):
    with codecs.open(nome_arquivo, 'wb', encoding='utf-8') as arq:
        escritor = csv.writer(arq)
        entradas = [(a.codigo, a.requisitos) for a in lista_disciplinas]
        escritor.writerows(entradas)

def abrir_lista_disciplinas_arquivo_csv(nome_arquivo):
    with codecs.open(nome_arquivo, encoding='utf-8') as arq:
        leitor = csv.reader(arq)
        return [a for a in leitor]

def salvar_lista_disciplinas_arquivo_json(nome_arquivo, lista_disciplinas):
    with codecs.open(nome_arquivo, 'wb', encoding='utf-8') as arq:
        json.dump(lista_disciplinas, arq, cls=WC.Serializador, encoding='utf-8', indent=4)

def abrir_dicionario_disciplinas(nome_arquivo):
    with codecs.open(nome_arquivo, encoding='utf-8') as arq:
        lista = json.load(arq, encoding='utf-8')
        return lista

def salvar_todas_as_disciplinas_usadas_sessao():
    salvar_lista_disciplinas_arquivo_json('disciplinas.json', Disciplinas)

"""
    Relatórios.
"""
def imprime_agenda(grade):
    matriz = grade_para_matriz(grade)
    saida = ''
    saida += cria_coluna(3, '')
    saida += cria_coluna(3, 'Segunda')
    saida += cria_coluna(3, 'Terça')
    saida += cria_coluna(3, 'Quarta')
    saida += cria_coluna(3, 'Quinta')
    saida += cria_coluna(3, 'Sexta')
    saida += cria_coluna(3, 'Sábado')
    saida += '\n'
    for h in horarios:
        saida += cria_coluna(3, h)
        d = 'Segunda'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        d = 'Terça'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        d = 'Quarta'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        d = 'Quinta'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        d = 'Sexta'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        d = 'Sábado'
        if not matriz[d][h]:
            matriz[d][h] = ('vazio', '\t')

        cod, id = matriz[d][h]
        saida += cria_coluna(3, cod + ' ' + id)
        saida += '\n'

    saida += '\n'
    for cod, turma in sorted(grade, key=lambda x: x[0]):
        saida += cria_coluna(2, cod)
        saida += cria_coluna(1, turma)
        saida += cria_coluna(4, Disciplina(cod).nome)
        saida += '\n'

    print(saida)

def propaganda_biblioteca(cod_curso, nome_curso):
    curriculo = baixar_curriculo(cod_curso)
    lista = [(len(disciplinas_dependentes(disc, curriculo)), disc.nome) for disc in curriculo]
    lista.sort(key=lambda x: x[0], reverse=True)

    print(('Estava trabalhando com esta biblioteca github.com/kuroudu/matricula-o-matic para analisar os dados do' \
           ' matrícula web e fiz uma análise da quantidade de disciplinas que dependem direta e indiretamente de cada' \
           ' materia do currículo de ' + str(nome_curso) + '. ' + str(lista[0][1]) + ' ficou no topo da lista com ' \
           + str(lista[0][0]) + ' pontos.'))

    print('Vocês podem ver a relação das disciplinas e suas influências abaixo.')
    print((linha()))
    for entrada in lista:
        print(str(entrada[0]) + '\t' + entrada[1])

def resumo_curso(cod_curso):
    print(cod_curso)
    fluxo = baixar_fluxo(cod_curso)
    nao_cursadas = disciplinas_nao_cursadas(fluxo, [])
    print(str(len(nao_cursadas)) + '\t' + ' disciplinas compõem o fluxo.')
    for entrada in nao_cursadas:
        entrada.faltam = altura_ate_historico(entrada, [])

    nao_cursadas = sorted(nao_cursadas, key=lambda x: x.faltam)
    print(str(max(nao_cursadas[-1].faltam, math.ceil(len(nao_cursadas) / 7))) + '\t' + ' semestres até a conclusão.')
    for entrada in nao_cursadas:
        print(str(entrada.faltam) + '\t' + entrada.nome)

def resumo_meu_curso(cod_curso, historico):
    print(cod_curso)
    fluxo = baixar_fluxo(cod_curso)
    nao_cursadas = disciplinas_nao_cursadas(fluxo, historico)
    print(str(len(nao_cursadas)) + '\t' + ' disciplinas a serem cursadas')
    for entrada in nao_cursadas:
        entrada.faltam = altura_ate_historico(entrada, historico)

    nao_cursadas = sorted(nao_cursadas, key=lambda x: x.faltam)
    print(str(max(nao_cursadas[-1].faltam, math.ceil(len(nao_cursadas) / 7))) + '\t' + ' semestres até a conclusão')
    for entrada in nao_cursadas:
        print(str(entrada.faltam) + '\t' + entrada.nome)

def sugestao_grades(cod_curso, historico):
    fluxo = baixar_fluxo(cod_curso)
    grades = monta_grades(disciplinas_cursaveis(fluxo, historico))
    grades = sorted(grades, key=lambda x: avalia_grade(x, fluxo), reverse=True)
    for entrada in grades:
        print(avalia_grade(entrada, fluxo))
        imprime_agenda(entrada)

"""
    Algoritmos
"""
""" 
    Retorna o conjunto C(fluxo - histórico). 
"""
def disciplinas_nao_cursadas(fluxo, historico):
    flux = [a.codigo for a in fluxo]
    hist = [a.codigo for a in historico]
    nao_cursadas = set(flux) - set(hist)
    return [Disciplina(a) for a in nao_cursadas]

""" 
    Disciplinas Cursaveis. 
"""
def disciplinas_cursaveis(fluxo, historico):
    flux = [a.codigo for a in fluxo]
    hist = [a.codigo for a in historico]
    nao_cursadas = list(set(flux) - set(hist))
    cursaveis = []
    for entrada in nao_cursadas:
        for req in Disciplina(entrada).requisitos:
            if set(req).issubset(set(hist)):
                cursaveis.append(entrada)

    return [Disciplina(a) for a in set(cursaveis)]

def existe_rota(disc1, disc2):
    existe = False
    for reqs in disc1.requisitos:
        if disc2.codigo in reqs:
            existe = True
            break

        else:
            for req in reqs:
                existe |= existe_rota(Disciplina(req), disc2)

    return existe

def disciplinas_dependentes(disciplina, todas_disciplinas):
    dep = []                                                # para cada disciplina em lista:
    for disc in todas_disciplinas:                          # se existe rota para disciplina:
        if existe_rota(disc, disciplina):                   # então adicione a dependencia
            dep += [disc]

    return dep

""" 
    Remove disciplinas marcadas como optativas de uma lista de disciplinas. 
"""
def remove_optativas(disciplinas):
    return [x for x in disciplinas if not re.search(r'c\Z', x.tipo, re.I)]

""" 
    É feita uma busca nas dependências das disciplinas de forma recursiva para determinar todas as dependências
    diretas e indiretas. É necessário fornecer um conjunto de todas as disciplinas a serem percorridas. Caso a saída
    em terminal esteja habilitada, uma tabela com quantidade de dependências e nome da disciplina será mostrada. 
"""
def ordena_por_dependencias(lista_disciplinas, todas_disciplinas, mostrar_em_terminal=False):
    nova = []
    for entrada in lista_disciplinas:
        dep = disciplinas_dependentes(entrada, todas_disciplinas)
        tamanho = len(dep)
        nova += [(tamanho, entrada.codigo)]

    nova.sort(key=lambda x: x[0], reverse=True)
    if mostrar_em_terminal:
        for entrada in nova:
            print((entrada[0], Disciplina(entrada[1]).nome))

    return [Disciplina(a[1]) for a in nova]

def monta(lista_disciplinas, grades=[]):
    # monta
    # avalia
    # desmonta
    # monta
    lista = []
    for entrada in lista_disciplinas:
        for turma in entrada.turmas:
            lista.append((entrada.codigo, turma.id))

    for entrada in lista:
        __monta_sub(lista, [entrada], grades)

    return grades

def __monta_sub(lista_disciplinas, grade=[], gradesCompletadas=[]):
    if grade_tem_repeticoes(grade) or \
        grade_tem_colisoes_horario(grade):                  # processa o atual
        return

    gradesCompletadas.append(grade)
    for entrada in lista_disciplinas:                       # abre próximos
        nova_lista = lista_disciplinas.copy()
        nova_lista.remove(entrada)
        __monta_sub(nova_lista, grade + [entrada], gradesCompletadas)

def monta_grades(lista_disciplinas):
    grades = []
    lista = []
    for entrada in lista_disciplinas:
        for turma in entrada.turmas:
            lista.append((entrada.codigo, turma.id))

    for entrada in lista:
        grade = monta_grade_a_partir_da_lista(lista, [entrada])
        grades.append(grade)

    return grades

def remove_grades_repetidas(grades):
    nova = []
    for entrada in grades:
        if not entrada in nova:
            nova.append(entrada)

    return nova

""" 
    Uma grade viável é montada a partir da lista de disciplinas, na grade é garantida que não haverão colisões de
    horário. As diciplinas no início da lista possuem maior prioridade.
"""
def monta_grade_a_partir_da_lista(lista_disciplinas, grade_base=[]):
    cadastradas = grade_base.copy()
    for cod, turma in lista_disciplinas:
        if cod in cadastradas:
            break

        grade_base.append((cod, turma))
        cadastradas.append(cod)
        if grade_tem_colisoes_horario(grade_base):
            grade_base.remove((cod, turma))

    return grade_base

def grade_tem_repeticoes(grade):
    discs = []
    for cod, cod_turma in grade:
        if cod in discs:
            return True

        else:
            discs.append(cod)

    return False

""" 
    Os horários das turmas de uma grade - lista de disciplinas - são checados para verificar se existe colisão
    de horários. 
 """
def grade_tem_colisoes_horario(grade):
    dias = []
    for cod, cod_turma in grade:
        for dia in Disciplina(cod).turma(cod_turma).dias:
            if dia in dias:
                return True

            else:
                dias.append(dia)

    return False

def gerar_arvore(cod_curso):
    # TODO desabilitado temporariamente
    # curriculo = baixar_fluxo(cod_curso)
    # grafo = pydot.Dot(prog='neato', splines='true', overlap='false')
    # for entrada in curriculo:
    #     grafo.add_node(pydot.Node(unicode(entrada.nome, encoding='latin-1')))
    #     for reqs in entrada.requisitos:
    #         for r in reqs:
    #             if [a for a in curriculo if a.codigo == r]:
    #                 grafo.add_edge(pydot.Edge(pydot.Node(unicode(Disciplina(r).nome, encoding='latin-1')),
    #                                           pydot.Node(unicode(entrada.nome, encoding='latin-1'))))
    # grafo.write(str(cod_curso) + ' arvore.png', format='png')
    pass

def distancia(disc1, disc2):
    if existe_rota(disc1, disc2):
        return math.fabs(altura_arvore(disc1) - altura_arvore(disc2))

    else:
        return float('inf')

def altura_ate_historico(disciplinaRaiz, historico):
    if disciplinaRaiz.codigo in set([a.codigo for a in historico]):
        return 0

    else:
        menor = float('inf')
        for entrada in disciplinaRaiz.requisitos:
            maior = 0
            for req in entrada:
                alt = altura_ate_historico(Disciplina(req), historico)
                if alt > maior:
                    maior = alt

            if menor > maior:
                menor = maior

        if menor == float('inf'):
            menor = 0

        return 1 + menor

def altura_arvore(disciplinaRaiz, altura=0):
    maior = 0
    for requisito in disciplinaRaiz.requisitos:
        for entrada in requisito:
            altura = altura_arvore(Disciplina(entrada), altura)
            if altura > maior:
                maior = altura

    return maior + 1

def mostrar_hierarquia(cod_curso):
    fluxo = baixar_fluxo(str(cod_curso))

    for entrada in fluxo:
        entrada.altura = altura_arvore(entrada)

    fluxo.sort(key=lambda x: x.altura)

    for entrada in fluxo:
        print(str(entrada.altura) + ' ' + entrada.nome)

def mostrar_hierarquia_pacote(lista_disciplinas):
    for entrada in lista_disciplinas:
        entrada.altura = altura_arvore(entrada)

    lista_disciplinas.sort(key=lambda x: x.altura)

    for entrada in lista_disciplinas:
        print(str(entrada.altura) + '\t' + entrada.nome + '\t' + entrada.codigo)

def rota_alternativa(disciplinaRaiz):
    rota = []
    menor = 10000
    if not disciplinaRaiz.requisitos:
        menor = 0

    for requisito in disciplinaRaiz.requisitos:
        maior = 0
        for entrada in requisito:
            altura = altura_arvore(Disciplina(entrada))
            if altura > maior:
                maior = altura

        if maior < menor:
            menor = maior
            rota.append(requisito)

    return menor, rota

def avalia_grade(grade, curriculo, bons_professores=[], professores_ruins=[]):
    if grade_tem_colisoes_horario(grade):
        return 0

    if grade_tem_repeticoes(grade):
        return 0

    pontos = 0
    # Pontos pela influência como pré requisito para outras disciplinas.
    # Pontos pela quantidade de matérias do fluxo.
    disciplinas = [Disciplina(a) for a, b in grade]
    for entrada in disciplinas:
        pontos += len(disciplinas_dependentes(entrada, curriculo)) + 1
        for cur in curriculo:
            if entrada.codigo == cur.codigo:
                entrada.tipo = cur.tipo

        try:
            if re.search(r'F', entrada.tipo, re.IGNORECASE):
                pontos += 1.7

            if re.search('C', entrada.tipo, re.IGNORECASE):
                pontos += 1.

        except:
            pass

    # Pontos por ter bons professores ou não.
    for cod, cod_turma in grade:
        for prof in Disciplina(cod).turma(cod_turma).professores:
            for bom in bons_professores:
                if re.search(bom, prof, re.I):
                    pontos *= 1.7

            for ruim in professores_ruins:
                if re.search(ruim, prof, re.I):
                    pontos *= .3

    # Pontos por ser compacta. Com disciplinas centradas no centro do dia às 12:00.
    # Usando distribuição normal de média 12 e variância 6.
    #for cod, cod_turma in grade:
    #    for dia, hora in Disciplina(cod).turma(cod_turma).dias:
    #        try:
    #            horaAtual = int(re.search('(\d{2}):', hora).group(1))
    #            pontos *= 10 / ((12 * 2 * math.pi) ** .5) * math.e ** (-1 * (horaAtual - 12) ** 2 / (2 * 12))
    #        except:
    #            pass

    return pontos

def ordenar_conforme_metrica(grades, curriculo, bons_professores=[], professores_ruins=[],
                             mostrar_em_terminal=False, relatorio=''):
    grades.sort(key=lambda x: avalia_grade(x, curriculo, bons_professores, professores_ruins), reverse=True)
    return grades

def grade_para_matriz(grade):
    matriz = {}
    for dia in semana:
        matriz[dia] = {}
        for hora in horarios:
            matriz[dia][hora] = ('', '')

    for cod, turma in grade:
        for dia, hora in Disciplina(cod).turma(turma).dias:
            matriz[dia][hora] = (cod, turma)

    return matriz

""" 
    Retorna a grade na forma de uma String formatada.
"""
def imprime_grade(grade):
    saida = ''
    for codigo, id in grade:
        turma = Disciplina(codigo).turma(id)
        disc = Disciplina(codigo)
        saida += cria_coluna(3, disc.codigo)
        saida += cria_coluna(12, disc.nome)
        saida += cria_coluna(2, turma.id)

        for p in turma.professores:
            saida += cria_coluna(8, p + ',')

        for dia, hora in turma.dias:
            saida += cria_coluna(4, dia)
            saida += cria_coluna(4, hora)

        saida += '\n'

    saida = re.sub(r'\n\Z', '', saida)
    return saida
