"""
This file is a collection of functions to interact with eppy.
They could do with some sorting and refactoring.
"""

import contextlib
import numpy as np

import config
from eppy.modeleditor import IDF
import json

from functools import partial
import warnings

from errors import ModeError

def convert_format(s: str, place, mode):
    """Converts energyPlus names to their Eppy equivalents

    :param s: name to convert
    :param place: the type of place that is being used 'field' or 'class'
    :param mode: whether to convert to idf or json formating
    :return: the converted name
    """
    # TODO: Find an authoritative source for the naming conventions
    if mode == 'idf':
        if place == 'field':
            return s.replace(' ', '_').replace('-', '')
        if place == 'class':
            return s.upper()
    if mode == 'json':
        if place == 'field':
            return s.replace(' ', '_').replace('-', '_').lower()
        if place == 'class':
            # uses camel case, s.title() removes some of the capitals and does not work
            return s
    raise ModeError(message=f'no format defined for place:{place} and mode:{mode}')


def get_mode(building):
    if isinstance(building, IDF):
        return 'idf'
    if isinstance(building, dict):
        return 'json'
    raise ModeError(message=f'Cannot find a valid mode for {building}')


def get_idf(idf_file: str = config.files.get('idf'),
            idd_file: str = config.files.get('idd'),
            output_directory=config.out_dir) -> IDF:
    """Uses eppy to read an idf file and generate the corresponding idf object"""
    # Trying to change the idd file inside a program will cause an error
    IDF.setiddname(idd_file)
    # TODO: Fix this rather than hiding it.
    # calling IDF causes a warning to appear, currently redirect_stdout hides this.
    with contextlib.redirect_stdout(None):
        idf = IDF(idf_file)
        # override the output location so I stop messing up
        idf.run = partial(idf.run, output_directory=output_directory)
        return idf


def get_building(building=None,
                 data_dict=None,
                 output_directory=config.out_dir,
                 mode=None):
    if mode is None and building is None:
        building = config.files.get('building')
        mode = config.energy_plus_mode
    if mode is None:
        ending = building.split('.')[-1]
        if ending == 'idf':
            mode = 'idf'
        elif ending == 'epJSON':
            mode = 'json'
        else:
            warnings.warn(f'expected "idf" or "epJSON" file. building has extension {ending}')
    if building is None:
        building = config.files[mode]
    if mode == 'idf':
        data_dict = data_dict or config.files['idd']
        return get_idf(idf_file=building, idd_file=data_dict, output_directory=output_directory)
    elif mode == 'json':
        with open(building) as f:
            return json.load(f)
    ModeError(mode)


def get_windows(building):
    mode = get_mode(building)
    if mode == 'idf':
        return ((window.Name, window) for window in building.idfobjects['FENESTRATIONSURFACE:DETAILED'])
    elif mode == 'json':
        return building['FenestrationSurface:Detailed'].items()
    else:
        raise ModeError(mode)


def wwr_all(building, wwr: float) -> None:
    """Sets the wwr for all walls that have a window.
    Will malfunction if there are multiple windows on one wall
    """
    mode = get_mode(building)
    windows = get_windows(building)
    for window_name, window in windows:
        wwr_single(window, wwr, building, mode)


def set_vertex(idfObj, vertexNum: int, x: float = 0, y: float = 0, z: float = 0):
    """Sets a single vertex of the passed idfObject (idfObj)
     to the specified x,y and z coordinates."""
    for val, name in zip((x, y, z), 'XYZ'):
        idfObj['Vertex_{}_{}coordinate'.format(vertexNum, name)] = round(val, 2)


def one_window(building):
    """Removes some windows so that each wall has no more than one"""
    mode = get_mode(building)
    walls = set()
    toRemove = []
    windows = get_windows(building)
    for window_name, window in windows:
        if mode == 'idf':
            wall_name = window.Building_Surface_Name
        elif mode == 'json':
            wall_name = window['building_surface_name']
        else:
            raise ModeError(mode)
        if wall_name in walls:
            toRemove.append((window_name, window))
        else:
            walls.add(wall_name)
    if mode == 'idf':
        for window_name, window in toRemove:
            building.idfobjects['FENESTRATIONSURFACE:DETAILED'].remove(window)
    elif mode == 'json':
        for window_name, window in toRemove:
            building['FenestrationSurface:Detailed'].pop(window_name)


def wwr_single(window, wwr: float, building, mode):
    """Sets `window` to have the window to wall ratio specified by `wwr`"""

    # will not work for some orientations
    # multiple windows on a single wall will break this
    if mode == 'idf':
        def coordinates(ax):
            return [window[f'Vertex_{n}_{ax.upper()}coordinate'] for n in range(1, 5)]

        wall = building.getobject('BUILDINGSURFACE:DETAILED', window.Building_Surface_Name)
    elif mode == 'json':
        def coordinates(ax):
            return [window[f'vertex_{n}_{ax.lower()}_coordinate'] for n in range(1, 5)]

        wall = building['BuildingSurface:Detailed'][window['building_surface_name']]
    else:
        raise ModeError(mode)

    xs = coordinates('X')
    ys = coordinates('Y')
    zs = coordinates('Z')

    if max(ys) == min(ys):
        axis = 'x'
        axs = xs
    elif max(xs) == min(xs):
        axis = 'y'
        axs = ys
    else:
        raise ValueError('The window is not aligned with the x or y axes')
    width = max(axs) - min(axs)
    scale_factor = np.sqrt(wwr)
    new_width = width * scale_factor
    height = max(zs) - min(zs)
    new_height = height * scale_factor

    startW = (width - new_width) / 2
    endW = startW + new_width
    startH = (height - new_height) / 2
    endH = startH + new_height

    # Maintains vertex order by mimicking the current order
    s = [0] * 4
    for vertex in range(0, 4):
        if zs[vertex] == max(zs):
            # vertex on the top
            if axs[vertex] == max(axs):
                # TOP RIGHT
                s[0] += 1
                set_vertex(window, vertex + 1, z=endH, **{axis: endW})
            else:
                # TOP LEFT
                s[1] += 1
                set_vertex(window, vertex + 1, z=endH, **{axis: startW})
        else:
            if axs[vertex] == max(axs):
                # BOTTOM RIGHT
                s[2] += 1
                set_vertex(window, vertex + 1, z=startH, **{axis: endW})
            else:
                # BOTTOM LEFT
                s[3] += 1
                set_vertex(window, vertex + 1, z=startH, **{axis: startW})
    assert s == [1] * 4, ('verticesS are wrong:', s)
