"""A comparison function for generating a smart diff from extracted content
"""

import hashlib
import json
from typing import Any, Dict, List, Optional


def _hash(obj):
    text = None
    if isinstance(obj, dict):
        if 'name' in obj:
            if obj['name'] == 'textblock':
                text = obj['block_text']
            elif obj['name'] == 'line':
                text =  " ".join([s['text'] for s in obj['spans']])
            elif obj['name'] == 'span':
                text =  obj['text']
            elif obj['name'] == 'image':
                text = "image-" + str(obj['image'])
            elif obj['name'] == 'aside':
                text = "aside-" + _hash(obj['items'])
            elif obj['name'] == 'table':
                text = "table-" + _hash(obj['cells'][0])
            elif obj['name'] == 'textlist':
                text = 'list-' + _hash(obj['items'])
            elif obj['name'] == 'textlistitem':
                text = 'listitem-' + _hash(obj['items'])
    elif isinstance(obj, list):
        parts = [_hash(p) for p in obj]
        parts.sort()
        text = "-".join(parts)
    if not text:
        text = json.dumps(obj, sort_keys=True, indent=2)
    return hashlib.md5(text.encode('utf-8'), usedforsecurity=False).hexdigest()


def _diff_value(path: str, value1: Any, value2: Any) -> List[Dict[str, Any]]:
    if value1 != value2:
        return [{'path': path, 'type': 'change', 'old': value1, 'new': value2}]
    return []


def _diff_dict(path: str, dict1: Dict[str, Any], dict2: Dict[str, Any]) -> List[Dict[str, Any]]:
    changes = []
    
    if path != "":
        root_path = f"{path}."
    else:
        root_path = ""
        
    for k in dict1:
        k_path = root_path + k
        if k not in dict2:
            changes.append(
                {'path': k_path, 'type': 'deletion', 'old': dict1[k]})
        else:
            changes += _do_diff(k_path, dict1[k], dict2[k])

    for k in dict2:
        k_path = root_path + k
        if k not in dict1:
            changes.append(
                {'path': k_path, 'type': 'addition', 'new': dict2[k]})
    return changes


def _diff_list(path: str, list1: List[Any], list2: List[Any]) -> List[Dict[str, Any]]:
    changes = []

    l1_hashes = {_hash(v): i for i, v in enumerate(list1)}
    l2_hashes = {_hash(v): j for j, v in enumerate(list2)}
              
    for l1_hash in l1_hashes:
        l1_index = l1_hashes[l1_hash]
        if l1_hash not in l2_hashes:
            changes.append(
                {'path': path, 'type': 'deletion', 'old': list1[l1_index]})
            continue
        l2_index = l2_hashes[l1_hash]
        if l1_index != l2_index:
            changes.append({'path': path, 'type': 'reorder',
                           'value': list1[l1_index], 'old': l1_index, 'new': l2_index})

        changes += _do_diff(f"{path}.[{l1_index},{l2_index}]",
                            list1[l1_index], list2[l2_index])

    for l1_hash in l2_hashes:
        if l1_hash not in l1_hashes:
            changes.append({'path': path, 'type': 'addition',
                           'new': list2[l2_hashes[l1_hash]]})

    return changes


def _do_diff(path: str, obj1: Any, obj2: Any) -> List[Dict[str, Any]]:
    v1_type = type(obj1).__name__
    v2_type = type(obj2).__name__
    if v1_type != v2_type:
        return [{'path': path, 'type': 'change', 'old': obj1, 'new': obj2}]

    if isinstance(obj1, dict):
        return _diff_dict(path, obj1, obj2)

    if isinstance(obj1, list):
        return _diff_list(path, obj1, obj2)

    return _diff_value(path, obj1, obj2)


def compare(obj1: Dict[str, Any], obj2: Dict[str, Any], ignore_paths: Optional[List[str]] = None) -> List[Dict[str, Any]]:
    """Compares to JSON objects generated by burdoc and returns a list of changes.
    Unlike most dictionary comparison systems, this detects re-orderings as well as changes
    

    Args:
        obj1 (Dict[str, Any]): A JSON output from Burdoc.
        obj2 (Dict[str, Any]): A JSON output from Burdoc.
        ignore_paths (List[str]): Any paths to ignore changes. Use this to exclude unstable fields such as file paths.

    Returns:
        List[Dict[str, Any]]: A list of changes in format:
        ::
        
                [
                    {
                        'path':path to change in object, 
                        'type':[change, addition, deletion, reorder], 
                        'old':old value, 
                        'new':new value, 
                        'value':value of the object (only used for reorder)
                    }
                ]
    """
    if not ignore_paths:
        ignore_paths = []

    changes = [c for c in _do_diff(
        "", obj1, obj2) if c['path'] not in ignore_paths]
    return changes
