# -----------------------------------------------------------
# Licensed under the terms of GNU GPL 2
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# -----------------------------------------------------------
# Tim Hancock/Matthias Kuhn 2017
# Oslandia 2022

import math
import os.path
from cmath import phase, rect

from qgis.core import (
    Qgis,
    QgsExpressionContextUtils,
    QgsFeatureRequest,
    QgsGeometry,
    QgsGeometryUtils,
    QgsPoint,
    QgsPointXY,
    QgsProject,
    QgsRectangle,
    QgsWkbTypes,
)
from qgis.PyQt.QtCore import QObject
from qgis.utils import iface

from .core.tomsMessageLog import TOMsMessageLog
from .utils import getLookupDescription

class GenerateGeometryUtils(QObject):
    # https://gis.stackexchange.com/questions/95528/produce-line-from-components-length-and-angle?noredirect=1&lq=1
    # direction cosines function

    @staticmethod
    def cosdirAzim(azim):
        azRad = math.radians(azim)
        cosa = math.sin(azRad)
        cosb = math.cos(azRad)
        return cosa, cosb

    @staticmethod
    def cosdirAzimRad(azim):
        # az = math.radians(azim)
        cosa = math.sin(azim)
        cosb = math.cos(azim)
        return cosa, cosb

    @staticmethod
    def sameSign(x, y):
        # https://stackoverflow.com/questions/45950285/python-challenge-comparing-signs-of-2-numbers-without-using-or
        return abs(x) + abs(y) == abs(x + y)

    @staticmethod
    def changeSign(x):
        return -x

    @staticmethod
    def turnToCL(az1, az2):
        """
        function to determine direction of turn to road centre    *** needs to be checked carefully ***
        Az1 = Az of current line; Az2 = Az to roadCentreline
        """
        # TOMsMessageLog.logMessage("In turnToCL Az1 = " + str(Az1) + " Az2 = " + str(Az2), level=TOMsMessageLog.DEBUG)

        azCL = az1 - 90.0
        if azCL < 0:
            azCL += 360.0

        # Need to check quadrant

        if 0 <= azCL <= 90.0:
            if 270.0 <= az2 <= 359.999:
                azCL = azCL + 360
        elif 0 <= az2 <= 90:
            if 270.0 <= azCL <= 359.999:
                az2 = az2 + 360

        direction = abs(float(azCL) - float(az2))

        if direction < 90:
            turn = -90
        else:
            turn = 90

        return turn

    @staticmethod
    def calcBisector(prevAz, currAz, turn, widthRest):
        # function to return Az of bisector

        prevAzA = GenerateGeometryUtils.checkDegrees(prevAz + float(turn))
        currAzA = GenerateGeometryUtils.checkDegrees(currAz + float(turn))

        diffAz = prevAzA - currAzA

        diffAngle = diffAz / float(2)
        bisectAz = prevAzA - diffAngle

        diffAngleRad = math.radians(diffAngle)
        distToPt = float(widthRest) / math.cos(diffAngleRad)

        return bisectAz, distToPt

    @staticmethod
    def calcInteriorBisectAzimuth(az1, az2):
        # function to return Az of bisector

        diffAz = GenerateGeometryUtils.checkDegrees(az1) - GenerateGeometryUtils.checkDegrees(az2)

        diffAz2 = diffAz
        if diffAz > 180.0:
            diffAz2 = diffAz - 360.0
        if diffAz < -180.0:
            diffAz2 = diffAz + 360.0

        diffAngle = diffAz2 / float(2)
        bisectAz = GenerateGeometryUtils.checkDegrees(az1 - diffAngle)

        # TOMsMessageLog.logMessage("In generateDisplayGeometry: bisectAz: " + str(bisectAz), level=TOMsMessageLog.DEBUG)

        return bisectAz

    @staticmethod
    def checkDegrees(azim):
        """newAz = Az

        if Az >= float(360):
            newAz = Az - float(360)
        elif Az < float(0):
            newAz = Az + float(360)
        TOMsMessageLog.logMessage("In checkDegrees (1): newAz: " + str(newAz), level=TOMsMessageLog.DEBUG)"""

        newAz = math.degrees(QgsGeometryUtils().normalizedAngle(math.radians(azim)))

        # TOMsMessageLog.logMessage("In checkDegrees (2): newAz: " + str(newAz), level=TOMsMessageLog.DEBUG)

        return newAz

    @staticmethod
    def determineRoadName(feature):

        TOMsMessageLog.logMessage("In determineRoadName(helper):", level=TOMsMessageLog.DEBUG)

        roadCasementLayer = QgsProject.instance().mapLayersByName("RoadCasement")[0]

        # take the first point from the geometry
        TOMsMessageLog.logMessage(
            "In determineRoadName: {}".format(feature.geometry().asWkt()),
            level=TOMsMessageLog.DEBUG,
        )

        geom = feature.geometry()

        toleranceNearby = 10.0  # somehow need to have this (and layer names) as global variables

        TOMsMessageLog.logMessage(
            "In determineRoadName: GeometryID: {}".format(feature.attribute("GeometryID")),
            level=TOMsMessageLog.DEBUG,
        )
        if geom:
            if geom.type() == QgsWkbTypes.LineGeometry:
                TOMsMessageLog.logMessage(
                    "In determineRoadName(helper): considering line",
                    level=TOMsMessageLog.DEBUG,
                )
                line = GenerateGeometryUtils.getLineForAz(feature)

                if len(line) == 0:
                    return None, None

                # testPt = line[0]
                length = geom.length()
                testPtGeom = geom.interpolate(length / 2.0)
                testPt = testPtGeom.asPoint()
                TOMsMessageLog.logMessage(
                    "In determineRoadName: GeometryID: {}. Length: {}. {}".format(
                        feature.attribute("GeometryID"), length, length / 2.0
                    ),
                    level=TOMsMessageLog.DEBUG,
                )

            elif geom.type() == QgsWkbTypes.PointGeometry:  # Point
                TOMsMessageLog.logMessage(
                    "In determineRoadName(helper): considering point",
                    level=TOMsMessageLog.DEBUG,
                )
                testPt = feature.geometry().asPoint()

                # tolerance_nearby = 5.0

            elif feature.geometry().type() == QgsWkbTypes.PolygonGeometry:  # Polygon
                TOMsMessageLog.logMessage(
                    "In determineRoadName(helper): considering polygon",
                    level=TOMsMessageLog.DEBUG,
                )
                ptList = feature.geometry().asPolygon()[0]
                testPt = ptList[
                    0
                ]  # choose second point to (try to) move away from any "ends" (may be best to get midPoint ...)

        else:
            TOMsMessageLog.logMessage("In determineRoadName: unknown geometry type", level=Qgis.Warning)
            return None, None

        # nrPts = len(ptList)
        # TOMsMessageLog.logMessage("In determineRoadName: number of pts in list: " + str(nrPts), level=TOMsMessageLog.DEBUG)

        TOMsMessageLog.logMessage(
            "In determineRoadName: GeometryID: {}; testPt: {}:{}".format(
                feature.attribute("GeometryID"), testPt.x(), testPt.y()
            ),
            level=TOMsMessageLog.DEBUG,
        )

        # check for the feature within RoadCasement_NSG_RoadName layer
        # tolerance_nearby = 1.0  # somehow need to have this (and layer names) as global variables

        # nearestRC_feature = generateGeometryUtils.findFeatureAt2(feature, testPt, RoadCasementLayer,
        #                                                        tolerance_nearby)

        (
            _,
            nearestRCFeature,
        ) = GenerateGeometryUtils.findNearestPointOnLineLayer(testPt, roadCasementLayer, toleranceNearby)

        if nearestRCFeature:

            idxRoadName = roadCasementLayer.fields().indexFromName("RoadName")
            idxUSRN = roadCasementLayer.fields().indexFromName("USRN")

            roadName = nearestRCFeature.attributes()[idxRoadName]
            usrn = nearestRCFeature.attributes()[idxUSRN]

            TOMsMessageLog.logMessage("In determineRoadName: RoadName: " + str(roadName), level=Qgis.Info)

            return roadName, usrn

        return None, None

    @staticmethod
    def setAzimuthToRoadCentreLine(feature):

        # now set the attribute
        feature.setAttribute(
            "AzimuthToRoadCentreLine",
            int(GenerateGeometryUtils.calculateAzimuthToRoadCentreLine(feature)),
        )

    @staticmethod
    def getLineForAz(feature):
        # gets a single "line" (array of points) from the current feature. Checks for:
        # - existence of geoemtry
        # - whether or not it is multi-part

        geom = feature.geometry()
        # line = QgsGeometry()

        if geom:
            if geom.type() == QgsWkbTypes.LineGeometry:
                if geom.isMultipart():
                    lines = geom.asMultiPolyline()
                    nrLines = len(lines)

                    # take the first line as the one we are interested in
                    if nrLines > 0:
                        line = lines[0]
                    else:
                        return 0

                else:
                    line = feature.geometry().asPolyline()

                # Now return the array
                return line

            TOMsMessageLog.logMessage("In getLineForAz(helper): Incorrect geometry found", level=Qgis.Warning)
            return None

        TOMsMessageLog.logMessage("In getLineForAz(helper): geometry not found", level=Qgis.Warning)
        return None

    @staticmethod
    def calculateAzimuthToRoadCentreLine(feature):
        # find the shortest line from this point to the road centre line layer
        # http://www.lutraconsulting.co.uk/blog/2014/10/17/getting-started-writing-qgis-python-plugins/
        # generates "closest feature" function

        TOMsMessageLog.logMessage("In calculateAzimuthToRoadCentreLine(helper):", level=TOMsMessageLog.DEBUG)

        roadCentreLineLayer = QgsProject.instance().mapLayersByName("RoadCentreLine")[0]

        # take the a point from the geometry
        # line = feature.geometry().asPolyline()
        line = GenerateGeometryUtils.getLineForAz(feature)

        if len(line) == 0:
            return 0

        # Get the mid point of the line
        # https://gis.stackexchange.com/questions/58079/finding-middle-point-midpoint-of-line-in-qgis

        testPt = feature.geometry().centroid().asPoint()

        # Find all Road Centre Line features within a "reasonable" distance
        # and then check each one to find the shortest distance

        toleranceRoadwidth = 25
        searchRect = QgsRectangle(
            testPt.x() - toleranceRoadwidth,
            testPt.y() - toleranceRoadwidth,
            testPt.x() + toleranceRoadwidth,
            testPt.y() + toleranceRoadwidth,
        )

        request = QgsFeatureRequest()
        request.setFilterRect(searchRect)
        request.setFlags(QgsFeatureRequest.ExactIntersect)

        shortestDistance = float("inf")
        featureFound = False

        # Loop through all features in the layer to find the closest feature
        for feat in roadCentreLineLayer.getFeatures(request):
            dist = feat.geometry().distance(QgsGeometry.fromPointXY(testPt))
            if dist < shortestDistance:
                shortestDistance = dist
                closestFeature = feat
                featureFound = True

        TOMsMessageLog.logMessage(
            "In calculateAzimuthToRoadCentreLine: shortestDistance: " + str(shortestDistance),
            level=TOMsMessageLog.DEBUG,
        )

        if featureFound:
            # now obtain the line between the testPt and the nearest feature
            startPt = QgsPoint(
                QgsGeometry.asPoint(closestFeature.geometry().nearestPoint(QgsGeometry.fromPointXY(testPt)))
            )

            TOMsMessageLog.logMessage(
                "In calculateAzimuthToRoadCentreLine: startPoint: " + str(startPt.x()),
                level=TOMsMessageLog.DEBUG,
            )

            azim = GenerateGeometryUtils.checkDegrees(QgsPoint(testPt).azimuth(startPt))

            return azim

        return 0

    @staticmethod
    def getReverseAzimuth(azim):
        return azim % 360

    @staticmethod
    def getDisplayGeometry(feature, restGeomType, offset, shpExtent, orientation, azimuthToCentreLine):
        # Obtain relevant variables

        # Need to check why the project variable function is not working

        restrictionID = feature.attribute("GeometryID")
        TOMsMessageLog.logMessage(
            "In getDisplayGeometry: New restriction ............................"
            "........................................ ID: " + str(restrictionID),
            level=Qgis.Info,
        )

        #  Within expression areas use:
        #  generateDisplayGeometry(   "RestrictionTypeID" , "GeomTypeID",
        #                             "AzimuthToRoadCentreLine",  @BayOffsetFromKerb ,  @BayWidth )
        #
        #  Logic is :
        #
        #  For vertex 0,
        #      Move for defined distance (typically 0.25m) in direction "AzimuthToCentreLine" and create point 1
        #      Move for bay width (typically 2.0m) in direction "AzimuthToCentreLine and create point 2
        #      Calculate Azimuth for line between vertex 0 and vertex 1
        #      Calc difference in Azimuths and decide < or > 180 (to indicate which side of kerb line to generate bay)
        #
        #  For each vertex (starting at 1)
        #      Calculate Azimuth for current vertex and previous
        #      Calculate perpendicular to centre of road (using knowledge of which side of kerb line generated above)
        #      Move for bay width (typically 2.0m) along perpendicular and create point
        #
        #  For last vertex
        #          Move for defined distance (typically 0.25m) along perpendicular and create last point

        line = GenerateGeometryUtils.getLineForAz(feature)

        if len(line) == 0:
            return 0

        # Now have a valid set of points

        ptsList = []
        parallelPtsList = []
        prevAz = 0
        diffEchelonAz = 0

        # now loop through each of the vertices and process as required. New geometry points are added to ptsList

        for i in range(len(line) - 1):

            azim = line[i].azimuth(line[i + 1])

            # if this is the first point

            if i == 0:
                # determine which way to turn towards CL

                turn = GenerateGeometryUtils.turnToCL(azim, azimuthToCentreLine)

                newAz = azim + turn
                cosa, cosb = GenerateGeometryUtils.cosdirAzim(newAz)

                ptsList.append(
                    QgsPoint(
                        line[i].x() + (float(offset) * cosa),
                        line[i].y() + (float(offset) * cosb),
                    )
                )

                # Now add the point at the extent. If it is an echelon bay:
                #   a. calculate the difference between the first Az and the echelon Az (??), and
                #   b. adjust the angle
                #   c. *** also need to adjust the length *** Not yet implemented

                # if restGeomType == 5 or restGeomType == 25:  # echelon
                if restGeomType in [5, 25]:  # echelon
                    TOMsMessageLog.logMessage(
                        "In getDisplayGeometry: orientation: " + str(orientation),
                        level=TOMsMessageLog.DEBUG,
                    )
                    diffEchelonAz = GenerateGeometryUtils.checkDegrees(orientation - newAz)
                    newAz = azim + turn + diffEchelonAz
                    TOMsMessageLog.logMessage(
                        "In getDisplayGeometry: newAz: " + str(newAz) + " diffEchelonAz: " + str(diffEchelonAz),
                        level=TOMsMessageLog.DEBUG,
                    )
                    cosa, cosb = GenerateGeometryUtils.cosdirAzim(newAz)

                ptsList.append(
                    QgsPoint(
                        line[i].x() + (float(shpExtent) * cosa),
                        line[i].y() + (float(shpExtent) * cosb),
                    )
                )

            else:

                # now pass along the feature

                # need to work out half of bisected angle

                newAz, distWidth = GenerateGeometryUtils.calcBisector(prevAz, azim, turn, shpExtent)

                cosa, cosb = GenerateGeometryUtils.cosdirAzim(newAz + diffEchelonAz)
                ptsList.append(
                    QgsPoint(
                        line[i].x() + (float(distWidth) * cosa),
                        line[i].y() + (float(distWidth) * cosb),
                    )
                )

                parallelPtsList.append(
                    QgsPoint(
                        line[i].x() + (float(distWidth) * cosa),
                        line[i].y() + (float(distWidth) * cosb),
                    )
                )

            prevAz = azim

        # have reached the end of the feature. Now need to deal with last point.
        # Use Azimuth from last segment but change the points

        # standard bay
        newAz = azim + turn + diffEchelonAz
        # TOMsMessageLog.logMessage("In generateDisplayGeometry: newAz: " + str(newAz), level=TOMsMessageLog.DEBUG)
        cosa, cosb = GenerateGeometryUtils.cosdirAzim(newAz)

        ptsList.append(
            QgsPoint(
                line[len(line) - 1].x() + (float(shpExtent) * cosa),
                line[len(line) - 1].y() + (float(shpExtent) * cosb),
            )
        )

        # add end point (without any consideration of Echelon)

        newAz = azim + turn
        cosa, cosb = GenerateGeometryUtils.cosdirAzim(newAz)

        ptsList.append(
            QgsPoint(
                line[len(line) - 1].x() + (float(offset) * cosa),
                line[len(line) - 1].y() + (float(offset) * cosb),
            )
        )

        newLine = QgsGeometry.fromPolyline(ptsList)

        return newLine, parallelPtsList

    @staticmethod
    def meanAngle(angle1, angle2):
        return phase((rect(1, angle1) + rect(1, angle2)) / 2.0)

    @staticmethod
    def generateMultiLabelLeaders(feature):
        """This generates leaders for labels as multipoints"""

        minScale = float(GenerateGeometryUtils.getMininumScaleForDisplay())
        currScale = float(iface.mapCanvas().scale())

        if currScale <= minScale:

            # we're accessing the labels layer, meaning that feature.geometry() is the label's position (multipoint)
            labelGeometry = feature.geometry().asMultiPoint()

            # we need to get the main geometry too
            # unfortunately, the attribute returns an eWKT instead of a QgsGeometry (see post on qgis-dev 22/04/2020)
            mainEwkt = feature.attribute("geom")
            mainWkt = mainEwkt[mainEwkt.index(";") + 1 :]
            mainGeom = QgsGeometry.fromWkt(mainWkt)

            # we build a collection for the leaders
            leaders = []
            for labelPos in labelGeometry:
                nearestPoint = mainGeom.nearestPoint(QgsGeometry.fromPointXY(labelPos)).asPoint()
                leaders.append([nearestPoint, labelPos])

            return QgsGeometry.fromMultiPolylineXY(leaders)

        return None

    @staticmethod
    def generateBayLabelLeader(feature):

        # TOMsMessageLog.logMessage("In generateBayLabelLeader", level=TOMsMessageLog.DEBUG)
        # check to see scale

        minScale = float(GenerateGeometryUtils.getMininumScaleForDisplay())
        currScale = float(iface.mapCanvas().scale())

        TOMsMessageLog.logMessage(
            "In generateBayLabelLeader. Current scale: " + str(currScale) + " min scale: " + str(minScale),
            level=TOMsMessageLog.DEBUG,
        )

        if currScale <= minScale:

            if feature.attribute("label_X"):

                length = feature.geometry().length()
                TOMsMessageLog.logMessage(
                    "In generateBayLabelLeader. label_X set for " + str(feature.attribute("GeometryID")),
                    level=TOMsMessageLog.DEBUG,
                )

                return QgsGeometry.fromPolyline(
                    [
                        QgsPoint(feature.geometry().interpolate(length / 2.0).asPoint()),
                        QgsPoint(feature.attribute("label_X"), feature.attribute("label_Y")),
                    ]
                )

        return None

    @staticmethod
    def generatePolygonLabelLeader(feature):

        # TOMsMessageLog.logMessage("In generateBayLabelLeader", level=TOMsMessageLog.DEBUG)
        # check to see scale

        minScale = float(GenerateGeometryUtils.getMininumScaleForDisplay())
        currScale = float(iface.mapCanvas().scale())

        if currScale <= minScale:

            if feature.attribute("label_X"):
                TOMsMessageLog.logMessage(
                    "In generatePolygonLabelLeader. label_X set for " + str(feature.attribute("GeometryID")),
                    level=TOMsMessageLog.DEBUG,
                )

                # now generate line
                return QgsGeometry.fromPolyline(
                    [
                        QgsPoint(feature.geometry().nearestPoint().asPoint()),
                        QgsPoint(feature.attribute("label_X"), feature.attribute("label_Y")),
                    ]
                )

        return None

    @staticmethod
    def getMininumScaleForDisplay():

        minScale = QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("MinimumTextDisplayScale")

        TOMsMessageLog.logMessage(
            "In getMininumScaleForDisplay. minScale(1): " + str(minScale),
            level=TOMsMessageLog.DEBUG,
        )

        if minScale is None:
            minScale = 1250

        return minScale

    @staticmethod
    def getWaitingLoadingRestrictionLabelText(feature):

        TOMsMessageLog.logMessage("In getWaitingLoadingRestrictionLabelText", level=TOMsMessageLog.DEBUG)

        minScale = float(GenerateGeometryUtils.getMininumScaleForDisplay())
        currScale = float(iface.mapCanvas().scale())

        if currScale > minScale:
            return None, None

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText(1): get details ...",
            level=TOMsMessageLog.DEBUG,
        )

        try:
            waitingTimeID = feature.attribute("NoWaitingTimeID")
            loadingTimeID = feature.attribute("NoLoadingTimeID")
            matchDayTimePeriodID = feature.attribute("MatchDayTimePeriodID")
            additionalConditionID = feature.attribute("AdditionalConditionID")
            geometryID = feature.attribute("GeometryID")
        except Exception:
            return None, None

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText(1): details found ... [{}]".format(geometryID),
            level=TOMsMessageLog.DEBUG,
        )

        timePeriodsLayer = QgsProject.instance().mapLayersByName("TimePeriodsInUse_View")[0]

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText(1): getting lookup values ... [{}]".format(geometryID),
            level=TOMsMessageLog.DEBUG,
        )

        waitDesc = GenerateGeometryUtils.getLookupLabelText(timePeriodsLayer, waitingTimeID)
        loadDesc = GenerateGeometryUtils.getLookupLabelText(timePeriodsLayer, loadingTimeID)

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText(1): waiting: " + str(waitDesc) + " loading: " + str(loadDesc),
            level=TOMsMessageLog.DEBUG,
        )

        restrictionCPZ = feature.attribute("CPZ")
        restrictionEDZ = feature.attribute("MatchDayEventDayZone")
        cpzWaitingTimeID = GenerateGeometryUtils.getCPZWaitingTimeID(restrictionCPZ)

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText ({}): wait_cpz: {}; wait_res: {}; load: {}; ed: {}".format(
                geometryID,
                cpzWaitingTimeID,
                waitingTimeID,
                loadingTimeID,
                matchDayTimePeriodID,
            ),
            level=TOMsMessageLog.DEBUG,
        )

        if cpzWaitingTimeID:
            if cpzWaitingTimeID == waitingTimeID:
                waitDesc = None

        if matchDayTimePeriodID:
            cpzMatchDayTimePeriodID = GenerateGeometryUtils.getEDWaitingTimeID(restrictionEDZ)
            TOMsMessageLog.logMessage(
                "In getWaitingLoadingRestrictionLabelText: ED: {}; restriction: {}".format(
                    cpzMatchDayTimePeriodID, matchDayTimePeriodID
                ),
                level=TOMsMessageLog.DEBUG,
            )
            if cpzMatchDayTimePeriodID != matchDayTimePeriodID:
                matchDayTimePeriodDesc = GenerateGeometryUtils.getLookupLabelText(
                    timePeriodsLayer, matchDayTimePeriodID
                )
                if waitDesc:
                    waitDesc = "{};Match Day: {}".format(waitDesc, matchDayTimePeriodDesc)
                else:
                    waitDesc = "Match Day: {}".format(matchDayTimePeriodDesc)

        if additionalConditionID:
            additionalConditionTypesLayer = QgsProject.instance().mapLayersByName("AdditionalConditionTypes")[0]
            additionalConditionDesc = getLookupDescription(additionalConditionTypesLayer, additionalConditionID)
            if waitDesc:
                waitDesc = "{};{}".format(waitDesc, additionalConditionDesc)
            else:
                waitDesc = "{}".format(additionalConditionDesc)

        TOMsMessageLog.logMessage(
            "In getWaitingLoadingRestrictionLabelText({}); waiting: {}; loading: {}".format(
                geometryID, waitDesc, loadDesc
            ),
            level=TOMsMessageLog.DEBUG,
        )
        return waitDesc, loadDesc

    @staticmethod
    def getBayRestrictionLabelText(feature):

        TOMsMessageLog.logMessage("In getBayRestrictionLabelText ..", level=TOMsMessageLog.DEBUG)

        minScale = float(GenerateGeometryUtils.getMininumScaleForDisplay())
        currScale = float(iface.mapCanvas().scale())

        if currScale > minScale:
            return None, None, None

        maxStayID = feature.attribute("MaxStayID")
        noReturnID = feature.attribute("NoReturnID")
        timePeriodID = feature.attribute("TimePeriodID")
        matchDayTimePeriodID = feature.attribute("MatchDayTimePeriodID")
        additionalConditionID = feature.attribute("AdditionalConditionID")
        permitCode = feature.attribute("PermitCode")

        lengthOfTimeLayer = QgsProject.instance().mapLayersByName("LengthOfTime")[0]
        timePeriodsLayer = QgsProject.instance().mapLayersByName("TimePeriodsInUse_View")[0]

        if feature.attribute("GeometryID"):
            TOMsMessageLog.logMessage(
                "In getBayRestrictionLabelText: GeometryID: " + feature.attribute("GeometryID"),
                level=TOMsMessageLog.DEBUG,
            )

        maxStayDesc = GenerateGeometryUtils.getLookupLabelText(lengthOfTimeLayer, maxStayID)
        noReturnDesc = GenerateGeometryUtils.getLookupLabelText(lengthOfTimeLayer, noReturnID)
        timePeriodDesc = GenerateGeometryUtils.getLookupLabelText(timePeriodsLayer, timePeriodID)

        TOMsMessageLog.logMessage("In getBayRestrictionLabelText (2) ..", level=TOMsMessageLog.DEBUG)

        restrictionCPZ = feature.attribute("CPZ")
        restrictionEDZ = feature.attribute("MatchDayEventDayZone")
        restrictionPTA = feature.attribute("ParkingTariffArea")

        cpzWaitingTimeID = GenerateGeometryUtils.getCPZWaitingTimeID(restrictionCPZ)
        (
            tariffZoneTimePeriodID,
            tariffZoneMaxStayID,
            tariffZoneNoReturnID,
        ) = GenerateGeometryUtils.getTariffZoneDetails(restrictionPTA)

        TOMsMessageLog.logMessage(
            "In getBayRestrictionLabelText (1): "
            + str(cpzWaitingTimeID)
            + " PTA hours: "
            + str(tariffZoneTimePeriodID),
            level=TOMsMessageLog.DEBUG,
        )
        TOMsMessageLog.logMessage(
            "In getBayRestrictionLabelText. bay hours: " + str(timePeriodID),
            level=TOMsMessageLog.DEBUG,
        )

        if timePeriodID == 1:  # 'At Any Time'
            timePeriodDesc = None

        if cpzWaitingTimeID:
            TOMsMessageLog.logMessage(
                "In getBayRestrictionLabelText: " + str(cpzWaitingTimeID) + " " + str(timePeriodID),
                level=TOMsMessageLog.DEBUG,
            )
            if cpzWaitingTimeID == timePeriodID:
                timePeriodDesc = None

        if tariffZoneTimePeriodID:
            if tariffZoneTimePeriodID == timePeriodID:
                timePeriodDesc = None

            if tariffZoneMaxStayID:
                if tariffZoneMaxStayID == maxStayID:
                    maxStayDesc = None

            if tariffZoneNoReturnID:
                if tariffZoneNoReturnID == noReturnID:
                    noReturnDesc = None

        if matchDayTimePeriodID:
            cpzMatchDayTimePeriodID = GenerateGeometryUtils.getEDWaitingTimeID(restrictionEDZ)
            TOMsMessageLog.logMessage(
                "In getBayRestrictionLabelText: ED: {}; restriction: {}".format(
                    cpzMatchDayTimePeriodID, matchDayTimePeriodID
                ),
                level=TOMsMessageLog.DEBUG,
            )
            if cpzMatchDayTimePeriodID != matchDayTimePeriodID:
                matchDayTimePeriodDesc = GenerateGeometryUtils.getLookupLabelText(
                    timePeriodsLayer, matchDayTimePeriodID
                )
                if timePeriodDesc:
                    timePeriodDesc = "{};Match Day: {}".format(timePeriodDesc, matchDayTimePeriodDesc)
                else:
                    timePeriodDesc = "Match Day: {}".format(matchDayTimePeriodDesc)

        if permitCode:
            if timePeriodDesc:
                timePeriodDesc = "{};Permit: {}".format(timePeriodDesc, permitCode)
            else:
                timePeriodDesc = "Permit: {}".format(permitCode)

        if additionalConditionID:
            additionalConditionTypesLayer = QgsProject.instance().mapLayersByName("AdditionalConditionTypes")[0]
            additionalConditionDesc = getLookupDescription(additionalConditionTypesLayer, additionalConditionID)
            if timePeriodDesc:
                timePeriodDesc = "{};{}".format(timePeriodDesc, additionalConditionDesc)
            else:
                timePeriodDesc = "{}".format(additionalConditionDesc)

        TOMsMessageLog.logMessage(
            "In getBayRestrictionLabelText. timePeriodDesc (2): " + str(timePeriodDesc),
            level=TOMsMessageLog.DEBUG,
        )

        return maxStayDesc, noReturnDesc, timePeriodDesc

    @staticmethod
    def getLookupLabelText(lookupLayer, code):

        # TOMsMessageLog.logMessage("In getLookupLabelText", level=TOMsMessageLog.DEBUG)

        if code:
            query = '"Code" = ' + str(code)
            request = QgsFeatureRequest().setFilterExpression(query)

            try:
                row = next(lookupLayer.getFeatures(request))
                return row["LabelText"]  # make assumption that only one row
            except Exception as e:
                TOMsMessageLog.logMessage(
                    "getLookupLabelText: error in expression function: {}".format(e),
                    level=Qgis.Warning,
                )

        # TOMsMessageLog.logMessage("In getLookupLabelText. returning without value ...", level=TOMsMessageLog.DEBUG)

        return None

    @staticmethod
    def getCurrentCPZDetails(feature):

        TOMsMessageLog.logMessage("In getCurrentCPZDetails", level=TOMsMessageLog.DEBUG)
        try:
            cpzLayer = QgsProject.instance().mapLayersByName("CPZs")[0]
        except Exception:
            cpzLayer = None

        if cpzLayer:
            restrictionID = feature.attribute("GeometryID")
            TOMsMessageLog.logMessage(
                "In getCurrentCPZDetails. restriction: " + str(restrictionID),
                level=TOMsMessageLog.DEBUG,
            )

            # geom = feature.geometry()

            currentCPZFeature = GenerateGeometryUtils.getPolygonForRestriction(feature, cpzLayer)

            if currentCPZFeature:

                currentCPZ = currentCPZFeature.attribute("CPZ")
                cpzWaitingTimeID = currentCPZFeature.attribute("TimePeriodID")
                # cpzMatchDayTimePeriodID = currentCPZFeature.attribute("MatchDayTimePeriodID")
                TOMsMessageLog.logMessage(
                    "In getCurrentCPZDetails. CPZ found: {}: control: {}".format(currentCPZ, cpzWaitingTimeID),
                    level=TOMsMessageLog.DEBUG,
                )

                return currentCPZ, cpzWaitingTimeID

        return None, None

    @staticmethod
    def getCurrentEventDayDetails(feature):

        TOMsMessageLog.logMessage("In getCurrentEventDayDetails", level=TOMsMessageLog.DEBUG)
        try:
            edLayer = QgsProject.instance().mapLayersByName("MatchDayEventDayZones")[0]
        except Exception:
            edLayer = None

        if edLayer:
            restrictionID = feature.attribute("GeometryID")
            TOMsMessageLog.logMessage(
                "In getCurrentEventDayDetails. restriction: " + str(restrictionID),
                level=TOMsMessageLog.DEBUG,
            )

            # geom = feature.geometry()

            currentEDZFeature = GenerateGeometryUtils.getPolygonForRestriction(feature, edLayer)

            if currentEDZFeature:

                currentEDZ = currentEDZFeature.attribute("EDZ")
                edzWaitingTimeID = currentEDZFeature.attribute("TimePeriodID")
                # cpzMatchDayTimePeriodID = currentCPZFeature.attribute("MatchDayTimePeriodID")
                TOMsMessageLog.logMessage(
                    "In getCurrentEventDayDetails. CPZ found: {}: control: {}".format(currentEDZ, edzWaitingTimeID),
                    level=TOMsMessageLog.DEBUG,
                )

                return currentEDZ, edzWaitingTimeID

        return None, None

    @staticmethod
    def getCurrentPTADetails(feature):

        TOMsMessageLog.logMessage("In getCurrentPTADetails", level=TOMsMessageLog.DEBUG)
        try:
            ptaLayer = QgsProject.instance().mapLayersByName("ParkingTariffAreas")[0]
        except Exception:
            ptaLayer = None

        if ptaLayer:
            restrictionID = feature.attribute("GeometryID")
            TOMsMessageLog.logMessage(
                "In getCurrentPTADetails. restriction: " + str(restrictionID),
                level=TOMsMessageLog.DEBUG,
            )

            # geom = feature.geometry()

            currentPTAFeature = GenerateGeometryUtils.getPolygonForRestriction(feature, ptaLayer)

            if currentPTAFeature:
                currentPTA = currentPTAFeature.attribute("ParkingTariffArea")
                ptaMaxStayID = currentPTAFeature.attribute("MaxStayID")
                ptaNoReturnID = currentPTAFeature.attribute("NoReturnID")

                # TOMsMessageLog.logMessage("In getCurrentPTADetails. PTA found: " + str(currentPTA), level=TOMsMessageLog.DEBUG)

                return currentPTA, ptaMaxStayID, ptaNoReturnID

        return None, None, None

    @staticmethod
    def getPolygonForRestriction(restriction, layer):
        # def findFeatureAt(self, pos, excludeFeature=None):
        """Find the feature close to the given position.
        #def findFeatureAt2(feature, layerPt, layer, tolerance):
            'layerPt' is the position to check, in layer coordinates.
            'layer' is specified layer
            'tolerance' is search distance in layer units

            If no feature is close to the given coordinate, we return None.
        """

        # TOMsMessageLog.logMessage("In getPolygonForRestriction.", level=TOMsMessageLog.DEBUG)

        line = GenerateGeometryUtils.getLineForAz(restriction)

        if line:
            if len(line) == 0:
                return 0

            testPt = line[
                0
            ]  # choose second point to (try to) move away from any "ends" (may be best to get midPoint ...)
            # TOMsMessageLog.logMessage("In getPolygonForRestriction." + str(testPt.x()), level=TOMsMessageLog.DEBUG)

            for poly in layer.getFeatures():
                if poly.geometry().contains(QgsGeometry.fromPointXY(testPt)):
                    # TOMsMessageLog.logMessage("In getPolygonForRestriction. feature found", level=TOMsMessageLog.DEBUG)
                    return poly

        return None

    @staticmethod
    def getAttributeFromLayer(layer, keyField, keyValue, attributeName):

        query = "\"{}\" = '{}'".format(keyField, keyValue)
        request = QgsFeatureRequest().setFilterExpression(query)

        try:
            for row in layer.getFeatures(request):
                TOMsMessageLog.logMessage(
                    "In getAttributeFromLayer. Returning {} with value {} from table {}".format(
                        attributeName,
                        row[layer.fields().indexFromName(attributeName)],
                        layer.name(),
                    ),
                    level=TOMsMessageLog.DEBUG,
                )
                return row[layer.fields().indexFromName(attributeName)]  # make assumption that only one row
        except Exception:
            pass

        return None

    @staticmethod
    def getCPZWaitingTimeID(cpzNr):

        TOMsMessageLog.logMessage(
            "In getCPZWaitingTimeID. Looking for {}.".format(cpzNr),
            level=TOMsMessageLog.DEBUG,
        )

        try:
            cpzLayer = QgsProject.instance().mapLayersByName("CPZs")[0]
        except Exception:
            return None

        if cpzNr:

            query = "\"CPZ\" = '{}'".format(cpzNr)

            TOMsMessageLog.logMessage(
                "In getCPZWaitingTimeID. table: {}; query: {}".format(cpzLayer.name(), str(query)),
                level=TOMsMessageLog.DEBUG,
            )

            try:
                row = next(cpzLayer.getFeatures(query))
            except Exception:
                return None

            TOMsMessageLog.logMessage("In getCPZWaitingTimeID. Found CPZ.", level=TOMsMessageLog.DEBUG)

            return row["TimePeriodID"]

        return None

    @staticmethod
    def getEDWaitingTimeID(edzNr):

        TOMsMessageLog.logMessage("In getEDWaitingTimeID", level=TOMsMessageLog.DEBUG)

        try:
            edzLayer = QgsProject.instance().mapLayersByName("MatchDayEventDayZones")[0]
        except Exception:
            edzLayer = None

        if edzLayer:
            for poly in edzLayer.getFeatures():
                currentEDZ = poly.attribute("EDZ")

                if currentEDZ == edzNr:
                    TOMsMessageLog.logMessage("In getEDWaitingTimeID. Found EDZ.", level=TOMsMessageLog.DEBUG)
                    edzWaitingTimeID = poly.attribute("TimePeriodID")
                    # cpzMatchDayTimeID = poly.attribute("MatchDayTimePeriodID")
                    # return cpzWaitingTimeID, cpzMatchDayTimeID
                    TOMsMessageLog.logMessage(
                        "In getEDWaitingTimeID. ID. {}".format(edzWaitingTimeID),
                        level=TOMsMessageLog.DEBUG,
                    )
                    return edzWaitingTimeID

        return None

    @staticmethod
    def getTariffZoneDetails(tpaNr):

        TOMsMessageLog.logMessage("In getTariffZoneDetails", level=TOMsMessageLog.DEBUG)

        try:
            tpaLayer = QgsProject.instance().mapLayersByName("ParkingTariffAreas")[0]
        except Exception:
            tpaLayer = None

        if tpaLayer:
            for poly in tpaLayer.getFeatures():
                currentPTA = poly.attribute("ParkingTariffArea")

                if currentPTA == tpaNr:
                    TOMsMessageLog.logMessage(
                        "In getTariffZoneDetails. Found PTA.",
                        level=TOMsMessageLog.DEBUG,
                    )
                    ptaTimePeriodID = poly.attribute("TimePeriodID")
                    ptaMaxStayID = poly.attribute("MaxStayID")
                    ptaNoReturnID = poly.attribute("NoReturnID")
                    TOMsMessageLog.logMessage(
                        "In getTariffZoneMaxStayID. ID." + str(ptaMaxStayID),
                        level=TOMsMessageLog.DEBUG,
                    )
                    return ptaTimePeriodID, ptaMaxStayID, ptaNoReturnID

        return None, None, None

    @staticmethod
    def findNearestPointOnLineLayer(searchPt, lineLayer, tolerance, geometryIDs=None):
        # TODO: re-organise - currently also in TOMsSnapTrace ....
        # given a point, find the nearest point (within the tolerance) within the line layer
        # returns QgsPoint
        TOMsMessageLog.logMessage(
            "In findNearestPointOnLineLayer. Checking lineLayer: {}: {}".format(lineLayer.name(), geometryIDs),
            level=TOMsMessageLog.DEBUG,
        )
        searchRect = QgsRectangle(
            searchPt.x() - tolerance,
            searchPt.y() - tolerance,
            searchPt.x() + tolerance,
            searchPt.y() + tolerance,
        )

        request = QgsFeatureRequest()
        request.setFilterRect(searchRect)
        request.setFlags(QgsFeatureRequest.ExactIntersect)

        shortestDistance = float("inf")
        # nearestPoint = QgsFeature()

        # Loop through all features in the layer to find the closest feature
        for feat in lineLayer.getFeatures(request):
            TOMsMessageLog.logMessage(
                "In findNearestPointOnLineLayer: {}".format(feat.id()),
                level=TOMsMessageLog.DEBUG,
            )
            if geometryIDs is not None:
                # print ('***** currGeometryID: {}; GeometryID: {}'.format(currGeometryID, f.attribute("GeometryID")))
                try:
                    testGeometryID = feat.attribute("GeometryID")
                except KeyError:
                    return (
                        None,
                        None,
                    )  # layer does not have "GeometryID" field, i.e., not restriction layer

                if testGeometryID in geometryIDs:
                    continue
            # Add any features that are found should be added to a list
            # print ('feature found: {}'.format(f.id()))
            closestPtOnFeature = feat.geometry().nearestPoint(QgsGeometry.fromPointXY(searchPt))
            dist = feat.geometry().distance(QgsGeometry.fromPointXY(searchPt))
            if dist < shortestDistance:
                shortestDistance = dist
                closestPoint = closestPtOnFeature
                closestFeature = feat

        TOMsMessageLog.logMessage(
            "In findNearestPointOnLineLayer: shortestDistance: " + str(shortestDistance),
            level=TOMsMessageLog.DEBUG,
        )

        if shortestDistance < float("inf"):
            # nearestPoint = QgsFeature()
            # add the geometry to the feature,
            # nearestPoint.setGeometry(QgsGeometry(closestPtOnFeature))
            return closestPoint, closestFeature  # returns a geometry
        return None, None

    @staticmethod
    def getLineOrientationAtPoint(point, lineFeature):
        TOMsMessageLog.logMessage("getLineOrientationAtPoint ...", level=TOMsMessageLog.DEBUG)

        lineGeom = lineFeature.geometry()
        (
            _,
            closestPt,
            vertexNrAfterPt,
            _,
        ) = lineGeom.closestSegmentWithContext(point)
        orientationToFeature = GenerateGeometryUtils.checkDegrees(point.azimuth(QgsPointXY(closestPt)))
        orientationInFeatureDirection = GenerateGeometryUtils.checkDegrees(
            closestPt.azimuth(QgsPointXY(lineGeom.vertexAt(vertexNrAfterPt)))
        )
        # orientationAwayFromFeature = generateGeometryUtils.checkDegrees(orientationToFeature + 180.0)
        orientationAwayFromFeature = GenerateGeometryUtils.checkDegrees(orientationToFeature + 180.0)
        # orientationOppositeFeatureDirection = generateGeometryUtils.checkDegrees(orientationInFeatureDirection + 180)
        orientationOppositeFeatureDirection = GenerateGeometryUtils.checkDegrees(orientationInFeatureDirection + 180.0)

        # TODO: Include type 5 - defined Az

        orientationObliqueInFeatureDirection = GenerateGeometryUtils.calcInteriorBisectAzimuth(
            orientationToFeature, orientationInFeatureDirection
        )
        orientationObliqueOppositeFeatureDirection = GenerateGeometryUtils.calcInteriorBisectAzimuth(
            orientationToFeature, orientationOppositeFeatureDirection
        )

        TOMsMessageLog.logMessage(
            f"getLineOrientationAtPoint 1: {orientationToFeature}; 2: {orientationInFeatureDirection}; 3: "
            f"{orientationAwayFromFeature}; 4: {orientationOppositeFeatureDirection}",
            level=TOMsMessageLog.DEBUG,
        )
        TOMsMessageLog.logMessage(
            f"getLineOrientationAtPoint 6: {orientationObliqueInFeatureDirection}; "
            f"7: {orientationObliqueOppositeFeatureDirection}",
            level=TOMsMessageLog.DEBUG,
        )

        return (
            orientationToFeature,
            orientationInFeatureDirection,
            orientationAwayFromFeature,
            orientationOppositeFeatureDirection,
            orientationObliqueInFeatureDirection,
            orientationObliqueOppositeFeatureDirection,
        )

    @staticmethod
    def createLinewithPointAzimuthDistance(point, azimuth, distance):
        # azimuth in degrees
        otherPointGeom = QgsPointXY(
            point.x() + (float(distance) * math.sin(math.radians(azimuth))),
            point.y() + (float(distance) * math.cos(math.radians(azimuth))),
        )
        lineGeom = QgsGeometry.fromPolylineXY([point, otherPointGeom])
        return lineGeom

    @staticmethod
    def getSignOrientation(ptFeature, lineLayer):
        TOMsMessageLog.logMessage("In getSignOrientation ...", level=TOMsMessageLog.DEBUG)
        try:
            signOrientation = ptFeature.attribute("SignOrientationTypeID")
        except KeyError:
            TOMsMessageLog.logMessage("Attribute 'SignOrientationTypeID' not found", level=Qgis.Warning)
            return [None, None, None, None, None, None, None]

        try:
            signOriginalGeometry = ptFeature.attribute("original_geom_wkt")
        except KeyError:
            TOMsMessageLog.logMessage("Attribute 'original_geom_wkt' not found", level=Qgis.Warning)
            return [None, None, None, None, None, None, None]

        TOMsMessageLog.logMessage(
            "In getSignOrientation - orientation: {}".format(signOrientation),
            level=TOMsMessageLog.DEBUG,
        )
        TOMsMessageLog.logMessage(
            "In getSignOrientation - signOriginalGeometry: {}".format(signOriginalGeometry),
            level=TOMsMessageLog.DEBUG,
        )

        if signOrientation is None:
            TOMsMessageLog.logMessage("signOrientation is None", level=Qgis.Warning)
            return [None, None, None, None, None, None, None]

        # find closest point/feature on lineLayer

        # signPt = ptFeature.geometry().asPoint()
        signPt = QgsGeometry.fromWkt(signOriginalGeometry).asPoint()
        # print('signPt: {}'.format(signPt.asWkt()))
        # TOMsMessageLog.logMessage('getSignOrientation signPt: {}'.format(signPt.asWkt()), level=TOMsMessageLog.DEBUG)
        TOMsMessageLog.logMessage(
            "In getSignOrientation lineLayer {}".format(lineLayer.name()),
            level=TOMsMessageLog.DEBUG,
        )
        (
            closestPoint,
            closestFeature,
        ) = GenerateGeometryUtils.findNearestPointOnLineLayer(signPt, lineLayer, 25)

        # Now generate a line in the appropriate direction
        if closestPoint:
            TOMsMessageLog.logMessage(
                "In getSignOrientation closestPoint: {}".format(closestPoint.asWkt()),
                level=TOMsMessageLog.DEBUG,
            )
            # get the orientation of the line feature
            (
                orientationToFeature,
                orientationInFeatureDirection,
                orientationAwayFromFeature,
                orientationOppositeFeatureDirection,
                orientationObliqueInFeatureDirection,
                orientationObliqueOppositeFeatureDirection,
            ) = GenerateGeometryUtils.getLineOrientationAtPoint(signPt, closestFeature)

            # make it match sign orientation
            return [
                0.0,
                orientationInFeatureDirection,
                orientationOppositeFeatureDirection,
                orientationToFeature,
                orientationAwayFromFeature,
                orientationObliqueInFeatureDirection,
                orientationObliqueOppositeFeatureDirection,
            ]

        TOMsMessageLog.logMessage("closestPoint not defined", level=Qgis.Warning)
        return [None, None, None, None, None, None, None]

    @staticmethod
    def getSignLine(ptFeature, lineLayer, distanceForIcons):

        try:
            signOrientation = ptFeature.attribute("SignOrientationTypeID")
        except KeyError:
            TOMsMessageLog.logMessage(
                "getSignLine - SignOrientationTypeID not found.",
                level=Qgis.Warning,
            )
            return None

        TOMsMessageLog.logMessage(
            "getSignLine - orientation: {}".format(signOrientation),
            level=TOMsMessageLog.DEBUG,
        )

        lineGeom = None
        if signOrientation is None:
            return None

        orientationList = GenerateGeometryUtils.getSignOrientation(ptFeature, lineLayer)

        # Now generate a line in the appropriate direction
        if orientationList[1]:
            # work out the length of the line based on the number of plates in the sign
            platesInSign = GenerateGeometryUtils.getPlatesInSign(ptFeature)
            nrPlatesInSign = len(platesInSign)
            TOMsMessageLog.logMessage(
                "getSignLine nrPlatesInSign: {}".format(nrPlatesInSign),
                level=TOMsMessageLog.DEBUG,
            )
            # TODO work out scaling required for sign here
            lineLength = (nrPlatesInSign + 1) * distanceForIcons
            TOMsMessageLog.logMessage(
                "getSignLine lineLength: {}".format(lineLength),
                level=TOMsMessageLog.DEBUG,
            )
            # print('getSignLine lineLength: {}'.format(lineLength))

            signPt = ptFeature.geometry().asPoint()
            # generate lines
            if signOrientation == 1:  # "Facing in same direction as road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[1], lineLength
                )
            elif signOrientation == 2:  # "Facing in opposite direction to road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[2], lineLength
                )
            elif signOrientation == 3:  # "Facing road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[3], lineLength
                )
            elif signOrientation == 4:  # "Facing away from road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[4], lineLength
                )
                # TODO: Include type 5 - defined angle
            elif signOrientation == 6:  # "Facing oblique in same direction as road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[5], lineLength
                )
            elif signOrientation == 7:  # "Facing oblique in opposite direction to road"
                lineGeom = GenerateGeometryUtils.createLinewithPointAzimuthDistance(
                    signPt, orientationList[6], lineLength
                )
            else:
                return None

        if lineGeom:
            TOMsMessageLog.logMessage(
                "getSignLine lineGeom: {}".format(lineGeom.asWkt()),
                level=TOMsMessageLog.DEBUG,
            )

        return lineGeom

    @staticmethod
    def getPlatesInSign(feature):
        TOMsMessageLog.logMessage("getNrPlatesInSign ...", level=TOMsMessageLog.DEBUG)
        finished = False
        platesInSign = []
        nrPlatesInSign = 0
        while not finished:
            field = "SignType_{}".format(nrPlatesInSign + 1)
            try:
                plateType = feature.attribute(field)
            except KeyError:
                break
            if plateType is None or str(plateType) == "NULL":
                break
            platesInSign.append(plateType)
            nrPlatesInSign = nrPlatesInSign + 1
        return platesInSign

    @staticmethod
    def addPointsToSignLine(lineGeom, nrPts, distance):
        # add points to line at equal distance
        if lineGeom is None:
            return None
        newLinePts = []
        # add points along line for icons
        for i in range(1, nrPts + 1, 1):
            newPt = (lineGeom.interpolate(i * distance)).asPoint()
            newLinePts.append(newPt)

        return newLinePts

    @staticmethod
    def finalSignLine(feature):
        return GenerateGeometryUtils.getGeneratedSignLine(feature)

    @staticmethod
    def getGeneratedSignLine(feature):
        TOMsMessageLog.logMessage(
            "getGeneratedSignLine ... {}".format(feature.attribute("GeometryID")),
            level=TOMsMessageLog.DEBUG,
        )
        roadCentreLineLayer = QgsProject.instance().mapLayersByName("RoadCentreLine")[0]
        try:
            distanceForIcons = float(
                QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("distanceForIcons")
            )
        except Exception:
            return None, None

        # distanceForIcons = 10
        # iconSize = 4
        # nrPlatesInSign = generateGeometryUtils.getNrPlatesInSign(feature)
        # print ('nrPlates: {}'.format(nrPlatesInSign))
        lineGeom = GenerateGeometryUtils.getSignLine(feature, roadCentreLineLayer, distanceForIcons)
        linePts = GenerateGeometryUtils.addPointsToSignLine(
            lineGeom,
            len(GenerateGeometryUtils.getPlatesInSign(feature)),
            distanceForIcons,
        )
        if lineGeom:
            TOMsMessageLog.logMessage(
                "getGeneratedSignLine: lineGeom: {}".format(lineGeom.asWkt()),
                level=TOMsMessageLog.DEBUG,
            )
        else:
            TOMsMessageLog.logMessage("getGeneratedSignLine: no geometry ...", level=TOMsMessageLog.DEBUG)

        return lineGeom, linePts

    @staticmethod
    def getSignIcons(ptFeature):
        TOMsMessageLog.logMessage("getSignIcons ... ", level=TOMsMessageLog.DEBUG)
        try:
            pathAbsolute = QgsExpressionContextUtils.projectScope(QgsProject.instance()).variable("iconPath")
        except Exception as e:
            TOMsMessageLog.logMessage("getSignIcons: iconPath not found {}".format(e), level=Qgis.Warning)
            return None

        platesInSign = GenerateGeometryUtils.getPlatesInSign(ptFeature)
        plateIconsInSign = []
        signTypesLayer = QgsProject.instance().mapLayersByName("SignTypes")[0]
        for plateType in platesInSign:
            signTypeRow = GenerateGeometryUtils.getLookupRow(signTypesLayer, plateType)
            if signTypeRow:
                icon = signTypeRow["Icon"]
                TOMsMessageLog.logMessage("getSignIcons: icon {}".format(icon), level=TOMsMessageLog.DEBUG)
                if not icon:
                    continue
                path = os.path.join(pathAbsolute, icon)
                plateIconsInSign.append(path)
        # TOMsMessageLog.logMessage('getSignIcons ... returning ... ', level=TOMsMessageLog.DEBUG)
        return plateIconsInSign

    @staticmethod
    def getSignOrientationList(ptFeature):
        TOMsMessageLog.logMessage("getSignOrientationList ...", level=TOMsMessageLog.DEBUG)
        try:
            roadCentreLineLayer = QgsProject.instance().mapLayersByName("RoadCentreLine")[0]
        except Exception as e:
            TOMsMessageLog.logMessage(
                "getSignOrientationList: RoadCentreLine layer not found {}".format(e),
                level=Qgis.Warning,
            )
            return None

        orientationList = GenerateGeometryUtils.getSignOrientation(ptFeature, roadCentreLineLayer)

        if orientationList[1]:  # check that valid values have been returned
            # This list give the orientation for the way the line is pointing.
            # Now need to swap each through 180 to give true direction for arrows, etc
            # add extra value to match SignsOrientation values
            newOrientationList = [
                0,
                orientationList[2],  # "Facing in same direction as road" inverted is 2
                orientationList[1],  # "Facing in opposite direction to road" inverted is 1
                orientationList[4],  # "Facing road" inverted is 4
                orientationList[3],  # "Facing away from road" inverted is 3
                0,  # defined azimuth ...
                GenerateGeometryUtils.checkDegrees(orientationList[5] + 180.0),  # oblique in road direction
                GenerateGeometryUtils.checkDegrees(orientationList[6] + 180.0),  # oblique opposite road direction
            ]

            try:
                featureSignOrientation = ptFeature.attribute("SignOrientationTypeID")
            except KeyError:
                return None
            TOMsMessageLog.logMessage(
                "getSignOrientationList ...{}".format(newOrientationList[featureSignOrientation]),
                level=TOMsMessageLog.DEBUG,
            )
            # return orientationList
            return newOrientationList[featureSignOrientation]

        return 0

    @staticmethod
    def getLookupRow(lookupLayer, code):
        query = "\"Code\" = '{}'".format(code)
        request = QgsFeatureRequest().setFilterExpression(query)
        for row in lookupLayer.getFeatures(request):
            return row  # make assumption that only one row
        return None


