import io
from datetime import datetime
from typing import List, Any
from xml.etree.ElementTree import ElementTree, Element, fromstring, register_namespace, SubElement
from ..kw.parser_exception import ParserException
from .vectorial_reservoir import VectorialReservoir
from .data_set_data import DataSet
from .keg5_well_property_inputs import Keg5WellPropertyInputs
keg5_AA_alias = "AA"
keg5_wellgeometry_alias = "keg5_wellgeometry"
keg5_reservoirgeometry_alias = "keg5_reservoirgeometry"
keg5_baseobjects_alias = "keg5_baseobjects"
keg5_well_alias = "keg5_well"
keg5_reservoir_alias = "keg5_reservoir"
keg5_petrophysics_alias = "keg5_petrophysics"
keg5_identifiers_alias = "keg5_identifiers"
keg5_wellcontrol_alias = "keg5_wellcontrol"
keg5_wellintake_alias = "keg5_wellintake"
[docs]
class ModelParser:
    """
        Parses XML from KEG5 KW document's model
    """
    # TODO default values to be changed?
    __default_z_top: float = 1828.8000000000002
    __default_layer_name = "Layer #1"
    __default_name = "Default"
    __ns = {
        keg5_AA_alias: 'KEG5',
        keg5_identifiers_alias: 'KEG5_Identifiers',
        keg5_baseobjects_alias: 'KEG5_BaseObjects',
        keg5_reservoir_alias: 'KEG5_Reservoir',
        keg5_well_alias: 'KEG5_Well',
        keg5_wellcontrol_alias: 'KEG5_WellControl',
        keg5_wellgeometry_alias: 'KEG5_WellGeometry',
        keg5_petrophysics_alias: 'KEG5_Petrophysics',
        keg5_reservoirgeometry_alias: 'KEG5_ReservoirGeometry',
        'keg5_pvt': 'KEG5_PVT',
        'keg5_compaction': 'KEG5_Compaction',
        'KEG5_desorption': 'KEG5_Desorption',
        'keg5_initialstate': 'KEG5_InitialState',
        'keg5_krpc': 'KEG5_KrPc',
        'keg5_aquifer': 'KEG5_Aquifer',
        'keg5_fault': 'KEG5_Fault',
        'keg5_welldata': 'KEG5_WellData',
        keg5_wellintake_alias: 'KEG5_WellIntake',
        'keg5_wellgroup': 'KEG5_WellGroup',
        'keg5_grid': 'KEG5_Grids',
        'keg5_results': 'KEG5_Results'
    }
[docs]
    def __init__(self, xml_string: str) -> None:
        for xmlns, value in self.__ns.items():
            register_namespace(xmlns, value)
        self.__tree = ElementTree(fromstring(xml_string))
        self.__root = self.__tree.getroot() 
[docs]
    def write_in_file(self, file_name: str) -> None:
        self.__tree.write(file_name) 
[docs]
    def export(self) -> str:
        stream = io.StringIO()
        self.__tree.write(stream, encoding='unicode', method='xml', xml_declaration=True)
        return stream.getvalue() 
[docs]
    def print_all(self) -> None:
        self.__print_nodes(self.__root, "") 
