################################################################################
#
# Copyright (c) 2010 The MadGraph5_aMC@NLO Development team and Contributors
#
# This file is a part of the MadGraph5_aMC@NLO project, an application which 
# automatically generates Feynman diagrams and matrix elements for arbitrary
# high-energy processes in the Standard Model and beyond.
#
# It is subject to the MadGraph5_aMC@NLO license which should accompany this 
# distribution.
#
# For more information, visit madgraph.phys.ucl.ac.be and amcatnlo.web.cern.ch
#
################################################################################

"""Module for calculation of symmetries between diagrams, by
evaluating amp2 values for permutations of momenta."""

from __future__ import division

from __future__ import absolute_import
import array
import copy
import fractions
import itertools
import logging
import math
import os
import re
import signal

import aloha.aloha_writers as aloha_writers
import aloha.create_aloha as create_aloha

import madgraph.iolibs.export_python as export_python
import madgraph.iolibs.group_subprocs as group_subprocs
import madgraph.iolibs.helas_call_writers as helas_call_writer
import models.import_ufo as import_ufo
import madgraph.iolibs.save_load_object as save_load_object

import madgraph.core.base_objects as base_objects
import madgraph.loop.loop_base_objects as loop_base_objects
import madgraph.core.helas_objects as helas_objects
import madgraph.loop.loop_helas_objects as loop_helas_objects

import madgraph.core.color_algebra as color
import madgraph.core.color_amp as color_amp
import madgraph.core.helas_objects as helas_objects
import madgraph.core.diagram_generation as diagram_generation

import madgraph.various.process_checks as process_checks
import madgraph.various.misc as misc

from madgraph import MG5DIR

import models.model_reader as model_reader
import aloha.template_files.wavefunctions as wavefunctions
from aloha.template_files.wavefunctions import \
     ixxxxx, oxxxxx, vxxxxx, sxxxxx
from six.moves import range
from six.moves import zip

#===============================================================================
# Logger for process_checks
#===============================================================================

logger = logging.getLogger('madgraph.various.diagram_symmetry')

#===============================================================================
# find_symmetry
#===============================================================================

