#!/usr/bin/env python2.7
# -*- coding: utf-8; -*-

##################################################################
# Libraries
import sys
import re
from os import environ
from os.path import basename, splitext
from alt_argparse import argparser
from alt_fio import AltFileInput, AltFileOutput
from ld import skip_comments
from ld.stringtools import is_xml_tag
from collections import Counter, defaultdict

##################################################################
# Constants
# character ranges
PUNCT = re.compile('[-.?!@#"$%^&*()|]')
# separators
FIELD_DELIMITER    = '\t'
POLARITY_DELIMITER = '/'
FIELD_DELIMITER_RE = re.compile(FIELD_DELIMITER)
# header of sentiment vocabulary class
CLASS_MARKER       = re.compile(r"^##\s*!\s*CLASS\s*:\s*(\S.*)")
# markers for sentence
SENTENCE_START  = r"<sentence>"
SENTENCE_STAR   = SENTENCE_START[:-1] + " class=\""
SENTENCE_END_RE = re.compile(r"<sentence\s*/>")
# markers for words
WORD_STAR          = "<word cnt=\""
WORD_END           = r"</word>"
# polarity classes
NEUT               = r"neut"
POS                = r"pos"
NEG                = r"neg"

INFTAG_START       = set(["V", "N"])
NONINF_WORDS       = set(["sollen", "haben", "sein", "werden", "müssen", \
                              "wollen", "können", "mögen", "eine", "die", \
                              "mit", "ohne", "so", "dann", "hier", "also",
                          "auch", "lt", "gt", "immer", "mehr"])
NEGATORS           = set(["keine", "nicht", "wenig", "kaum", "schwerlich"])
NOUNTAGS           = ["NN", "NE", "XY"]
VERBTAGS           = ["VVFIN", "VVIMP", "VVINF", "VVIZU", "VVPP", "VAFIN", \
                          "VAIMP", "VAINF", "VAPP", "VMFIN", "VMINF", "VMPP"]
ADJTAGS            = ["ADJA", "ADJD"]
ADVTAGS            = ["ADV"]
ALLTAGS = set(NOUNTAGS + VERBTAGS + ADJTAGS + ADVTAGS)
NEGATOR_CANCELLOR  = dict({"keine": set(NOUNTAGS + VERBTAGS), \
                               "nicht": ALLTAGS - set(NOUNTAGS), \
                               "wenig": ALLTAGS, \
                               "kaum":  ALLTAGS, \
                               "schwerlich": ALLTAGS})

##################################################################
# Variables
word = tag = lemma = ''
fields   = []
lines    = []
sentiw   = dict()
sentiwt  = dict()
negators = []
swght_v  = []
swords   = Counter()
current_negator = ""

##################################################################
# Functions
def read_class_file(ifile):
    """Read list of words of particular class with their probabilities.

    The type of the class (so far only positive or negative) for which the file
    is being read is determined in two ways. By default, it is the basename of
    the input file without its extension. But if somewhere in the file a
    comment of type `##! CLASS: ' occurs, the string token following this
    comment will be assumed, as class name.
    """
    # by default, the name of the class will be the name of the file, from
    # which the list is being read with its path and extension stripped off
    classname = splitext(basename(ifile.filename))[0]
    classmatch = None
    word = tag = ''
    weight = 0.0
    for line in ifile:
        classmatch = CLASS_MARKER.match(line)
        if classmatch:
            classname = classmatch.group(1)
            continue
        line = skip_comments(line)
        if line:
            # please note, that input line should contain exactly 3
            # tab-separated fields and word should already be lowercased
            word, tag, weight = FIELD_DELIMITER_RE.split(line)
            # We don't check if same (word, tag) pair appears twice. If it does
            # the latter encountered will overwrite the former Also, if tag
            # appears to be `...', we assume that this word is polar regardless
            # of its tag and therefore put it into another hash
            if tag == "...":
                sentiw[word] = float(weight)
            else:
                sentiwt[word, tag] = float(weight)
# file descriptor will be closed automatically at the end of the for loop

def reset_counters():
    """Reset all counters for given sentence."""
    global swords, swght_v, negators, current_negator
    swords.clear()
    swght_v  = []
    negators = []
    current_negator = ""