## 비밀번호 입력 및 photo함수 내에 경로 설정 필수

from flask import Flask, request, render_template
import pymysql
import urllib.request
from sqlalchemy import create_engine, MetaData, Table, select, func
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.automap import automap_base
import pygal
from pygal.style import Style
from datetime import datetime, timedelta
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import matplotlib
import io
import base64
import json
import threading

matplotlib.use('Agg')

app = Flask(__name__)

## 한글 출력
app.config['JSON_AS_ASCII'] = False

## DB 연동 (SQLAlchemy)
user = "admin"
password = "" ## 비밀번호 입력
host = "blogdb.cm2yxwfja9ii.ap-northeast-2.rds.amazonaws.com"
port = 3306
db = "blogdb"
db_url = f'mysql+pymysql://{user}:{password}@{host}:{port}/{db}'

## 엔진 생성
engine = create_engine(db_url, echo=False) ## SQL문 출력하지 않도록 설정

## 기존 테이블을 매핑할 기본 클래스 생성
Base = automap_base()

## 엔진과 테이블 매핑
Base.prepare(engine, reflect=True)

## 테이블 클래스 가져오기
tb_res = Base.classes.restaurants
tb_post = Base.classes.post
tb_words = Base.classes.keywords
tb_menu = Base.classes.menu_list
tb_p_cnt = Base.classes.post_year