[docs]
    def add_vectorial_objects(self, wells: List[Keg5WellPropertyInputs]) -> None:
        """ Will remove the existing wells from this keg5 and create new ones taking
        into account the parameters of each input
        Parameters
        ----------
        wells: list of all needed properties to create new wells
        """
        common_field: Element = self.__find_element(self.__root, 'AA:CommonFields/AA:CommonField')
        common_wells_container: Element = self.__get_or_add_element(common_field, keg5_AA_alias, "CommonWells")
        self.__remove_children(common_wells_container, f"{keg5_AA_alias}:Well")
        vectorial_field: Element = self.__find_element(self.__root, 'AA:VectorialFields/AA:VectorialField')
        vectorial_wells_container: Element = self.__get_or_add_element(vectorial_field, keg5_AA_alias, "VectorialWells")
        self.__remove_children(vectorial_wells_container, f"{keg5_AA_alias}:VectorialWell")
        for well in wells:
            self.__add_well(common_wells_container, vectorial_wells_container, well) 
    def __print_nodes(self, node: Element, indent: str) -> None:
        print(f"{indent}{node.tag}")
        for sub_node in node:
            self.__print_nodes(sub_node, indent + "\t")
    def __add_well(self, common_wells_container: Element, vectorial_wells_container: Element, data: Keg5WellPropertyInputs) -> None:
        self.__add_common_well(common_wells_container, data)
        self.__add_vectorial_well(vectorial_wells_container, data)
    def __add_common_well(self, common_wells_container: Element, data: Keg5WellPropertyInputs) -> None:
        new_well = self.__add_sub_element(common_wells_container, keg5_AA_alias, "Well")
        self.__set_ab_type_and_value(new_well, f"{keg5_well_alias}:KEG5_CommonWell")
        self.__add_sub_element(new_well, keg5_wellcontrol_alias, "Controls")
        self.__add_sub_element(new_well, keg5_well_alias, "Name", data.name)
        self.__add_sub_element(new_well, keg5_well_alias, "WellID", data.well_id)
        well_general_info_element = self.__add_sub_element(new_well, keg5_well_alias, "WellGeneralInformation")
        self.__fill_well_general_info(well_general_info_element, data)
        common_perforations = self.__add_sub_element(new_well, keg5_well_alias, "CommonPerforations")
        self.__fill_common_perforations(common_perforations, data)
        intake = self.__add_sub_element(new_well, keg5_well_alias, "Intake")
        self.__add_sub_element(intake, keg5_wellintake_alias, "Name", f"{data.name} - intake")
        self.__add_sub_element(intake, keg5_wellintake_alias, "PressureDrops")
    def __fill_well_general_info(self, general_info_element: Element, data: Keg5WellPropertyInputs) -> None:
        self.__add_sub_element(general_info_element, keg5_well_alias, "UniqueWellIndentifier", data.uwi)
        self.__add_sub_element(general_info_element, keg5_well_alias, "WellType", data.get_well_type())
        self.__add_sub_element(general_info_element, keg5_well_alias, "BottomholeDepth", str(self.__default_z_top))
        self.__add_sub_element(general_info_element, keg5_well_alias, "DrillFloorElevation", "0")
        well_head_coord = self.__add_sub_element(general_info_element, keg5_well_alias, "WellHeadCoordinates")
        self.__add_sub_element(well_head_coord, keg5_well_alias, "X", str(data.x))
        self.__add_sub_element(well_head_coord, keg5_well_alias, "Y", str(data.y))
    def __fill_common_perforations(self, common_perforations: Element, data: Keg5WellPropertyInputs) -> None:
        perforation = self.__add_sub_element(common_perforations, keg5_well_alias, "CommonPerforation")
        self.__add_sub_element(perforation, keg5_wellgeometry_alias, "ID", data.perforation_id)
        skins = self.__add_sub_element(perforation, keg5_wellgeometry_alias, "PerforationSkins")
        default_skin = self.__add_sub_element(skins, keg5_wellgeometry_alias, "DefaultSkin")
        self.__add_sub_element(default_skin, keg5_wellgeometry_alias, "Skin", "0" if data.skin is None else str(data.skin))
        self.__add_sub_element(default_skin, keg5_wellgeometry_alias, "TurbulenceSkinFactor", "0")
    def __add_vectorial_well(self, vectorial_wells_container: Element, data: Keg5WellPropertyInputs) -> None:
        new_well = self.__add_sub_element(vectorial_wells_container, keg5_AA_alias, "VectorialWell")
        trajectory = self.__add_sub_element(new_well, keg5_well_alias, "VectorialTrajectory")
        self.__fill_well_trajectory(trajectory, data)
        perforations = self.__add_sub_element(new_well, keg5_well_alias, "VectorialPerforations")
        self.__fill_vectorial_perforations(perforations, data)
        self.__add_sub_element(new_well, keg5_well_alias, "VectorialWellProperties", data.well_id)
        self.__add_sub_element(new_well, keg5_well_alias, "HydraulicFractures")
    def __fill_well_trajectory(self, trajectory: Element, data: Keg5WellPropertyInputs) -> None:
        mainbore = self.__add_sub_element(trajectory, keg5_wellgeometry_alias, "VectorialMainbore")
        self.__add_sub_element(mainbore, keg5_wellgeometry_alias, "BoreholeID", data.borehole_id)
        self.__add_sub_element(mainbore, keg5_wellgeometry_alias, "Radius", str(data.radius))
        path = self.__add_sub_element(mainbore, keg5_wellgeometry_alias, "Path")
        for point in data.get_well_trajectory(self.__default_z_top):
            self.__add_point_to_geometry(path, point.x, point.y, point.z)
    def __fill_vectorial_perforations(self, perforations: Element, data: Keg5WellPropertyInputs) -> None:
        perforation = self.__add_sub_element(perforations, keg5_well_alias, "Perforation")
        self.__add_sub_element(perforation, keg5_wellgeometry_alias, "RefBoreholeID", data.borehole_id)
        self.__add_sub_element(perforation, keg5_wellgeometry_alias, "PerforationID", data.perforation_id)
        self.__add_sub_element(perforation, keg5_wellgeometry_alias, "MDStart", str(data.get_perforation_md_start(self.__default_z_top)))
        self.__add_sub_element(perforation, keg5_wellgeometry_alias, "MDEnd", str(data.get_perforation_md_end(self.__default_z_top)))
    def __add_point_to_geometry(self, path: Element, x: float, y: float, z: float) -> None:
        point = self.__add_sub_element(path, keg5_wellgeometry_alias, "Point")
        self.__add_sub_element(point, keg5_baseobjects_alias, "X", str(x))
        self.__add_sub_element(point, keg5_baseobjects_alias, "Y", str(y))
        self.__add_sub_element(point, keg5_baseobjects_alias, "Z", str(z))
    def __add_sub_sub_element(self, parent: Element, namespace: str, name: str, sub_namespace: str, sub_name: str, text: str) -> Element:
        element = self.__add_sub_element(parent, namespace, name)
        self.__add_sub_element(element, sub_namespace, sub_name, text)
        return element
    def __add_sub_element(self, parent: Element, namespace: str, name: str, text: str = "") -> Element:
        path = '{' + f'{self.__ns[namespace]}' + '}' + f'{name}'
        sub_element = SubElement(parent, path)
        if text is not None and text != "":
            sub_element.text = text
        return sub_element
    def __get_or_add_element(self, parent: Element, namespace: str, name: str) -> Element:
        path = '{' + f'{self.__ns[namespace]}' + '}' + f'{name}'
        child_element = parent.find(path, self.__ns)
        if child_element is None:
            return self.__add_sub_element(parent, namespace, name)
        return child_element
    def __replace_data_sets(self, well_data: List[Keg5WellPropertyInputs]) -> VectorialReservoir:
        x: List[float] = [well.x for well in well_data]
        y: List[float] = [well.y for well in well_data]
        data_sets: List[DataSet] = [
            DataSet("PermDataSet", x, y, [well.permeability for well in well_data]),
            DataSet("PorosityDataSet", x, y, [well.porosity for well in well_data]),
            DataSet("ThicknessDataSet", x, y, [well.thickness for well in well_data])
        ]
        vectorial_reservoir = self.__get_vectorial_reservoir()
        self.__remove_children(vectorial_reservoir, f"{keg5_reservoir_alias}:DataSets")
        for data_set in data_sets:
            self.__add_data_set(vectorial_reservoir, data_set)
        return VectorialReservoir(data_sets[0].id, data_sets[1].id, data_sets[2].id)
    def __add_data_set(self, vectorial_reservoir: Element, data_set_data: DataSet) -> None:
        data_set: Element = self.__add_sub_element(vectorial_reservoir, keg5_reservoir_alias, "DataSets")
        self.__add_sub_element(data_set, keg5_baseobjects_alias, "ID", data_set_data.id)
        self.__add_sub_element(data_set, keg5_baseobjects_alias, "Name", data_set_data.name)
        varying_values: Element = self.__add_sub_element(data_set, keg5_baseobjects_alias, "VaryingValues")
        self.__add_vector_double(varying_values, "X", data_set_data.x)
        self.__add_vector_double(varying_values, "Y", data_set_data.y)
        self.__add_vector_double(varying_values, "Value", data_set_data.values)
    def __configure_geometry(self, data_sets: VectorialReservoir) -> None:
        vectorial_reservoir = self.__get_vectorial_reservoir()
        vectorial_geometry = self.__find_element(vectorial_reservoir, f"{keg5_reservoir_alias}:VectorialGeometry")
        self.__configure_horizons(vectorial_geometry, data_sets)
        self.__configure_layers(vectorial_geometry, data_sets)
        self.__configure_property_zones(vectorial_geometry, data_sets)
        self.__configure_regions(vectorial_geometry)
        self.__configure_petrophysics(vectorial_reservoir, data_sets)
    def __configure_horizons(self, geometry: Element, data_sets: VectorialReservoir) -> None:
        horizons: Element = self.__find_element(geometry, f"{keg5_reservoirgeometry_alias}:Horizons")
        self.__remove_children(horizons, f"{keg5_reservoirgeometry_alias}:Horizon")
        top_horizon = self.__add_sub_element(horizons, keg5_reservoirgeometry_alias, "Horizon")
        self.__add_sub_element(top_horizon, keg5_reservoirgeometry_alias, "ID", data_sets.top_horizon_id)
        self.__add_sub_sub_element(top_horizon,
                                   keg5_reservoirgeometry_alias, "HorizonGeometry",
                                   keg5_baseobjects_alias,
                                   "ConstantValue",
                                   str(self.__default_z_top))
        bottom_horizon = self.__add_sub_element(horizons, keg5_reservoirgeometry_alias, "Horizon")
        self.__add_sub_element(bottom_horizon, keg5_reservoirgeometry_alias, "ID", data_sets.bottom_horizon_id)
        self.__add_sub_sub_element(bottom_horizon,
                                   keg5_reservoirgeometry_alias, "HorizonGeometry",
                                   keg5_baseobjects_alias,
                                   "DataSetID",
                                   data_sets.thickness_id)
    def __configure_layers(self, geometry: Element, data_sets: VectorialReservoir) -> None:
        layers: Element = self.__find_element(geometry, f"{keg5_reservoirgeometry_alias}:Layers")
        self.__remove_children(layers, f"{keg5_reservoirgeometry_alias}:Layer")
        layer = self.__add_sub_element(layers, keg5_reservoirgeometry_alias, "Layer")
        self.__add_sub_element(layer, keg5_reservoirgeometry_alias, "Name", self.__default_layer_name)
        self.__add_sub_element(layer, keg5_reservoirgeometry_alias, "ID", data_sets.layer_id)
        self.__add_sub_element(layer, keg5_reservoirgeometry_alias, "TopHorizon", data_sets.top_horizon_id)
        self.__add_sub_element(layer, keg5_reservoirgeometry_alias, "BottomHorizon", data_sets.bottom_horizon_id)
    def __configure_property_zones(self, geometry: Element, data_sets: VectorialReservoir) -> None:
        zones: Element = self.__find_element(geometry, f"{keg5_reservoirgeometry_alias}:PropertyZones")
        self.__remove_children(zones, f"{keg5_reservoirgeometry_alias}:PropertyZone")
        zone = self.__add_sub_element(zones, keg5_reservoirgeometry_alias, "PropertyZone")
        self.__set_ab_type_and_value(zone, f"{keg5_reservoirgeometry_alias}:KEG5_VectorialPropertyZone")
        self.__add_sub_element(zone, keg5_identifiers_alias, "ZoneID", data_sets.zone_id)
        self.__add_sub_element(zone, keg5_reservoirgeometry_alias, "Name", self.__default_name)
        zone_geometry = self.__add_sub_element(zone, keg5_reservoirgeometry_alias, "PropertyZoneGeometry")
        layer_region_geo = self.__add_sub_element(zone_geometry, keg5_reservoirgeometry_alias, "LayerAndRegionIDs")
        self.__add_sub_element(layer_region_geo, keg5_identifiers_alias, "LayerID", data_sets.layer_id)
        self.__add_sub_element(layer_region_geo, keg5_identifiers_alias, "RegionID", data_sets.region_id)
    def __configure_regions(self, geometry: Element) -> None:
        regions: Element = self.__find_element(geometry, f"{keg5_reservoirgeometry_alias}:Regions")
        self.__remove_children(regions, f"{keg5_reservoirgeometry_alias}:Region")
    def __configure_petrophysics(self, vectorial_reservoir: Element, data_sets: VectorialReservoir) -> None:
        vectorial_petro: Element = self.__find_element(vectorial_reservoir, f"{keg5_reservoir_alias}:VectorialPetrophysics")
        self.__remove_children(vectorial_petro, f"{keg5_reservoir_alias}:Petro")
        petro = self.__add_sub_element(vectorial_petro, keg5_reservoir_alias, "Petro")
        self.__set_ab_type_and_value(petro, f"{keg5_petrophysics_alias}:KEG5_VectorialPetrophysics")
        self.__add_sub_element(petro, keg5_identifiers_alias, "ZoneID", data_sets.zone_id)
        self.__add_sub_element(petro, keg5_petrophysics_alias, "Name")
        data_set_id_tag = "DataSetID"
        perm_map = self.__add_sub_element(petro, keg5_petrophysics_alias, "PermeabilityMap")
        self.__add_sub_sub_element(perm_map, keg5_petrophysics_alias, "PermeabilityXY", keg5_baseobjects_alias, data_set_id_tag, data_sets.perm_id)
        self.__add_sub_sub_element(perm_map, keg5_petrophysics_alias, "PermeabilityZ", keg5_baseobjects_alias, data_set_id_tag, data_sets.perm_id)
        self.__add_sub_sub_element(petro, keg5_petrophysics_alias, "PorosityMap", keg5_baseobjects_alias, data_set_id_tag, data_sets.porosity_id)
        self.__add_sub_sub_element(petro, keg5_petrophysics_alias, "NetToGrossMap", keg5_baseobjects_alias, "ConstantValue", "1")
        self.__add_sub_element(petro, keg5_petrophysics_alias, "Type", "REFERENCE")
    def __get_vectorial_reservoir(self) -> Element:
        return self.__find_element(self.__root, 'AA:VectorialFields/AA:VectorialField/AA:VectorialReservoir')
    def __add_vector_double(self, parent: Element, name: str, values: List[float]) -> None:
        self.__add_vector(parent, "Double", name, values)
    def __add_vector_int(self, parent: Element, name: str, values: List[int]) -> None:
        self.__add_vector(parent, "Integer", name, values)
    def __add_vector(self, parent: Element, vector_type: str, name: str, values: List[Any]) -> None:
        values_container: Element = self.__add_sub_element(parent, keg5_baseobjects_alias, name)
        self.__set_ab_type_and_value(values_container, f"{keg5_baseobjects_alias}:KEG5_Vector{vector_type}")
        direct_doubles_container = self.__add_sub_element(values_container, keg5_baseobjects_alias, f"Direct{vector_type}")
        value_type_tag = f"{vector_type}Value"
        for value in values:
            self.__add_sub_element(direct_doubles_container, keg5_baseobjects_alias, value_type_tag, str(value))
    def __remove_children(self, parent: Element, nodes: str) -> None:
        children = parent.findall(nodes, self.__ns)
        for child in children:
            parent.remove(child)
    def __find_element(self, parent: Element, child: str) -> Element:
        child_element = parent.find(child, self.__ns)
        if child_element is None:
            raise ParserException(f"Document does not contain {child}")
        return child_element
    @staticmethod
    def __set_ab_type_and_value(element: Element, value: str) -> None:
        element.set("xmlns:AB", "http://www.w3.org/2001/XMLSchema-instance")
        element.set("AB:type", value)