def find_symmetry(matrix_element):
    """Find symmetries between amplitudes by comparing diagram tags
    for all the diagrams in the process. Identical diagram tags
    correspond to different external particle permutations of the same
    diagram.
    
    Return list of positive number corresponding to number of
    symmetric diagrams and negative numbers corresponding to the
    equivalent diagram (for e+e->3a, get [6, -1, -1, -1, -1, -1]),
    list of the corresponding permutations needed, and list of all
    permutations of identical particles."""

    if isinstance(matrix_element, group_subprocs.SubProcessGroup):
        return find_symmetry_subproc_group(matrix_element)

    nexternal, ninitial = matrix_element.get_nexternal_ninitial()

    # diagram_numbers is a list of all relevant diagram numbers
    diagram_numbers = []
    # Prepare the symmetry vector with non-used amp2s (due to
    # multiparticle vertices)
    symmetry = []
    permutations = []
    ident_perms = []
    process = matrix_element.get('processes')[0]
    base_model = process.get('model')
    
    if isinstance(matrix_element, loop_helas_objects.LoopHelasMatrixElement):
        # For loop induced processes we consider only the loops (no R2) and
        # the shrunk diagram instead of the lcut one.
        FDStructRepo = loop_base_objects.FDStructureList([])
        base_diagrams = base_objects.DiagramList(
                   [(d.get_contracted_loop_diagram(base_model,FDStructRepo) if  
                   isinstance(d,loop_base_objects.LoopDiagram) else d) for d in
               matrix_element.get('base_amplitude').get('loop_diagrams') \
                                                            if d.get('type')>0])
        diagrams = matrix_element.get_loop_diagrams()
    else:
        diagrams = matrix_element.get('diagrams')
        base_diagrams = matrix_element.get_base_amplitude().get('diagrams')

    vert_list = [max(diag.get_vertex_leg_numbers()) for diag in diagrams if \
                                        diag.get_vertex_leg_numbers()!=[]]
    min_vert = min(vert_list) if vert_list!=[] else 0
    
    for diag in diagrams:
        diagram_numbers.append(diag.get('number'))
        permutations.append(list(range(nexternal)))
        if diag.get_vertex_leg_numbers()!=[] and \
                                  max(diag.get_vertex_leg_numbers()) > min_vert:
            # Ignore any diagrams with 4-particle vertices
            symmetry.append(0)
        else:
            symmetry.append(1)

    # Check for matrix elements with no identical particles
    if matrix_element.get("identical_particle_factor") == 1:
        return symmetry, \
               permutations,\
               [list(range(nexternal))]

    logger.info("Finding symmetric diagrams for process %s" % \
                 matrix_element.get('processes')[0].nice_string().\
                 replace("Process: ", ""))

    # diagram_tags is a list of unique tags
    diagram_tags = []
    # diagram_classes is a list of lists of diagram numbers belonging
    # to the different classes
    diagram_classes = []
    perms = []
    for diag, base_diagram in zip(diagrams, base_diagrams):
        if any([vert > min_vert for vert in
                diag.get_vertex_leg_numbers()]):
            # Only 3-vertices allowed in configs.inc
            continue
        
        tag = diagram_generation.DiagramTag(base_diagram)
        try:
            ind = diagram_tags.index(tag)
        except ValueError:
            diagram_classes.append([diag.get('number')])
            perms.append([tag.get_external_numbers()])
            diagram_tags.append(tag)
        else:
            diagram_classes[ind].append(diag.get('number'))
            perms[ind].append(tag.get_external_numbers())

    for inum, diag_number in enumerate(diagram_numbers):
        if symmetry[inum] == 0:
            continue
        idx1 = [i for i, d in enumerate(diagram_classes) if \
                diag_number in d][0]
        idx2 = diagram_classes[idx1].index(diag_number)
        if idx2 == 0:
            symmetry[inum] = len(diagram_classes[idx1])
        else:
            symmetry[inum] = -diagram_classes[idx1][0]
        # Order permutations according to how to reach the first perm
        permutations[inum] = diagram_generation.DiagramTag.reorder_permutation(perms[idx1][idx2],
                                                            perms[idx1][0])
        # ident_perms ordered according to order of external momenta
        perm = diagram_generation.DiagramTag.reorder_permutation(perms[idx1][0],
                                                           perms[idx1][idx2])
        if not perm in ident_perms:
            ident_perms.append(perm)

    return (symmetry, permutations, ident_perms)

