from nesta.core.orms.orm_utils import db_session
from nesta.core.orms.orm_utils import get_mysql_engine
from nesta.core.orms.mag_orm import FieldOfStudy as FoS

from more_itertools import unique_everseen
from collections import defaultdict
import json

class _UniqueList(list):
    """List with insert-ordered unique entries"""
    def append(self, item):
        if item not in self:
            super().append(item)


def _unique_index(item, pairs):
    """Find the 'unique_everseen' index 
    of an item in an iterable. The each item in
    the iterable is assumed to be a pair of values,
    and only the first element is inspected.

    For example, with a = [2,2,1,3,5,5,3,4],
    _unique_index(1, a) => 1
    _unique_index(2, a) => 0
    _unique_index(3, a) => 2
    _unique_index(4, a) => 4
    _unique_index(4, a) => 3
    because unique_everseen(a) = [2,1,3,5,4]    
    """
    first_items = (it[0] for it in pairs)
    uniques = list(unique_everseen(first_items))
    return uniques.index(item)


def _find_index(field, fos_map):
    """
    Find the (fos_level, idx) index of a field within
    the FoS map.    
    """
    for i_level, fields in fos_map.items():
        try:
            return i_level, _unique_index(field, fields)
        except ValueError:
            pass
    return None, None


def _make_fos_map(fos_rows, fos_lookup):
    """
    Apply the FoS lookup to the rows of FoS for this
    data row, in order to build the intermediate FoS map.
    """
    ids = set(fos['id'] for fos in fos_rows)
    fos_map = defaultdict(_UniqueList)
    for row in fos_rows:
        # Get child ids and remove missing ids
        parent_ids = split_ids(row['parent_ids'], ids)
        child_ids = split_ids(row['child_ids'], ids)        
        # Iterate over children
        for cid in child_ids:
            parent, child = fos_lookup[(row['id'], cid)]
            level = row['level']
            if len(parent_ids) == 0: 
                fos_map[level].append([parent])
            fos_map[level + 1].append([child, parent])
    return fos_map


def _make_fos_tree(fos_map):
    """
    Convert the FoS map to a full (and useful) 
    tree structure
    """
    nodes = defaultdict(dict)
    links = []
    # Iterate over each level in the FoS map
    for _, fields in fos_map.items():
        # Iterate over each field in this level
        for row in fields:
            child = row[0]  # Indexing used since size not known
            parent = row[-1]  # Note: child could equal parent
            c_level, c_idx = _find_index(child, fos_map)
            if c_level is None:
                continue
            if child != parent:
                # If there is a parent, generate the link
                # from parent to child
                p_level, p_idx = _find_index(parent, fos_map)
                if p_level is not None:
                    links.append([[p_level, p_idx],
                                  [c_level, c_idx]])
            nodes[c_level][c_idx] = child
    # Convert nodes to nested arrays
    for level, node_collection in nodes.items():
        nodes[level] = intdict_to_list(node_collection)
    nodes = intdict_to_list(nodes)
    return {'nodes': nodes, 'links': sorted(links)}


def intdict_to_list(_dict):
    _list = list()
    keys = sorted((int(k) for k in _dict.keys()))
    if len(keys) == 0:
        return _list
    seqential_keys = range(max(keys)+1)
    if not all(k in seqential_keys for k in keys):
        raise ValueError('All keys in dict must be ints.')
    for k in seqential_keys:
        value = _dict[k] if k in keys else []
        _list.append(value)
    return _list


def make_fos_tree(fos_rows, fos_lookup):
    """Convert the FoS for this data row to 
    a useful tree structure.
    
    Args:
        fos_rows (list): List of MAG fields of study, from the ORM.
        fos_lookup (dict): Lookup table generated by :obj:`build_fos_lookup`
    Returns:
        fos_tree (dict): Useful tree-like representation of the FoS.
    """
    fos_map = _make_fos_map(fos_rows, fos_lookup)
    fos_tree = _make_fos_tree(fos_map)
    return fos_tree


