"""
Utils for the workchains.

Auxiliary routines that are not part of any of the workchain classes, but needed
to make code more compact in the workchains.
"""

# pylint: disable=import-outside-toplevel
from copy import deepcopy
from typing import Any, List, Optional

import numpy as np
from aiida.common.extendeddicts import AttributeDict
from aiida.engine.processes.exit_code import ExitCode
from aiida.orm import Dict
from aiida.plugins import DataFactory

from aiida_vasp.utils.extended_dicts import delete_keys_from_dict


def prepare_process_inputs(
    inputs: Any, namespaces: Optional[List[str]] = None, exclude_parameters: Optional[List[str]] = None
) -> AttributeDict:
    """
    Prepare the inputs dictionary for a calculation.

    Any remaining bare dictionaries in the inputs dictionary will be wrapped in a Dict data node
    except for the 'options', 'metadata', 'potential' and any key specified in the parameters
    namespaces. They all should remain a standard dictionary. Another exception are dictionaries
    whose keys are not strings but for example tuples.
    """
    prepared_inputs = AttributeDict()

    if namespaces is None:
        namespaces = []

    if exclude_parameters is None:
        exclude_parameters = []

    no_dict = ['options', 'metadata', 'potential', 'parameters']
    no_dict = no_dict + namespaces
    # Copy and convert dict
    for key, val in inputs.items():
        if key not in no_dict and isinstance(val, dict) and all([isinstance(k, str) for k in val.keys()]):  # pylint: disable=use-a-generator
            prepared_inputs[key] = Dict(dict=val)
        else:
            prepared_inputs[key] = val

    try:
        # Remove excluded entries for parameters
        parameters = prepared_inputs.parameters
        if exclude_parameters:
            # First make sure we have a proper copy so that any removal does not havoc elements in the dictionary
            if isinstance(parameters, DataFactory('core.dict')):
                parameters = prepared_inputs.parameters.clone()
                # Unpack in case the parameters is a Dict data structure
                parameters = parameters.get_dict()
            else:
                parameters = deepcopy(prepared_inputs.parameters)
            delete_keys_from_dict(parameters, exclude_parameters)
        if not isinstance(parameters, DataFactory('core.dict')):
            # Convert parameters to Dict
            parameters = DataFactory('core.dict')(dict=parameters)
        prepared_inputs.parameters = parameters
    except AttributeError:
        # In case parameters is not present at all
        pass

    return prepared_inputs


def compare_structures(structure_a: Any, structure_b: Any) -> AttributeDict:
    """Compare two StructureData objects A, B and return a delta (A - B) of the relevant properties."""

    delta = AttributeDict()
    delta.absolute = AttributeDict()
    delta.relative = AttributeDict()
    volume_a = structure_a.get_cell_volume()
    volume_b = structure_b.get_cell_volume()
    delta.absolute.volume = np.absolute(volume_a - volume_b)
    delta.relative.volume = np.absolute(volume_a - volume_b) / volume_a

    pos_a = np.array([site.position for site in structure_a.sites])
    pos_b = np.array([site.position for site in structure_b.sites])
    delta.absolute.pos = pos_a - pos_b

    site_vectors = [delta.absolute.pos[i, :] for i in range(delta.absolute.pos.shape[0])]
    a_lengths = np.linalg.norm(pos_a, axis=1)
    delta.absolute.pos_lengths = np.array([np.linalg.norm(vector) for vector in site_vectors])
    delta.relative.pos_lengths = np.array([np.linalg.norm(vector) for vector in site_vectors]) / a_lengths

    cell_lengths_a = np.array(structure_a.cell_lengths)
    delta.absolute.cell_lengths = np.absolute(cell_lengths_a - np.array(structure_b.cell_lengths))
    delta.relative.cell_lengths = np.absolute(cell_lengths_a - np.array(structure_b.cell_lengths)) / cell_lengths_a

    cell_angles_a = np.array(structure_a.cell_angles)
    delta.absolute.cell_angles = np.absolute(cell_angles_a - np.array(structure_b.cell_angles))
    delta.relative.cell_angles = np.absolute(cell_angles_a - np.array(structure_b.cell_angles)) / cell_angles_a

    return delta


def fetch_k_grid(rec_cell: Any, k_spacing: Any) -> List[int]:
    """
    Suggest a sensible k-point sampling based on a supplied spacing.

    :param rec_cell: A two dimensional ndarray of floats defining the reciprocal lattice with each vector
      as row elements.
    :param k_spacing: The k-point spacing.

    :return kgrid: The k-point grid given the supplied `rec_cell` and `kstep`

    This is usable for instance when performing
    plane wave cutoff convergence tests without a base k-point grid.

    """
    rec_cell_lenghts = np.linalg.norm(rec_cell, axis=1)
    kgrid = np.ceil(rec_cell_lenghts / np.float64(k_spacing))

    return kgrid.astype('int').tolist()


def compose_exit_code(status: int, message: str) -> ExitCode:
    """Compose an ExitCode instance based on a status and message."""
    exit_code = ExitCode(status=status, message=message)
    return exit_code


def site_magnetization_to_magmom(site_dict: Any) -> List[Any]:
    """
    Convert site magnetization to MAGMOM used for restart
    NOTE: Only tested for colinear cases
    """
    if 'site_magnetization' in site_dict:
        site_dict = site_dict['site_magnetization']

    site_dict = site_dict['sphere']
    to_use = None
    for symbol in 'xyz':
        if site_dict.get(symbol) and site_dict.get(symbol, {}).get('site_moment'):
            to_use = symbol
            break
    # No available site magnetization for setting MAGMOM, something is wrong
    if to_use is None:
        raise ValueError('No valid site-projected magnetization available')
    # Ensure sorted list
    tmp = list(site_dict[to_use]['site_moment'].items())
    tmp.sort(key=lambda x: int(x[0]))
    return [entry[1]['tot'] for entry in tmp]