def find_symmetry_by_evaluation(matrix_element, evaluator, max_time = 600):
    """Find symmetries between amplitudes by comparing the squared
    amplitudes for all permutations of identical particles.
    
    Return list of positive number corresponding to number of
    symmetric diagrams and negative numbers corresponding to the
    equivalent diagram (for e+e->3a, get [6, -1, -1, -1, -1, -1]),
    list of the corresponding permutations needed, and list of all
    permutations of identical particles.
    max_time gives a cutoff time for finding symmetries (in s)."""

    #if isinstance(matrix_element, group_subprocs.SubProcessGroup):
    #    return find_symmetry_subproc_group(matrix_element, evaluator, max_time)

    assert isinstance(matrix_element, helas_objects.HelasMatrixElement)

    # Exception class and routine to handle timeout
    class TimeOutError(Exception):
        pass
    def handle_alarm(signum, frame):
        raise TimeOutError

    (nexternal, ninitial) = matrix_element.get_nexternal_ninitial()
    vert_list = [max(diag.get_vertex_leg_numbers()) for diag in \
            matrix_element.get('diagrams') if diag.get_vertex_leg_numbers()!=[]]
    min_vert = min(vert_list) if vert_list!=[] else 0
    # Prepare the symmetry vector with non-used amp2s (due to
    # multiparticle vertices)
    symmetry = []
    for diag in matrix_element.get('diagrams'):
        # It used to be hardcoded to three instead of min_vert. Need to check
        # if it is ok to use the general min_vert instead.
        if diag.get_vertex_leg_numbers()!=[] and \
                                  max(diag.get_vertex_leg_numbers()) > min_vert:
            # Ignore any diagrams with 4-particle vertices
            symmetry.append(0)
        else:
            symmetry.append(1)

    # Check for matrix elements with no identical particles
    if matrix_element.get("identical_particle_factor") == 1:
        return symmetry, \
               [list(range(nexternal))]*len(symmetry),\
               [list(range(nexternal))]

    logger.info("Finding symmetric diagrams for process %s" % \
                 matrix_element.get('processes')[0].nice_string().\
                 replace("Process: ", ""))

    process = matrix_element.get('processes')[0]
    base_model = process.get('model')
    equivalent_process = base_objects.Process({\
                     'legs': base_objects.LegList([base_objects.Leg({
                               'id': wf.get('pdg_code'),
                               'state': wf.get('leg_state')}) \
                       for wf in matrix_element.get_external_wavefunctions()]),
                     'model': base_model})

    # Get phase space point
    p, w_rambo = evaluator.get_momenta(equivalent_process)
    
    # Check matrix element value for all permutations
    amp2start = []
    final_states = [l.get('id') for l in \
                    equivalent_process.get('legs')[ninitial:]]
    nperm = 0
    perms = []
    ident_perms = []

    # Set timeout for max_time
    signal.signal(signal.SIGALRM, handle_alarm)
    signal.alarm(max_time)
    try:
        for perm in itertools.permutations(list(range(ninitial, nexternal))):
            if [equivalent_process.get('legs')[i].get('id') for i in perm] != \
               final_states:
                # Non-identical particles permutated
                continue
            ident_perms.append([0,1]+list(perm))
            nperm += 1
            new_p = p[:ninitial] + [p[i] for i in perm]

            res = evaluator.evaluate_matrix_element(matrix_element, new_p)
            if not res:
                break
            me_value, amp2 = res
            # Make a list with (8-pos value, magnitude) to easily compare
            amp2sum = sum(amp2)
            amp2mag = []
            for a in amp2:
                a = a*me_value/max(amp2sum, 1e-30)
                if a > 0:
                    amp2mag.append(int(math.floor(math.log10(abs(a)))))
                else:
                    amp2mag.append(0)
            amp2 = [(int(a*10**(8-am)), am) for (a, am) in zip(amp2, amp2mag)]

            if not perms:
                # This is the first iteration - initialize lists
                # Initiate symmetry with all 1:s
                symmetry = [1 for i in range(len(amp2))]
                # Store initial amplitudes
                amp2start = amp2
                # Initialize list of permutations
                perms = [list(range(nexternal)) for i in range(len(amp2))]
                continue

            for i, val in enumerate(amp2):
                if val == (0,0):
                    # If amp2 is 0, just set symmetry to 0
                    symmetry[i] = 0
                    continue
                # Only compare with diagrams below this one
                if val in amp2start[:i]:
                    ind = amp2start.index(val)
                    # Replace if 1) this amp is unmatched (symmetry[i] > 0) or
                    # 2) this amp is matched but matched to an amp larger
                    # than ind
                    if symmetry[ind] > 0 and \
                       (symmetry[i] > 0 or \
                        symmetry[i] < 0 and -symmetry[i] > ind + 1):
                        symmetry[i] = -(ind+1)
                        perms[i] = [0, 1] + list(perm) 
                        symmetry[ind] += 1
    except TimeOutError:
        # Symmetry canceled due to time limit
        logger.warning("Cancel diagram symmetry - time exceeded")

    # Stop the alarm since we're done with this process
    signal.alarm(0)

    return (symmetry, perms, ident_perms)

#===============================================================================
# DiagramTag class to identify matrix elements
#===============================================================================

class IdentifySGConfigTag(diagram_generation.DiagramTag):
    """DiagramTag daughter class to identify diagrams giving the same
    config. Need to compare state, spin, mass, width, and color.
    Warning: If changing this tag, then also CanonicalConfigTag in 
             helas_objects.py must be changed!
    """

    @staticmethod
    def link_from_leg(leg, model):
        """Returns the end link for a leg needed to identify symmetric
        configs: ((leg number for initial state, spin, mass,
        width, color), number)."""

        part = model.get_particle(leg.get('id'))

        state = 0
        if not leg.get('state'):
            # Distinguish identical initial state particles
            state = leg.get('number')

        if part.get('color') != 1:
            charge = 0
        else:
            charge = abs(part.get('charge'))
        
        return [((state, part.get('spin'), part.get('color'), charge,
                  part.get('mass'), part.get('width')),
                 leg.get('number'))]
        
    @staticmethod
    def vertex_id_from_vertex(vertex, last_vertex, model, ninitial):
        """Returns the info needed to identify symmetric configs:
        interaction color, mass, width."""

        inter = model.get_interaction(vertex.get('id'))
            
        if last_vertex:
            return (0,)
        else:
            part = model.get_particle(vertex.get('legs')[-1].get('id'))
            try:
                QCD = inter.get('orders')['QCD']
            except Exception:
                QCD = 0

            onshell = vertex.get('legs')[-1].get('onshell')
            if onshell is True:
                onshell = 1
            elif onshell is False:
                onshell = 0
            else:
                onshell = -1



            return ((part.get('color'),
                     part.get('mass'), part.get('width'), QCD, onshell),)

