import logging
import pyparsing
import re
import numpy

POSSIBLE_REACTION_ARROWS = ('=', '=>', '<=>', '->',
                            '<->', u'\u2192', u'\u21CC')

class ParseError(Exception):
    pass


def _parsedCompound(c_list):
    """Always put a stoichiometric coefficient with a compound."""
    if len(c_list) == 2:
        return c_list[0], c_list[1]
    return 1, c_list[0]


def _MakeReactionSideParser():
    """Builds a parser for a side of a reaction."""
    # Coefficients are usually integral, but they can be floats or fractions too.
    int_coeff = pyparsing.Word(pyparsing.nums)
    float_coeff = pyparsing.Word(pyparsing.nums + '.' + pyparsing.nums)
    frac_coeff = int_coeff + '/' + int_coeff
    int_coeff.setParseAction(lambda i:int(i[0]))
    float_coeff.setParseAction(lambda t:float(t[0]))
    frac_coeff.setParseAction(lambda f:float(f[0])/float(f[2]))
    
    coeff = pyparsing.Or([int_coeff, float_coeff, frac_coeff])
    optional_coeff = pyparsing.Optional(coeff)
    
    compound_separator = pyparsing.Literal('+').suppress()
    
    compound_name_component = pyparsing.Word(pyparsing.alphanums + "()",
                                             pyparsing.alphanums + "-+,()'_")
    compound_name = pyparsing.Forward()
    compound_name << (compound_name_component + pyparsing.ZeroOrMore(compound_name_component))
    compound_name.setParseAction(lambda s: ' '.join(s))
    
    compound_with_coeff = pyparsing.Forward()
    compound_with_coeff << ((optional_coeff + compound_name) | compound_name)
    compound_with_coeff.setParseAction(_parsedCompound)
    compound_with_coeff.setResultsName("compound")
    
    compound_with_separator = pyparsing.Forward()
    compound_with_separator << (compound_with_coeff + compound_separator)
    
    reaction_side = pyparsing.Forward()
    reaction_side << (pyparsing.ZeroOrMore(compound_with_separator) +
                      compound_with_coeff)
    reaction_side.setParseAction(lambda l: [l])
    reaction_side.setResultsName("reaction_side")
    return reaction_side
    

def _MakeReactionParser():
    """Builds a pyparsing-based recursive descent parser for chemical reactions."""
    reaction_side = _MakeReactionSideParser()
    
    side_separators = [pyparsing.Literal(s) for s in POSSIBLE_REACTION_ARROWS]
    side_separator = pyparsing.Or(side_separators).suppress()
    
    reaction = pyparsing.Forward()
    reaction << (reaction_side + side_separator + reaction_side)
    return reaction


class ParsedReactionQuery(object):
    """A parsed reaction query."""
    
    def __init__(self, substrates=None, products=None):
        """Initialize the ParsedReaction object.
        
        Args:
            reactants: a list of tuples for the reactants.
            products: a list of tuples for the products.
        """
        self.substrates = substrates or []
        self.products = products or []
    
    def __eq__(self, other):
        """Equality test."""
        r = frozenset(self.substrates)
        p = frozenset(self.products)
        o_r = frozenset(other.substrates)
        o_p = frozenset(other.products)
        
        reactants_diff = r.symmetric_difference(o_r)
        products_diff = p.symmetric_difference(o_p)
        
        if not reactants_diff and not products_diff:
            return True
        
        return False
    
    def __str__(self):
        joined_rs = ['%s %s' % (numpy.abs(c),r) for c,r in self.substrates]
        joined_ps = ['%s %s' % (numpy.abs(c),p) for c,p in self.products]
        return '%s => %s' % (' + '.join(joined_rs), ' + '.join(joined_ps))


class QueryParser(object):
    """Parses search queries."""
    
    REACTION_PATTERN = u'.*(' + '|'.join(POSSIBLE_REACTION_ARROWS) + ').*'
    REACTION_MATCHER = re.compile(REACTION_PATTERN)
    
    def __init__(self):
        """Initialize the parser."""
        self._rparser = _MakeReactionParser()
        
    def is_reaction_query(self, query):
        """Returns True if this query is likely to be a reaction query.
        
        Args:
            query: the query string.
        """
        m = self.REACTION_MATCHER.match(query.strip())
        return m is not None
    
    def parse_reaction_query(self, query):
        """Parse the query as a reaction.
        
        Args:
            query: the query string.
        
        Returns:
            An initialized ParsedReaction object, or None if parsing failed.
        """
        try:
            results = self._rparser.parseString(query)
            substrates, products = results
            logging.debug('substrates = %s' % str(substrates))
            logging.debug('products = %s' % str(products))
            return ParsedReactionQuery(substrates, products)
        except pyparsing.ParseException as msg:
            logging.error('Failed to parse query %s', query)
            raise ParseError(msg)
        