def print_sentence():
    """Output sentence along with its polarity class and informative words."""
    wght = 0.0
    global negators, current_negator
    # if we have landed at the end of the sentence and we have some unresolved
    # negators, assume that they negate verbs
    for (tag, wweight) in swght_v:
        if current_negator:
            wweight = -wweight
            negators.pop()
            current_negator = negators[-1] if negators else ""
        wght += wweight
    if wght == 0.0:
        sclass = NEUT
    elif wght < 0.0:
        sclass = NEG
    else:
        sclass = POS

    foutput.fprint(sclass)
    for (w,c) in swords.iteritems():
        foutput.fprint(str(c) + '\t' + w)

def invert_weight(wght):
    """Change the sign of the weight from negative to positive."""
    if current_negator:
        return -wght
    return wght

##################################################################
# Arguments
argparser.description="""Utility for assigning sentiment classes and
probabilities to words."""
argparser.add_argument("--esc-char", help = """escape character which should
precede lines with meta-information""", nargs = 1, type = str, \
                           default = environ.get("SOCMEDIA_ESC_CHAR", ""))
argparser.add_argument('-c', '--class-file', help="""file with a list
of words belonging to a particular class along with their probabilities""", \
                           action='append', default = [class_filename.format(**environ) \
                                                           for class_filename in \
                                                           ("{SOCMEDIA_LINGTMP}/semdict/positive.gpc.smdict", \
                                                                "{SOCMEDIA_LINGTMP}/semdict/negative.gpc.smdict")])
argparser.add_argument("-n", "--enable-negation", help="enable recognition of sentiment negation", \
                           action = "store_true")
argparser.add_argument("-x", "--skip-xml", help = "skip XML tags", \
                           action = "store_true")
args = argparser.parse_args()

esc_char = args.esc_char
# establish input/output utilities
actnegate = args.enable_negation
foutput = AltFileOutput(flush = args.flush)
finput  = AltFileInput(*args.files, skip_line = args.skip_line, \
                            print_func = foutput.fprint)
# read statistics from files and populate sentiment classes
for cfile in args.class_file:
    read_class_file(AltFileInput(cfile))

##################################################################
# Main
# care of encoding/decoding, as well as skipping lines and XML markup will be
# taken in finput and foutput internals
for line in finput:
    if not line or line[0] == esc_char:
        foutput.fprint(line)
        continue
    elif SENTENCE_END_RE.match(line):
        print_sentence()
        reset_counters()
        foutput.fprint(line)
        continue

    fields = FIELD_DELIMITER_RE.split(line)
    # skip inappropriately formatted lines
    if len(fields) < 3:
        foutput.fprint(line)
        continue
    # lowercase lemma, so that we could refer to it later without worrying
    # about case
    word, tag, lemma = fields[:3]
    lemma = lemma.lower()
    # re-instantiate line with updated lemma
    # line = FIELD_DELIMITER.join([word, tag, lemma] + fields[3:])

    # check, if this lemma and tag are known to our sentiment vocabulary and
    # if it is, add known information to it
    if (lemma, tag) in sentiwt:
        swght_v.append((tag, invert_weight(sentiwt[(lemma, tag)])))
        continue
    elif lemma in sentiw:
        swght_v.append((tag, invert_weight(sentiw[lemma])))
        continue

    # if word appears to be informative according to its lemma and tag - add it
    # to our word counter
    if (tag[0] in INFTAG_START or (len(tag) > 2 and tag[0:3] == "ADJ")) \
            and len(lemma) > 3 and lemma not in NONINF_WORDS and \
            not PUNCT.search(lemma):
        swords.update([lemma])

    # if recognition of negation is activated, track negated words
    if actnegate:
        # check if we have encountered a negator
        if lemma in NEGATORS:
            # foutput.fprint(line)
            current_negator = lemma
            negators.append(current_negator)
        elif current_negator and current_negator in  NEGATOR_CANCELLOR and \
                tag in NEGATOR_CANCELLOR[current_negator]:
            negators.pop()
            current_negator = negators[-1] if negators else ""