def find_symmetry_subproc_group(subproc_group):
    """Find symmetric configs by directly comparing the configurations
    using IdentifySGConfigTag."""

    assert isinstance(subproc_group, group_subprocs.SubProcessGroup),\
           "Argument to find_symmetry_subproc_group has to be SubProcessGroup"

    # diagram_numbers is a list of all relevant diagram numbers
    diagram_numbers = []
    # Prepare the symmetry vector with non-used amp2s (due to
    # multiparticle vertices)
    symmetry = []
    permutations = []
    diagrams = subproc_group.get('mapping_diagrams')
    nexternal, ninitial = \
               subproc_group.get('matrix_elements')[0].get_nexternal_ninitial()
    model = subproc_group.get('matrix_elements')[0].get('processes')[0].\
            get('model')
    vert_list = [max(diag.get_vertex_leg_numbers()) for diag in diagrams if \
                                        diag.get_vertex_leg_numbers()!=[]]
    min_vert = min(vert_list) if vert_list!=[] else 0

    for idiag,diag in enumerate(diagrams):
        diagram_numbers.append(idiag+1)
        permutations.append(list(range(nexternal)))
        if diag.get_vertex_leg_numbers()!=[] and \
                                  max(diag.get_vertex_leg_numbers()) > min_vert:
            # Ignore any diagrams with 4-particle vertices
            symmetry.append(0)
        else:
            symmetry.append(1)

    logger.info("Finding symmetric diagrams for subprocess group %s" % \
                subproc_group.get('name'))

    # diagram_tags is a list of unique tags
    diagram_tags = []
    # diagram_classes is a list of lists of diagram numbers belonging
    # to the different classes
    diagram_classes = []
    perms = []
    for idiag, diag in enumerate(diagrams):
        if diag.get_vertex_leg_numbers()!=[] and \
                                  max(diag.get_vertex_leg_numbers()) > min_vert:
            # Only include vertices up to min_vert
            continue
        tag = IdentifySGConfigTag(diag, model)
        try:
            ind = diagram_tags.index(tag)
        except ValueError:
            diagram_classes.append([idiag + 1])
            perms.append([tag.get_external_numbers()])
            diagram_tags.append(tag)
        else:
            diagram_classes[ind].append(idiag + 1)
            perms[ind].append(tag.get_external_numbers())
    for inum, diag_number in enumerate(diagram_numbers):
        if symmetry[inum] == 0:
            continue
        idx1 = [i for i, d in enumerate(diagram_classes) if \
                diag_number in d][0]
        idx2 = diagram_classes[idx1].index(diag_number)
        # Note that for subproc groups, we want symfact to be 1
        if idx2 > 0:
            symmetry[inum] = -diagram_classes[idx1][0]
        # Order permutations according to how to reach the first perm
        permutations[inum] = diagram_generation.DiagramTag.reorder_permutation(perms[idx1][idx2],
                                                            perms[idx1][0])
    return (symmetry, permutations, [permutations[0]])
    