## 세션 생성
Session = sessionmaker(engine)
session = Session()

## 그래프에 사용할 폰트 경로
font_path = "C:/work/python/blog_API/static/bmjua.ttf"

## 차트의 style 설정
custom_style = Style(
    background='transparent',
    plot_background='white',
    font_family = "NanumGothic",
    title_font_size = 30,
    label_font_size = 25,
    legend_font_size = 25,
    value_font_size = 25,
    value_label_font_size = 25,
    tooltip_font_size = 25,
    no_data_font_size = 25,
    major_label_font_size = 25
)

## url로 사진 저장하는 함수
def photo(img_url, img_name):
    img_folder = "C:/work/python/blog_API/static/"
    urllib.request.urlretrieve(img_url, img_folder+img_name)

def save_photo_async(url, img_name):
    def save_image():
        img_folder = "C:/work/python/blog_API/static/"
        urllib.request.urlretrieve(url, img_folder + img_name)

    thread = threading.Thread(target=save_image)
    thread.start()

## 첫 번째 페이지
@app.route('/')
@app.route('/home')
def home():
    return render_template('home.html')

## 두 번째 페이지
@app.route('/place', methods=['GET', 'POST'])
def place():
    try:
        location = request.form.get('location', '')  # HTML에서 location값 받아옴

        # 트랜잭션을 시작하여 연결을 얻어옴
        with engine.begin() as conn:
            sel_rec = select(tb_res.name, tb_res.id).where(tb_res.district == f'{location}').order_by(tb_res.score.desc()).limit(5)
            result = conn.execute(sel_rec)
            restaurants = result.fetchall()  # 결과를 리스트로 저장

        sel_cnt = session.query(
            tb_res.district,
            func.sum(tb_res.total_post_cnt),
            func.sum(tb_res.post_cnt)
        ).group_by(tb_res.district)

        result = sel_cnt.all()

        result_dict = {district: [int(total_post_cnt), int(post_cnt)] for district, total_post_cnt, post_cnt in result}
        non_ad_rate = round(result_dict[location][1] / result_dict[location][0], 3) * 100
        ad_rate = 100 - non_ad_rate

        ## Pie
        pie_chart = pygal.Pie(inner_radius=.4, style=custom_style)
        pie_chart.title = '광고 비율'
        pie_chart.add('AD', ad_rate)
        pie_chart.add('NON-AD', non_ad_rate)
        pie_chart = pie_chart.render_data_uri()

        ## WordCloud
        sel_dic = select(tb_words.key_words).where(tb_words.district == f'{location}')
        data2 = session.execute(sel_dic).all()
        data = data2[0][0]
        data_str = data.replace("'", "\"")
        data_dict = json.loads(data_str)

        wordcloud = WordCloud(
            font_path='malgun',
            width=400,
            height=300,
            background_color='white'
        ).generate_from_frequencies(data_dict)

        img_buffer = io.BytesIO()
        wordcloud.to_image().save(img_buffer, format='PNG')  # WordCloud를 이미지로 저장

        img_buffer.seek(0)
        img_data = base64.b64encode(img_buffer.read()).decode('utf-8')

        return render_template('page2.html', name=location, restaurants=restaurants, pie_chart=pie_chart, img_path=img_data)

    except Exception as e:
        print("An error occurred:", e)
        session.rollback()  # 트랜잭션 롤백
        return render_template('error.html', error_message="An error occurred.")

    finally:
        session.close()  # 세션 닫기

