##!/usr/bin/python
# -*- coding: utf-8 -*-

# The MIT License (MIT)

# This code is part of the CityGML2OBJs package

# Copyright (c) 2023
# Thomas Fröch
# Technische Universität München (TUM)
# thomas.froech@tum.de

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import markup3dmodule as m3dm
from decimal import Decimal, getcontext
import numpy as np

# Setting the precision of Decimal
getcontext().prec = 28

def performStableAddition(number1, number2):
    # Input: string number1, string number 2, output: string sum
    # Determine the number of positions after the comma.
    # print("Number 1 totally before: ", number1)
    # print("Number 2 totally before: ", number2)

    try:
        n_after_comma_number1 = len(number1.split(".")[1])
    except:
        n_after_comma_number1 = 0
    try:
        n_after_comma_number2 = len(number2.split(".")[1])
    except:
        n_after_comma_number2 = 0
    lengths = [n_after_comma_number1, n_after_comma_number2]

    # convert float numbers into integers by removing the comma
    # print("Number 1 before all: ", number1)
    # print("Number 2 before all: ", number2)
    number1 = number1.replace(".", "")
    number2 = number2.replace(".", "")

    # print("Number 1 after: ", number1)
    # print("Number 2 after: ", number2)

    # find the number with more positions after the comma
    abs_lengths = np.abs(lengths)  # Erstellen eines Arrays mit den Beträgen
    max_index = np.argmax(abs_lengths)

    # Distinguish the two different cases
    # Case 1: number one has more positions after the comma than number two
    if max_index == 0:
        # Find the difference of positions after the comma
        n_positions_difference = (lengths[0] - lengths[1])
        # print("n_positions_difference: ", n_positions_difference)
        # Fill up the missing digits of number 2 with zeros
        # print("number2 before: ", number2)
        for i in range(n_positions_difference):
            number2 = number2 + "0"
        # print("number2_after", number2)
        # print("Number1: ", number1)
        # convert both stings into integers
        number1_int = np.double(number1)
        number2_int = np.double(number2)

        # Add both numbers
        number_sum = number1_int + number2_int

        # convert back into string
        format_string = "{:.0f}"
        number_sum_string = format_string.format(number_sum)

        # Insert the comma at the correct position
        length_of_number = len(number_sum_string.replace("-",
                                                         ""))
        if length_of_number >= lengths[max_index]:
            if n_after_comma_number1 != 0:
                number_sum_string = number_sum_string[:-(lengths[0])] + "." + number_sum_string[-(lengths[0]):]
                return number_sum_string
            else:
                number_sum_string = number_sum_string
                return number_sum_string
        elif length_of_number < n_after_comma_number1:
            new_string = ""
            for i in range(len(number_sum_string)):
                if number_sum_string[i] == "-":
                    new_string += "0."
                else:
                    new_string += number_sum_string[i]
            number_sum_string = new_string
            return number_sum_string
        else:
            number_sum_string = number_sum_string
            return number_sum_string

    # Case 2: number two has more positions after the comma than number one
    else:
        # find the difference of positions after the comma
        n_positions_difference = lengths[1] - lengths[0]

        # Fill up the missing digits of number 2 with zeros
        for i in range(n_positions_difference):
            number1 = number1 + "0"

        # convert both stings into integers
        number1_int = Decimal(number1)
        number2_int = Decimal(number2)

        # Add both numbers
        number_sum = number1_int + number2_int

        # convert back into string
        format_string = "{:.0f}"
        number_sum_string = format_string.format(number_sum)

        # Insert the comma at the correct position
        length_of_number = len(number_sum_string.replace("-",
                                                         ""))
        if length_of_number > n_after_comma_number2:
            if n_after_comma_number2 != 0:

                number_sum_string = number_sum_string[:-(lengths[1])] + "." + number_sum_string[-(lengths[1]):]
                return number_sum_string

            else:
                number_sum_string = number_sum_string
                return number_sum_string

        elif length_of_number < n_after_comma_number2:
            new_string = ""
            for i in range(len(number_sum_string)):
                if number_sum_string[i] == "-":
                    new_string += "0."
                else:
                    new_string += number_sum_string[i]
            number_sum_string = new_string
            return number_sum_string
        else:
            number_sum_string = number_sum_string
            return number_sum_string