def old_find_symmetry_subproc_group(subproc_group):
    """Find symmetries between the configs in the subprocess group.
    For each config, find all matrix elements with maximum identical
    particle factor. Then take minimal set of these matrix elements,
    and determine symmetries based on these."""

    assert isinstance(subproc_group, group_subprocs.SubProcessGroup),\
           "Argument to find_symmetry_subproc_group has to be SubProcessGroup"

    matrix_elements = subproc_group.get('matrix_elements')

    contributing_mes, me_config_dict = \
                      find_matrix_elements_for_configs(subproc_group)

    nexternal, ninitial = matrix_elements[0].get_nexternal_ninitial()

    all_symmetry = {}
    all_perms = {}

    for me_number in contributing_mes:
        diagram_config_map = dict([(i,n) for i,n in \
                       enumerate(subproc_group.get('diagram_maps')[me_number]) \
                                   if n > 0])
        symmetry, perms, ident_perms = find_symmetry(matrix_elements[me_number])

        # Go through symmetries and remove those for any diagrams
        # where this ME is not supposed to contribute
        for isym, sym_config in enumerate(symmetry):
            if sym_config == 0 or isym not in diagram_config_map:
                continue
            config = diagram_config_map[isym]
            if config not in me_config_dict[me_number] or \
               sym_config < 0 and diagram_config_map[-sym_config-1] not in \
               me_config_dict[me_number]:
                symmetry[isym] = 1
                perms[isym]=list(range(nexternal))
                if sym_config < 0 and diagram_config_map[-sym_config-1] in \
                       me_config_dict[me_number]:
                    symmetry[-sym_config-1] -= 1

        # Now update the maps all_symmetry and all_perms
        for isym, (perm, sym_config) in enumerate(zip(perms, symmetry)):
            if sym_config in [0,1] or isym not in diagram_config_map:
                continue
            config = diagram_config_map[isym]

            all_perms[config] = perm

            if sym_config > 0:
                all_symmetry[config] = sym_config
            else:
                all_symmetry[config] = -diagram_config_map[-sym_config-1]

    # Fill up all_symmetry and all_perms also for configs that have no symmetry
    for iconf in range(len(subproc_group.get('mapping_diagrams'))):
        all_symmetry.setdefault(iconf+1, 1)
        all_perms.setdefault(iconf+1, list(range(nexternal)))
        # Since we don't want to multiply by symmetry factor here, set to 1
        if all_symmetry[iconf+1] > 1:
            all_symmetry[iconf+1] = 1

    symmetry = [all_symmetry[key] for key in sorted(all_symmetry.keys())]
    perms = [all_perms[key] for key in sorted(all_perms.keys())]

    return symmetry, perms, [perms[0]]
        

def find_matrix_elements_for_configs(subproc_group):
    """For each config, find all matrix elements with maximum identical
    particle factor. Then take minimal set of these matrix elements."""

    matrix_elements = subproc_group.get('matrix_elements')

    n_mes = len(matrix_elements)

    me_config_dict = {}

    # Find the MEs with maximum ident factor corresponding to each config.
    # Only include MEs with identical particles (otherwise no contribution)
    for iconf, diagram_list in \
                           enumerate(subproc_group.get('diagrams_for_configs')):
        # Check if any diagrams contribute to config
        if set(diagram_list) == set([0]):
            continue
        # Add list of MEs with maximum ident factor contributing to this config
        max_ident = max([matrix_elements[i].get('identical_particle_factor') \
                         for i in range(n_mes) if diagram_list[i] > 0])
        max_mes = [i for i in range(n_mes) if \
                   matrix_elements[i].get('identical_particle_factor') == \
                   max_ident and diagram_list[i] > 0 and  max_ident > 1]
        for me in max_mes:
            me_config_dict.setdefault(me, [iconf+1]).append(iconf + 1)

    # Make set of the configs
    for me in me_config_dict:
        me_config_dict[me] = sorted(set(me_config_dict[me]))

    # Sort MEs according to 1) ident factor, 2) number of configs they
    # contribute to
    def me_sort(me1, me2):
        return (matrix_elements[me2].get('identical_particle_factor') \
                - matrix_elements[me1].get('identical_particle_factor'))\
                or (len(me_config_dict[me2]) - len(me_config_dict[me1]))

    sorted_mes = sorted([me for me in me_config_dict], key=misc.cmp_to_key(me_sort))

    # Reduce to minimal set of matrix elements
    latest_me = 0
    checked_configs = []
    while latest_me < len(sorted_mes):
        checked_configs.extend(me_config_dict[sorted_mes[latest_me]])
        for me in sorted_mes[latest_me+1:]:
            me_config_dict[me] = [conf for conf in me_config_dict[me] if \
                                  conf not in checked_configs]
            if me_config_dict[me] == []:
                del me_config_dict[me]
        # Re-sort MEs
        sorted_mes = sorted([me for me in me_config_dict], me_sort)
        latest_me += 1

    return sorted_mes, me_config_dict    