## 세 번째 페이지
@app.route('/restaurant', methods=['GET', 'POST'])
def restaurant():
    try:
        clicked_restaurant = request.args.get('restaurant', '')
        restaurant_info = clicked_restaurant.strip('()').split(', ')
        restaurant_name = restaurant_info[0]
        restaurant_id = int(restaurant_info[1])

        # 트랜잭션을 시작하여 연결을 얻어옴
        with engine.begin() as conn:
            sel_top5_post = select(tb_post.title, tb_post.url, tb_post.description, tb_post.food_img_url).where(tb_post.res_id == f'{restaurant_id}').limit(30)
            post_result = conn.execute(sel_top5_post)
            post = post_result.fetchall()  # 결과를 리스트로 저장

        l_post = [[],[],[],[]]

        for i in range(len(post)):
            try:
                l_post[0].append(post[i][0])
                l_post[1].append(post[i][1])
                l_post[2].append(post[i][2])
                url = post[i][3]
                img = f'img{i}.jpg'
                l_post[3].append(img)
                save_photo_async(url, img)  # 이미지 저장을 비동기적으로 실행
            except Exception as e:
                continue

        ## 메뉴, 개수 가져오기
        sel_menu = select(tb_menu.menu, tb_menu.menu_cnt).where(tb_menu.res_id == f'{restaurant_id}').order_by(tb_menu.menu_cnt.desc()).limit(5)
        menu = session.execute(sel_menu).all()

        ## Bar
        bar_chart = pygal.HorizontalBar(style=custom_style)
        bar_chart.title = '메뉴 언급 수'
        bar_chart.legend_at_bottom = True
        for i in range(len(menu)):
            bar_chart.add(menu[i][0], menu[i][1])
        bar_chart.x_label_rotation = (360 - 45)
        bar_chart = bar_chart.render_data_uri()

        ## 분기별 포스팅 수 받아오기
        sel_p_cnt = select(tb_p_cnt.res_id, tb_p_cnt.year_q, tb_p_cnt.post_count).where(tb_p_cnt.res_id == f'{restaurant_id}').order_by(tb_p_cnt.year_q)
        p_cnt = session.execute(sel_p_cnt).all()

        ## Line
        date_chart = pygal.Line(x_label_rotation=20, style=custom_style)
        date_chart.title = '기간별 포스팅 수'
        date_chart.x_title = "날짜(년도-분기)"
        x_labels = [item[1] for item in p_cnt]
        values = [item[2] for item in p_cnt]
        date_chart.x_labels = x_labels
        date_chart.add('Posts', values)
        date_chart = date_chart.render_data_uri()

        return render_template('page3.html', restaurant=restaurant_name, l_post=l_post, bar_chart=bar_chart, date_chart=date_chart)

    except Exception as e:
        print("An error occurred:", e)
        session.rollback()  # 트랜잭션 롤백
        return render_template('error.html', error_message="An error occurred.")

    finally:
        session.close()  # 세션 닫기

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=8000)