def split_ids(ids, existing=None):
    """Split the child or parent ID fields from the MAG FoS ORM.
    
    Args:
        ids (str): Comma-delimited string of ids
        existing (set): Only return IDs found in this set.
    Returns:
        ids (set): A set of ids split out of the input.
    """
    if ids is None:
        return set()
    found = set(int(x) for x in ids.split(','))
    if existing is None:
        existing = found
    missing = found - existing
    return found - missing


def build_fos_lookup(engine, max_lvl=2):
    """A FoS lookup table of IDs to names
    
    Args:
        engine (sqlalchemy.engine): Database engine.
        max_lvl (int): Maximum FoS level to consider
    Returns:
        fos_lookup (dict): Lookup of IDs to names, where keys and values are (parent, child) pairs.
    """
    with db_session(engine) as session:
        fos = [f.__dict__ for f in (session.query(FoS)
                                    .filter(FoS.level <= max_lvl)
                                    .all())]
    fos_children = {f['id']: split_ids(f['child_ids'])
                    for f in fos}
    fos_names = {f['id']: f['name'] for f in fos}    
    return {(pid, cid): [fos_names[pid], fos_names[cid]]
            for pid, children in fos_children.items()
            for cid in children if cid in fos_children}

from nesta.core.orms.orm_utils import db_session
from nesta.core.orms.orm_utils import get_mysql_engine
from nesta.core.orms.mag_orm import FieldOfStudy as FoS

from more_itertools import unique_everseen
from collections import defaultdict
import json

class _UniqueList(list):
    """List with insert-ordered unique entries"""
    def append(self, item):
        if item not in self:
            super().append(item)


def _unique_index(item, pairs):
    """Find the 'unique_everseen' index 
    of an item in an iterable. The each item in
    the iterable is assumed to be a pair of values,
    and only the first element is inspected.

    For example, with a = [2,2,1,3,5,5,3,4],
    _unique_index(1, a) => 1
    _unique_index(2, a) => 0
    _unique_index(3, a) => 2
    _unique_index(4, a) => 4
    _unique_index(4, a) => 3
    because unique_everseen(a) = [2,1,3,5,4]    
    """
    first_items = (it[0] for it in pairs)
    uniques = list(unique_everseen(first_items))
    return uniques.index(item)


def _find_index(field, fos_map):
    """
    Find the (fos_level, idx) index of a field within
    the FoS map.    
    """
    for i_level, fields in fos_map.items():
        try:
            return i_level, _unique_index(field, fields)
        except ValueError:
            pass
    return None, None


def _make_fos_map(fos_rows, fos_lookup):
    """
    Apply the FoS lookup to the rows of FoS for this
    data row, in order to build the intermediate FoS map.
    """
    ids = set(fos['id'] for fos in fos_rows)
    fos_map = defaultdict(_UniqueList)
    for row in fos_rows:
        # Get child ids and remove missing ids
        parent_ids = split_ids(row['parent_ids'], ids)
        child_ids = split_ids(row['child_ids'], ids)        
        # Iterate over children
        for cid in child_ids:
            parent, child = fos_lookup[(row['id'], cid)]
            level = row['level']
            if len(parent_ids) == 0: 
                fos_map[level].append([parent])
            fos_map[level + 1].append([child, parent])
    return fos_map


def _make_fos_tree(fos_map):
    """
    Convert the FoS map to a full (and useful) 
    tree structure
    """
    nodes = defaultdict(dict)
    links = []
    # Iterate over each level in the FoS map
    for _, fields in fos_map.items():
        # Iterate over each field in this level
        for row in fields:
            child = row[0]  # Indexing used since size not known
            parent = row[-1]  # Note: child could equal parent
            c_level, c_idx = _find_index(child, fos_map)
            if c_level is None:
                continue
            if child != parent:
                # If there is a parent, generate the link
                # from parent to child
                p_level, p_idx = _find_index(parent, fos_map)
                if p_level is not None:
                    links.append([[p_level, p_idx],
                                  [c_level, c_idx]])
            nodes[c_level][c_idx] = child
    # Convert nodes to nested arrays
    for level, node_collection in nodes.items():
        nodes[level] = intdict_to_list(node_collection)
    nodes = intdict_to_list(nodes)
    return {'nodes': nodes, 'links': sorted(links)}


