"""
This is an excerpt of code from the PLACES codebase that illustrates the
computation of the similarity index between seller and buyer names.

The function load_owner_word_counts() computes the frequency of single
words across all provided strings. It is usually run on a Series of all
buyer and seller names of a given locality (e.g. county) before the
computation of the similarity index.

The function owner_similarity() is then used to compute the similarity
index between two strings, usually a seller and buyer name.

Notes
-----

When a sale has multiple sellers or buyers, their names are aggregated
into a single string column (buy_name, sel_name)in PLACES. In order to
allow for the possibility to split the string afterwards, a sufficiently
unique string separator (SEP_STR) is used that does not otherwise occur
in the text data.
"""

import re
import numpy as np
import pandas as pd
from collections import Counter

# Separator for strings in your codebase
# (e.g. to be able to aggregate multiple owner names into a single
#  but then separate them again, when needed)
SEP_STR = ' ~~ '

# Regex for ignoring characters but keeping dashes
RE_IGNORECHARS_KEEP_DASHES = '[^0-9A-Z& -]+'

# Regex for ignoring characters
RE_IGNORECHARS = '[^0-9A-Z& ]+'

# Regex for ignoring short owner names
RE_IGNORESHORT = '( |^)[A-Z0-9&]{,2}(?=( |$))'

# Words excluded from owner similiarity computation
GENERIC_WORDS = ['LLC', 'LTD', 'LLP', 'LLLP', 'INC', 'EST',
                 'RET', 'INT', 'MTG', 'IRT', 'DEV', 'III', '2ND', '3RD',
                 'BANK', 'HOLDINGS', 'TRUST', 'REVOCABLE', 'ASSOCIATION',
                 'FAMILY', 'PROPERTIES', 'LIVING', 'COMPANY', 'ESTATE',
                 'PROPERTIES', 'NATIONAL', 'RANCH', 'DEVELOPMENT', 'CONDO',
                 'INVESTMENT', 'HOME', 'LOANS', 'INVESTMENTS', 'PARTNERS',
                 'PARTNERSHIP', 'LAND', 'CORPORATION', 'LIMITED',
                 'ENTERPRISES', 'GROUP', 'VENTURES', 'HOUSING', 'RESORT',
                 'RESORTS', 'CORP', 'ASSN', 'ASSOCIATES', 'IRREVOCABLE',
                 'REALTY', 'TRUSTS', 'VALLEY', 'CREEK', 'OWNERS', 'TOWNHOMES',
                 'RIDGE', 'LODGE', 'TRAILHEAD', 'MORTGAGE', 'COUNTY']

# OWNER SIMILARITY

def get_word_list(x, keep_dashes=False, ignore_gw=True):
    """Return list of words from a string that might contain owner names

    Parameters
    ----------
    x : string
        String of a name
    keep_dashes : boolean
        Keep combined names (e.g. STURM-WERNER) together
    ignore_gw : boolean
        Ignore generic words (from list GENERIC_WORDS)
    """

    if pd.isnull(x):
        return []

    if keep_dashes:
        x = re.sub(RE_IGNORECHARS_KEEP_DASHES, ' ', x)
    else:
        x = re.sub(RE_IGNORECHARS, ' ', x)

    wl = [n for n in re.sub(RE_IGNORESHORT, ' ', x).split(' ') if len(n) > 0]
    if ignore_gw:
        wl = [n for n in wl if n not in GENERIC_WORDS]
    return wl


def get_name_list(x, keep_dashes=False, ignore_gw=True, drop_first=False,
                  last_only=False):
    """Obtain list of words from a string that contains owner names

    Parameters
    ----------
    x : str
        Owner names (string separator: SEP_STR)
    keep_dashes : boolean
        Keep combined names (e.g. STURM-WERNER) together
    ignore_gw : boolean
        Ignore generic words (from list GENERIC_WORDS)
    drop_first : boolean
        Drop first words (hopefully first names) from each name
    last_only : boolean
        Return only last words from each name
    """
    if pd.isnull(x):
        return []

    # Get word lists of individual names
    namelists = [get_word_list(n, keep_dashes=keep_dashes,
                               ignore_gw=ignore_gw)
                 for n in x.split(SEP_STR)]
    if drop_first:
        namelists = [nl[1:] for nl in namelists if len(nl) > 1]
    if last_only:
        namelists = [nl[-1:] for nl in namelists if len(nl) > 0]
    return [n for nl in namelists for n in nl]


owner_word_counts = Counter()


def load_owner_word_counts(s):
    """Add counted words from a Series of names to owner_word_counts

    Parameters
    ----------
    s : pd.Series of str
        Series of owner names
    """
    global owner_word_counts
    owner_word_counts += Counter(get_word_list(' '.join(s.fillna('')),
                                               ignore_gw=False))


def get_sim_strength(l):
    """Get strength of owner name similarity

    Notes
    -----
    Computes the sum of (transformed) inverse word frequencies from a
    list of owner names (used to compute similarity)
    """
    return sum([1 / np.sqrt(owner_word_counts[w]) for w in l])


def owner_similarity(o1, o2, ignore_gw=True):
    """Compute the owner similarity score

    Parameters
    ----------
    o1, o2 : str
        Owner names (string separator: SEP_STR)
    ignore_gw : boolean
        Ignore generic words (from list GENERIC_WORDS)

    Notes
    -----
    Similarity scores is based on overlapping sets of words and their
    relative frequencies
    """
    os1 = set(get_name_list(o1, ignore_gw=ignore_gw))
    os2 = set(get_name_list(o2, ignore_gw=ignore_gw))
    if len(os1) == 0 or len(os2) == 0:
        return 0
    osim1 = get_sim_strength(os1 & os2) / get_sim_strength(os1)
    osim2 = get_sim_strength(os2 & os1) / get_sim_strength(os2)
    return max(osim1, osim2)