import sys, xbmc, json

try:
	from urlparse import parse_qsl
except:
	from urllib.parse import parse_qsl

if __name__ == '__main__':
	item = sys.listitem
	message = item.getLabel()
	path = item.getPath()
	plugin = 'plugin://plugin.video.venom/'
	args = path.split(plugin, 1)
	# xbmc.log('args = %s' % args, 2)
	params = dict(parse_qsl(args[1].replace('?', '')))

	if 'meta' in params:
		meta = json.loads(params['meta'])
		imdb = meta.get('imdb', '')
		action = 'tvshows' if 'tvshowtitle' in meta else 'movies'

	else:
		imdb = params.get('imdb', '')
		action = 'tvshows' if 'tvshowtitle' in params else 'movies'

	xbmc.executebuiltin('ActivateWindow(Videos,plugin://plugin.video.venom/?action=%s&url=https://api.trakt.tv/%s/%s/related,return)' % (action, 'shows' if 'tvshows' in action else 'movies', imdb))


from collections import defaultdict
from Maze import *
from BFS import *
import math
from datetime import datetime
from time import time
import pickle
def print_dict(d):
    for key,value in d.items():
        print(key," : ",value)

if __name__=="__main__":
# def main():
    start = time()

    # #=========== Log file =======================
    # # filename_txt="Results/Utility4.txt"
    # filename_txt="/Users/abhishek.sawalkar/Library/Mobile Documents/com~apple~CloudDocs/AI Project/BetterSmarterFasterCircleOfLife/Results/Utility17.txt"
    # # filename_csv="Results/AgentOne.csv"
    # file=open(filename_txt,"w")
    # # csvfile = open(filename_csv, "a")
    # # csv_writer=csv.writer(csvfile)
    # # fields=['Date Time','Simulation Number','Number of Graphs','Won','Died','Hanged','Comments']
    # # csv_writer.writerow(fields)
    # text = "\n\n\n======  Start Time  =========->  " + \
    #     datetime.now().strftime("%m/%d/%y %H:%M:%S")
    # file.write(text)
    # # file.write("\nNo. of Simulations = 30")
    # # file.write("\nNo. of trials for each simulation = 100")
    # # csv_writer.writerow(["Execution Started"])
    #============================================
    file_path='/Users/abhishek.sawalkar/Desktop/520 Video Lectures/Finding_Your_Way/Schematics/Schema_1.txt'
    maze=generate_maze(file_path)
    n_row=maze.shape[0]
    n_col=maze.shape[1]

    state_dict=defaultdict()
    final_utility=defaultdict()

    right=[0, 1]
    left=[0, -1]
    down=[1, 0]
    up=[-1, 0]
    # Defining State dictionary
    for pos_1_r in range(0,n_row):
        for pos_1_c in range(0,n_col):
            for pos_2_r in range(0,n_row):
                for pos_2_c in range(0,n_col):
                    if maze[pos_1_r,pos_1_c] != 5 and maze[pos_2_r,pos_2_c] != 5:

                        key=[(pos_1_r,pos_1_c),(pos_2_r,pos_2_c)]
                            # print(key)
                        # key=sorted(key)
                            # print(key)
                        key=(key[0][0],key[0][1],key[1][0],key[1][1])
                        if key not in state_dict.keys():

                            state_dict[key]=[0,0] #reward, utility
                            final_utility[key]=0

    #Reading the stored graph
    # G = nx.read_gpickle("StoredGraph/Graph1.gpickle")
    # G = nx.read_gpickle("/Users/abhishek.sawalkar/Library/Mobile Documents/com~apple~CloudDocs/AI Project/BetterSmarterFasterCircleOfLife/StoredGraph/Graph1.gpickle")

    #======Distance Dictionary Generation =========
    # dist_dict=defaultdict()
    # for pos_1_r in range(0,n_row):
    #     for pos_1_c in range(0,n_col):
    #         for pos_2_r in range(0,n_row):
    #             for pos_2_c in range(0,n_col):
    #                 if maze[pos_1_r,pos_1_c] != 5 and maze[pos_2_r,pos_2_c] != 5:
    #                     key=[(pos_1_r,pos_1_c),(pos_2_r,pos_2_c)]
    #                     # print(key)
    #                     # key=sorted(key)
    #                     # print(key)
    #                     key=(key[0][0],key[0][1],key[1][0],key[1][1])
    #                     if key not in dist_dict.keys():
    #                         distance=len(get_bfs_path(maze, n_row, n_col, (pos_1_r,pos_1_c), (pos_2_r,pos_2_c)))-1
    #                         # print(distance)
    #                         dist_dict[key]=distance
    # dist_file = open("StoredDistances/dist_dict_Schema1.pkl", "wb")
    # pickle.dump(dist_dict, dist_file)
    # dist_file.close()
    #==========================

    # print(len(dist_dict))
    # print(len(state_dict))
    with open("StoredDistances/dist_dict_schema1.pkl", 'rb') as handle:
        data = handle.read()
    dist_dict = pickle.loads(data)

    #Setting initial rewards and utility
    for state in state_dict:
        pos_1=(state[0],state[1])
        pos_2=(state[2],state[3])

        if pos_1==pos_2: #agent and prey has same position
            state_dict[state]=[0,0] #minimum possible expected number of steps to prey is 0
        # elif state[0]==state[2]:
        #     state_dict[state]=[math.inf,0] #reaching prey is impossible so a high value
        else:
            # distance_to_prey=len(get_bfs_path(G,state[0],state[1]))

            distance=dist_dict[state]
            state_dict[state]=[1]+[distance]
    # print(dist_dict[(0, 0, 0, 1)])
    #=============================================================
    #=========== Value Iteration Algorithm =======================
    #=============================================================
    beta=0.9
    n_iterations=50
    # error_list=[0]*len(state_dict)
    error_list=[]
    for k in range(n_iterations):
        #Running for all states
        error=0
        #For each particular state-We will calculate U* for all possible actions. Then take the min of these values to be the final U for this iteration
        for state in state_dict:
            #Generate action space by getting the neighbors of the current cell of both cells
            pos_1=(state[0],state[1])
            pos_2=(state[2],state[3])

            if pos_1==pos_2: #agent and prey has same position
                min_utility=0
                state_dict[state].append(min_utility)
                continue
            #Cell1 is in neighborhood of cell 2
            if dist_dict[state]==1:
                min_utility=1
                state_dict[state].append(min_utility)
                continue

            #Now we need to figure out the action space of the 2 cells, which would be mainly 4 actions - UP, DOWN, Left, Right

            #Up operation
            r1=state[0]
            c1=state[1]
            r2=state[2]
            c2=state[3]
            action_space=defaultdict()

            up_state=get_dir_state(n_row,n_col,maze,r1,c1,r2,c2,up)
            down_state=get_dir_state(n_row,n_col,maze,r1,c1,r2,c2,down)
            left_state=get_dir_state(n_row,n_col,maze,r1,c1,r2,c2,left)
            right_state=get_dir_state(n_row,n_col,maze,r1,c1,r2,c2,right)
            #Initializing utilities
            action_space["Up"]=state_dict[up_state][-1]
            action_space["Down"]=state_dict[down_state][-1]
            action_space["Left"]=state_dict[left_state][-1]
            action_space["Right"]=state_dict[right_state][-1]
            # #Initializing utilities
            # action_space[up_state]=state_dict[up_state][-1]
            # action_space[down_state]=state_dict[up_state][-1]
            # action_space[left_state]=state_dict[up_state][-1]
            # action_space[right_state]=state_dict[up_state][-1]

            # down_state,left_state,right_state]
            # prev_utility=

            reward=state_dict[state][0]
            min_utility=min(action_space.values())
            min_possible_action_list=[key for key in action_space if action_space[key]==min_utility] # getting the minimum action
            min_action=random.choice(min_possible_action_list)
            prev_utility=action_space[min_action]

            utility=reward+beta*prev_utility

            state_dict[state].append(utility)

            # #Up states for pos 1
            # if r1-1>=0:
            #     if maze[r1-1,c1]!=5:
            #         up_state_1=(r1-1,c1)
            #     else:
            #         up_state_1=(r1,c1)
            # else:
            #     up_state_1=(r1,c1)

            # #Up states for pos 1
            # if r2-1>=0:
            #     if maze[r2-1,c1]!=5:
            #         up_state_2=(r2-1,c2)
            #     else:
            #         up_state_2=(r2,c2)
            # else:
            #     up_state_2=(r2,c2)

            # up_state=[up_state_1,up_state_2]
            # up_state=sorted(up_state)
            # up_state=(up_state[0][0],up_state[0][1],up_state[1][0],up_state[1][1])
            # print("State->",state)
            # print(up_state)

            # continue

            if k>2 :
                error+=state_dict[state][-1]-state_dict[state][-2]
        print("Iteration Done ->",k)
        iteration_error=error/len(state_dict)
        print("Iteration Error = ",iteration_error)
        # if k>5 and abs(iteration_error)<0.001:
        #     break
        error_list.append(iteration_error)
    # print(state_dict)
    value_list=state_dict.values()
    m=-1
    for l in value_list:
        l2=l[2:]
        m_l=max(l)
        if m<m_l and not math.isinf(m_l):
            m=m_l

    # #========= Constructing utility dictionary ==========
    for state in final_utility:
        final_utility[state]=state_dict[state][-1]
    # #======== Dumping a dictionary as pickle and then reading it again using loads =====
    final_utility_file = open("StoredUtilities/Schema3.pkl", "wb")
    pickle.dump(final_utility, final_utility_file)
    # final_utility_file.write(str(final_utility))
    final_utility_file.close()

    #Whole utility file

    end=time()
    file = open("StoredUtilities/Schema3_Result.txt", "w")
    print("Max Utility = ",m)
    file.write('\n Max Utility =  %s'%str(m))
    file.write("\n\nExecution Time = "+str(end-start)+" s\n")

    file.write('\n Iteration Error->')
    for error in error_list:
        file.write('  %s, '%str(error))

    file.write('\n \nState Dictionary->')
    for key, value in state_dict.items():
        file.write('    %s  :       %s  \n' % (key, value[30:]))
    print("Done")
    # print("Execution time : "+str(end-start)+" s")
    # print_dict(state_dict)