# This function is used in order to extract all the envelopes from the CityGML-File
# These envelopes are going to be used in order to determine the translation parameters later
def getEnvelopes(root, ns_bldg, ns_gml, ns_citygml):
    envelopes = []
    for envelope in root.getiterator('{%s}Envelope' % ns_gml):
        envelopes.append(envelope)
    return envelopes


# This function is used in order to calculate the translation parameters from the
# envelopes that were previously extracted from the envelopes
def getTranslationParameters(envelopes, ns_gml):
    # Setting up some initial values
    dx = Decimal("0")
    dy = Decimal("0")
    lowerCorner = []
    upperCorner = []

    # Iterating through all the envelopes in the CityGML-File
    for envelope in envelopes:
        # print(" Envelope: ", envelope)
        # Finding the upper and the lower corner
        for child in envelope.getchildren():
            if child.tag == '{%s}lowerCorner' % ns_gml:
                lowerCorner.append(child.text)
            elif child.tag == '{%s}upperCorner' % ns_gml:
                upperCorner.append(child.text)
    # Converting into Decimal
    pointCounter = 0
    for point in lowerCorner:
        dy = dy + (Decimal(point.split(" ")[0]))
        dx = dx + (Decimal(point.split(" ")[1]))
        pointCounter = pointCounter + 1

    dyret = -dy / pointCounter
    dxret = -dx / pointCounter

    return [Decimal(str(int(dxret))), Decimal(str(int(dyret)))]


# This function is used to Parse the text from the CityGML file to Decimal numbers and
# to aplly the previously calculated translation parameters
# Notice: this code only allows ONE "gml:PosList-Element" for each Interior and exterior of a Polygon
# If there are more than just one,just the first one is going to be transformed
def splitAndApplyTrafo(coordString, transParam):
    # Splitting the coordinate string by empty spaces
    split = coordString.split(" ")
    alalala = len(split)
    # Apply the Trafo
    if split[0] == '':
        split.pop(0)
    if split[-1] == "":
        split.pop(-1)
    counter = 0
    length = int(len(split))
    length_new = int(length / 3)
    for i in range(length_new):


        # print("y")
        split[counter] = performStableAddition(split[counter], str(transParam[1]))
        # print("x")
        split[counter + 1] = performStableAddition(split[counter + 1], str(transParam[0]))
        # print("z")
        split[counter + 2] = performStableAddition(split[counter + 2], str(transParam[2]))

        counter += 3
    # converting back to a string
    translated = ""
    for i in split:
        if len(translated) == 0:
            translated = str(i)
        else:
            translated = translated + " " + str(i)
    return translated