def intdict_to_list(_dict):
    _list = list()
    keys = sorted((int(k) for k in _dict.keys()))
    if len(keys) == 0:
        return _list
    seqential_keys = range(max(keys)+1)
    if not all(k in seqential_keys for k in keys):
        raise ValueError('All keys in dict must be ints.')
    for k in seqential_keys:
        value = _dict[k] if k in keys else []
        _list.append(value)
    return _list


def make_fos_tree(fos_rows, fos_lookup):
    """Convert the FoS for this data row to 
    a useful tree structure.
    
    Args:
        fos_rows (list): List of MAG fields of study, from the ORM.
        fos_lookup (dict): Lookup table generated by :obj:`build_fos_lookup`
    Returns:
        fos_tree (dict): Useful tree-like representation of the FoS.
    """
    fos_map = _make_fos_map(fos_rows, fos_lookup)
    fos_tree = _make_fos_tree(fos_map)
    return fos_tree


def split_ids(ids, existing=None):
    """Split the child or parent ID fields from the MAG FoS ORM.
    
    Args:
        ids (str): Comma-delimited string of ids
        existing (set): Only return IDs found in this set.
    Returns:
        ids (set): A set of ids split out of the input.
    """
    if ids is None:
        return set()
    found = set(int(x) for x in ids.split(','))
    if existing is None:
        existing = found
    missing = found - existing
    return found - missing


def build_fos_lookup(engine, max_lvl=2):
    """A FoS lookup table of IDs to names
    
    Args:
        engine (sqlalchemy.engine): Database engine.
        max_lvl (int): Maximum FoS level to consider
    Returns:
        fos_lookup (dict): Lookup of IDs to names, where keys and values are (parent, child) pairs.
    """
    with db_session(engine) as session:
        fos = [f.__dict__ for f in (session.query(FoS)
                                    .filter(FoS.level <= max_lvl)
                                    .all())]
    fos_children = {f['id']: split_ids(f['child_ids'])
                    for f in fos}
    fos_names = {f['id']: f['name'] for f in fos}    
    return {(pid, cid): [fos_names[pid], fos_names[cid]]
            for pid, children in fos_children.items()
            for cid in children if cid in fos_children}

List with insert-ordered unique entries Find the 'unique_everseen' index      of an item in an iterable. The each item in     the iterable is assumed to be a pair of values,     and only the first element is inspected.      For example, with a = [2,2,1,3,5,5,3,4],     _unique_index(1, a) => 1     _unique_index(2, a) => 0     _unique_index(3, a) => 2     _unique_index(4, a) => 4     _unique_index(4, a) => 3     because unique_everseen(a) = [2,1,3,5,4] Find the (fos_level, idx) index of a field within     the FoS map. Apply the FoS lookup to the rows of FoS for this     data row, in order to build the intermediate FoS map. # Get child ids and remove missing ids # Iterate over children Convert the FoS map to a full (and useful)      tree structure # Iterate over each level in the FoS map # Iterate over each field in this level # Indexing used since size not known # Note: child could equal parent # If there is a parent, generate the link # from parent to child # Convert nodes to nested arrays Convert the FoS for this data row to      a useful tree structure.          Args:         fos_rows (list): List of MAG fields of study, from the ORM.         fos_lookup (dict): Lookup table generated by :obj:`build_fos_lookup`     Returns:         fos_tree (dict): Useful tree-like representation of the FoS. Split the child or parent ID fields from the MAG FoS ORM.          Args:         ids (str): Comma-delimited string of ids         existing (set): Only return IDs found in this set.     Returns:         ids (set): A set of ids split out of the input. A FoS lookup table of IDs to names          Args:         engine (sqlalchemy.engine): Database engine.         max_lvl (int): Maximum FoS level to consider     Returns:         fos_lookup (dict): Lookup of IDs to names, where keys and values are (parent, child) pairs.