def get_dir_state(n_row,n_col,maze,r1,c1,r2,c2,dir):
    #Up states for pos 1
    next_r1=r1+dir[0]
    next_c1=c1+dir[1]
    if next_r1>=0 and next_r1<n_row and next_c1>=0 and next_c1<n_col:
        if maze[next_r1,next_c1]!=5:
            dir_state_1=(next_r1,next_c1)
        else:
            dir_state_1=(r1,c1)
    else:
        dir_state_1=(r1,c1)

    #Up states for pos 2
    next_r2=r2+dir[0]
    next_c2=c2+dir[1]
    if next_r2>=0 and next_r2<n_row and next_c2>=0 and next_c2<n_col:
        if maze[next_r2,next_c2]!=5:
            dir_state_2=(next_r2,next_c2)
        else:
            dir_state_2=(r2,c2)
    else:
        dir_state_2=(r2,c2)

    # if r2-1>=0:
    #     if maze[r2-1,c1]!=5:
    #         dir_state_2=(r2-1,c2)
    #     else:
    #         dir_state_2=(r2,c2)
    # else:
    #     dir_state_2=(r2,c2)

    dir_state=[dir_state_1,dir_state_2]
    dir_state=(dir_state[0][0],dir_state[0][1],dir_state[1][0],dir_state[1][1])
    return dir_state