# This code is used in order to find all the coordinates that are defined in the CityGML-File
# Please Notice: the search for coordinates here has the same limitations as the search for coordinates that
# is used in the "CityGML2OBJ" functionality!
def applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, filename):
    # Iterate over all the cityObjectMembers
    for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
        # Iterate over all the children of cityObject Member
        for child in obj.getchildren():
            # Exclude all the implicitly referenced objects from the transformation
            if child.findall(
                    './/{%s}ImplicitGeometry' % ns_citygml) == []:
                polys = m3dm.polygonFinder(child)
                # Iterate over all the polygons of the children of cityObjectMember
                for poly in polys:
                    # decompose all the polygons in the interior and exterior rings
                    exter, inter = m3dm.polydecomposer(poly)
                    # iterate over all the exterior rings
                    for e in exter:
                        # find all the coordinates that are stored as a "posList"
                        if len(e.findall('.//{%s}posList' % ns_gml)) > 0:
                            points_tmp = e.findall('.//{%s}posList' % ns_gml)[0].text
                            points = points_tmp.replace('\n', ' ')
                            translated = splitAndApplyTrafo(points, transParam)
                            # print("Before: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
                            e.findall('.//{%s}posList' % ns_gml)[0].text = translated
                            # print("Result: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
                        # find all the coordinates that are stored as "pos"
                        elif len(e.findall('.//{%s}pos' % ns_gml)) > 0:
                            points = e.findall('.//{%s}pos' % ns_gml)
                            counter = 0
                            for k in points:
                                translated = splitAndApplyTrafo(k.text, transParam)
                                e.findall('.//{%s}pos' % ns_gml)[counter].text = translated
                                counter = counter + 1
                    # iterate over all the interior rings
                    for i in inter:
                        # find all the coordinates that are stored as a "posList"
                        if len(i.findall('.//{%s}posList' % ns_gml)) > 0:
                            points_tmp = i.findall('.//{%s}posList' % ns_gml)[0].text
                            points = points_tmp.replace('\n', ' ')
                            translated = splitAndApplyTrafo(points, transParam)
                            i.findall('.//{%s}posList' % ns_gml)[0].text = translated
                        # find all the coordinates that are stored as "pos"
                        elif len(i.findall('.//{%s}pos' % ns_gml)) > 0:
                            points = i.findall('.//{%s}pos' % ns_gml)
                            counter = 0
                            for k in points:
                                translated = splitAndApplyTrafo(k.text, transParam)
                                i.findall('.//{%s}pos' % ns_gml)[counter].text = translated
                                counter = counter + 1

            else:  # This condition is used in order to transform the reference points of the implicitly defined geometries
                # Step 1: find all the reference points:
                referencePoints = child.findall('.//{%s}referencePoint' % ns_citygml)
                for referencePoint in referencePoints:
                    points_tmp = referencePoint.findall('.//{%s}pos' % ns_gml)
                    points = points_tmp.replace('\n', ' ')
                    counter = 0
                    for l in points:
                        translated = splitAndApplyTrafo(l.text, transParam)
                        referencePoint.findall('.//{%s}pos' % ns_gml)[counter].text = translated
                        counter = counter + 1

    # Iterate over all the envelopes
    for envelope in root.getiterator('{%s}Envelope' % ns_gml):
        lowerCorner = envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text
        upperCorner = envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text
        translatedLowerCorner = splitAndApplyTrafo(lowerCorner, transParam)
        translatedUpperCorner = splitAndApplyTrafo(upperCorner, transParam)
        envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text = translatedLowerCorner
        envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text = translatedUpperCorner

    CITYGML.write(filename + "_local_" + ".gml")
    return root


# This function is used in order to write the previously calculated translation parameters to a
# designated .txt file. The use of this functionality is optional and can be activated by setting the
# optional "write2file" parameter to "True" when calling the "translateToLocalCRS" - function.
def writeTransparam2File(filename, directory, transParam):
    textfileName = directory + filename + "_Translation_Parameters.txt"
    f = open(textfileName, "w")
    f.write("This file contains the translation parameters that were applied to the original CityGML file." + "\n" +
            "Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>" + "\n" +
            "Conversion tool extended by Thomas Fröch, TUM <thomas.froech@tum.de>" + "\n" +
            "see more at Github:" + "\n" +
            "https://github.com/tudelft3d/CityGML2OBJs" + "\n" + "\n")
    key = ['y', 'x', 'z']
    for i in range(len(transParam)):
        f.write(key[i] + ': ' + str(transParam[i]) + '  ')
    f.close()
    print("Translation parameters written to: " + textfileName)
    return 0

def translateToLocalCRS(CITYGML, file, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, directory, write2file=False,
                        applyHeight=Decimal("0")):
    envelopes = getEnvelopes(root, ns_bldg, ns_gml, ns_citygml)
    transParam = getTranslationParameters(envelopes, ns_gml)
    transParam.append(applyHeight)
    if write2file == True:
        writeTransparam2File(file, directory, transParam)
    applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, file)
    return 0