# main()


#!/usr/bin/env python3

import socket
import random
import hashlib
import sys
import threading
import time
from bitarray import bitarray

TEAM_NUMBER = 0
MODULE_CHOICE = 4963

HOST = "p2psec.net.in.tum.de"
PORT = 13337
ENROLL_REGISTER = 681

class Register:

    data = b''

    def main(self):
        # get data from server
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
            server.connect((HOST, PORT))
            # serverfile = server.makefile('rwb')
            # self.data = serverfile.read(12)
            self.data = server.recv(12)
            # t = threading.Thread(target=listen_for_messages,args=(server,))
            # t.daemon = True
            # t.start()
            # time.sleep(1)
            print([hex(x) for x in self.data])
            packet = build_message(self.data[4:])
            self.data = b''
            print("the packet send ready: ", packet)
            # packet_size = int.from_bytes(packet[:2],byteorder='big')
            # serverfile.write(packet)
            # serverfile.flush()

            back = server.send(packet)
            print("sent packet", back)
            # time.sleep(1)
            # self.data = serverfile.read(12)
            print("before receive",self.data)
            self.data = server.recv(1024)

            # try:
            #     self.data = server.recv(1024,socket.MSG_DONTWAIT|socket.MSG_PEEK)
            # except Exception:
            #     print("eep",sys.exc_info()[0])
            print("received data: ", self.data)
            print([hex(x) for x in self.data])
            message = self.data[2:4]
            team_number = self.data[6:8]
            error_description = self.data[8:]
            print("status", message)
            print("error/team-number", team_number)
            print("error/team-number", int.from_bytes(team_number,"little"))
            print("error/team-number", int.from_bytes(team_number,"big"))
            print("error description", error_description)
            print("the hash is", hashlib.sha256(packet[4:]).digest())

    def listen_for_messages(self,server):
        run = 0
        print("start thread with socket", server)
        self.data = b''
        while True:
            message = server.recv(1024)
            old_data = self.data
            self.data += message
            if(old_data != self.data):
                print("message received from thread: ",self.data)
            # print(f'run number: {run}')
            run += 1

def build_message(challenge):
    message = proof_of_work(challenge, get_choice_message(), get_info_message())
    # message = challenge +  get_choice_message() + random.randbytes(8)+  get_info_message()
    header = get_header(len(message))
    return header + message

def proof_of_work(challenge,choosing_part,info_part):
    run = 0
    while True:
        nonce = random.randbytes(8)
        message = challenge + choosing_part + nonce + info_part
        # message = pad_for_sha256(message)
        sha = hashlib.sha256(message).digest()
        # sha = b'\x00\x00\x00'
        # print(f'run number: {run}',end='\r')
        if(sha[:3] == b'\x00\x00\x00'):
            print(len(message))
            return message
        run += 1

def pad_for_sha256(message):
    m_as_bits = bitarray()
    m_as_bits.frombytes(message)
    m_as_bits.append(1)
    m_as_bits.extend(calculate_K_padding(message) * '0')
    size = bytes.fromhex(str.format('{:016X}',len(message)))
    m_as_bits.frombytes(size)
    return m_as_bits.tobytes()

def calculate_K_padding(message):
    return  (512 - (len(message) * 8 + 1 + 64)) % 512

def get_choice_message():
    team_number = bytes.fromhex(str.format('{:04X}',TEAM_NUMBER))
    project_choice = bytes.fromhex(str.format('{:04X}',MODULE_CHOICE))
    return team_number + project_choice

def get_info_message():
    email = "s.walchshaeusl@tum.de\r\n".encode()
    firstname = "Sebastian\r\n".encode()
    lastname = "Walchshaeusl\r\n".encode()
    lrz_gitlab_username = "ga84mim".encode()
    return email + firstname + lastname + lrz_gitlab_username

def get_header(size):
    size = bytes.fromhex(str.format('{:04X}',size + 4))
    enroll_register=bytes.fromhex(str.format('{:04X}',ENROLL_REGISTER))
    return size + enroll_register

    # def mysend(server,msg,MSGLEN):
    #     totalsent = 0
    #     while totalsent < MSGLEN:
    #         sent = server.send(msg[totalsent:])
    #         if sent == 0:
    #             raise RuntimeError("socket connection broken")
    #         totalsent = totalsent + sent

    # def myreceive(server,MSGLEN):
    #     chunks = []
    #     bytes_recd = 0
    #     while bytes_recd < MSGLEN:
    #         chunk = server.recv(min(MSGLEN - bytes_recd, 2048))
    #         if chunk == b'':
    #             raise RuntimeError("socket connection broken")
    #         chunks.append(chunk)
    #         bytes_recd = bytes_recd + len(chunk)
    #         return b''.join(chunks)

if __name__ == "__main__":
    program = Register()
    program.main()


"""
==================
example_service.py
==================

An example service adapter implementation and example CLI parser
"""

import argparse
import shutil
import os
from tempfile import mkdtemp
from pystac import Asset

import harmony
from harmony.util import generate_output_filename, stage, download

# IMPORTANT: The following line avoids making real calls to a non-existent
# Harmony frontend.  Service authors should not set this variable to "dev"
# or "test" when releasing the service.
os.environ['ENV'] = 'dev'

class ExampleAdapter(harmony.BaseHarmonyAdapter):
    """
    Shows an example of what a service adapter implementation looks like
    """
    def process_item(self, item, source):
        """
        Processes a single input item.  Services that are not aggregating multiple input files
        should prefer to implement this method rather than #invoke

        This example copies its input to the output, marking "dpi" and "variables" message
        attributes as having been processed

        Parameters
        ----------
        item : pystac.Item
            the item that should be processed
        source : harmony.message.Source
            the input source defining the variables, if any, to subset from the item

        Returns
        -------
        pystac.Item
            a STAC catalog whose metadata and assets describe the service output
        """
        result = item.clone()
        result.assets = {}

        # Create a temporary dir for processing we may do
        workdir = mkdtemp()
        try:
            # Get the data file
            asset = next(v for k, v in item.assets.items() if 'data' in (v.roles or []))
            input_filename = download(asset.href, workdir, logger=self.logger, access_token=self.message.accessToken)

            # Mark any fields the service processes so later services do not repeat work
            dpi = self.message.format.process('dpi')
            # Variable subsetting
            variables = source.process('variables')

            # Do the work here!
            var_names = [v.name for v in variables]
            print('Processing item %s, DPI=%d, vars=[%s]' % (item.id, dpi, ', '.join(var_names)))
            working_filename = os.path.join(workdir, 'tmp.txt')
            shutil.copyfile(input_filename, working_filename)

            # Stage the output file with a conventional filename
            output_filename = generate_output_filename(asset.href, ext=None, variable_subset=None,
                                                       is_regridded=False, is_subsetted=False)
            url = stage(working_filename, output_filename, 'text/plain', location=self.message.stagingLocation,
                        logger=self.logger)

            # Update the STAC record
            result.assets['data'] = Asset(url, title=output_filename, media_type='text/plain', roles=['data'])
            # Other metadata updates may be appropriate, such as result.bbox and result.geometry
            # if a spatial subset was performed

            # Return the STAC record
            return result
        finally:
            # Clean up any intermediate resources
            shutil.rmtree(workdir)

def run_cli(args):
    """
    Runs the CLI.  Presently stubbed to demonstrate how a non-Harmony CLI fits in and allow
    future implementation or removal if desired.

    Parameters
    ----------
    args : Namespace
        Argument values parsed from the command line, presumably via ArgumentParser.parse_args

    Returns
    -------
    None
    """
    print("TODO: You can implement a non-Harmony CLI here.")
    print('To see the Harmony CLI, pass `--harmony-action=invoke '
          '--harmony-input="$(cat example/example_message.json)" '
          '--harmony-sources=example/source/catalog.json --harmony-output-dir=tmp/`')

def main():
    """
    Parses command line arguments and invokes the appropriate method to respond to them

    Returns
    -------
    None
    """
    parser = argparse.ArgumentParser(prog='example', description='Run an example service')

    harmony.setup_cli(parser)

    args = parser.parse_args()

    if (harmony.is_harmony_cli(args)):
        harmony.run_cli(parser, args, ExampleAdapter)
    else:
        run_cli(args)

if __name__ == "__main__":
    main()


#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2017
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program.  If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Invoice."""

from telegram import TelegramObject

class Invoice(TelegramObject):
    """This object contains basic information about an invoice.

    Attributes:
        title (:obj:`str`): Product name.
        description (:obj:`str`): Product description.
        start_parameter (:obj:`str`): Unique bot deep-linking parameter.
        currency (:obj:`str`): Three-letter ISO 4217 currency code.
        total_amount (:obj:`int`): Total price in the smallest units of the currency.

    Args:
        title (:obj:`str`): Product name.
        description (:obj:`str`): Product description.
        start_parameter (:obj:`str`): Unique bot deep-linking parameter that can be used to
            generate this invoice.
        currency (:obj:`str`): Three-letter ISO 4217 currency code.
        total_amount (:obj:`int`): Total price in the smallest units of the currency (integer, not
            float/double). For example, for a price of US$ 1.45 pass amount = 145.
        **kwargs (:obj:`dict`): Arbitrary keyword arguments.

    """

    def __init__(self, title, description, start_parameter, currency, total_amount, **kwargs):
        self.title = title
        self.description = description
        self.start_parameter = start_parameter
        self.currency = currency
        self.total_amount = total_amount

    @classmethod
    def de_json(cls, data, bot):
        if not data:
            return None

        return cls(**data)