import math
import requests

def CreateLayout(Node_Start, Elem_Start, alignData, segmData):
    # Coordinates and Angle of Nodes
    Xi = [0]
    Yi = [0]
    Ti = [math.pi / 2]

    # Alignment Informations
    Alin_Info = Alignment(alignData)
    TempAlin = [[], [], []]
    Alin_Info.append([])
    Alin_Info.append([])
    Alin_Info.append([])
    
    for i in range(len(Alin_Info[0])):
        alignment_type = Alin_Info[0][i]
        
        if alignment_type == "ST":
            if i == 0:
                TempAlin = Straight_Line(Xi[0], Yi[0], Ti[0], Alin_Info[1][i])
            else:
                TempAlin = Straight_Line(Alin_Info[4][i - 1], Alin_Info[5][i - 1], Alin_Info[6][i - 1], Alin_Info[1][i])
        elif alignment_type == "AR":
            if i == 0:
                TempAlin = Arc_Line(Xi[0], Yi[0], Ti[0], Alin_Info[1][i], Alin_Info[2][i])
            else:
                TempAlin = Arc_Line(Alin_Info[4][i - 1], Alin_Info[5][i - 1], Alin_Info[6][i - 1], Alin_Info[1][i], Alin_Info[2][i])
        elif alignment_type == "CL":
            if i == 0:
                TempAlin = Clothoid_Line(Xi[0], Yi[0], Ti[0], Alin_Info[1][i], Alin_Info[2][i], Alin_Info[3][i], Alin_Info[1][i])
            else:
                TempAlin = Clothoid_Line(Alin_Info[4][i - 1], Alin_Info[5][i - 1], Alin_Info[6][i - 1], Alin_Info[1][i], Alin_Info[2][i], Alin_Info[3][i], Alin_Info[1][i])
        elif alignment_type == "CP":
            if i == 0:
                TempAlin = CubicParabola_Line(Xi[0], Yi[0], Ti[0], Alin_Info[1][i], Alin_Info[2][i], Alin_Info[3][i], Alin_Info[1][i])
            else:
                TempAlin = CubicParabola_Line(Alin_Info[4][i - 1], Alin_Info[5][i - 1], Alin_Info[6][i - 1], Alin_Info[1][i], Alin_Info[2][i], Alin_Info[3][i], Alin_Info[1][i])

        Alin_Info[4].append(TempAlin[0])
        Alin_Info[5].append(TempAlin[1])
        Alin_Info[6].append(TempAlin[2])

    # Segment Informations
    Seg_Info = Segment(segmData, Alin_Info[0], Alin_Info[1], Node_Start, Elem_Start)
    
    for i in range(len(Seg_Info[0])):
        segment_type = Seg_Info[4][i]
        
        if segment_type == "ST":
            if Seg_Info[3][i] == 0:
                TempAlin = Straight_Line(Xi[0], Yi[0], Ti[0], Seg_Info[5][i])
            else:
                TempAlin = Straight_Line(Alin_Info[4][Seg_Info[3][i] - 1], Alin_Info[5][Seg_Info[3][i] - 1], Alin_Info[6][Seg_Info[3][i] - 1], Seg_Info[5][i])
        elif segment_type == "AR":
            if Seg_Info[3][i] == 0:
                TempAlin = Arc_Line(Xi[0], Yi[0], Ti[0], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]])
            else:
                TempAlin = Arc_Line(Alin_Info[4][Seg_Info[3][i] - 1], Alin_Info[5][Seg_Info[3][i] - 1], Alin_Info[6][Seg_Info[3][i] - 1], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]])
        elif segment_type == "CL":
            if Seg_Info[3][i] == 0:
                TempAlin = Clothoid_Line(Xi[0], Yi[0], Ti[0], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]], Alin_Info[3][Seg_Info[3][i]], Alin_Info[1][Seg_Info[3][i]])
            else:
                TempAlin = Clothoid_Line(Alin_Info[4][Seg_Info[3][i] - 1], Alin_Info[5][Seg_Info[3][i] - 1], Alin_Info[6][Seg_Info[3][i] - 1], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]], Alin_Info[3][Seg_Info[3][i]], Alin_Info[1][Seg_Info[3][i]])
        elif segment_type == "CP":
            if Seg_Info[3][i] == 0:
                TempAlin = CubicParabola_Line(Xi[0], Yi[0], Ti[0], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]], Alin_Info[3][Seg_Info[3][i]], Alin_Info[1][Seg_Info[3][i]])
            else:
                TempAlin = CubicParabola_Line(Alin_Info[4][Seg_Info[3][i] - 1], Alin_Info[5][Seg_Info[3][i] - 1], Alin_Info[6][Seg_Info[3][i] - 1], Seg_Info[5][i], Alin_Info[2][Seg_Info[3][i]], Alin_Info[3][Seg_Info[3][i]], Alin_Info[1][Seg_Info[3][i]])

        Xi.append(TempAlin[0])
        Yi.append(TempAlin[1])
        Ti.append(TempAlin[2])

    dbNODE = create_node(Node_Start, Xi, Yi)
    dbELEM = create_elem(Node_Start, Elem_Start, Seg_Info[6], Seg_Info[7])
    dbSKEW = create_skew(Node_Start, Ti)
    dbGRUP = create_grup(Seg_Info[0], Seg_Info[8], Seg_Info[9], Seg_Info[10])

    return [Xi, Yi, dbNODE, dbELEM, dbSKEW, dbGRUP]

def create_node(node_start, xi, yi):
    # JSON body
    db_node = {"Assign": {}}

    # Assign Coordinates into JSON body
    for i in range(len(xi)):
        db_node["Assign"][node_start + i] = {"X": xi[i], "Y": yi[i]}

    return db_node

def create_elem(node_start, elem_start, matl, sect):
    # JSON body
    db_elem = {"Assign": {}}

    # Assign Element information into JSON body
    for i in range(len(matl)):
        db_elem["Assign"][elem_start + i] = {
            "TYPE": "BEAM",
            "MATL": matl[i],
            "SECT": sect[i],
            "NODE": [node_start + i, node_start + i + 1]
        }

    return db_elem

def create_skew(node_start, ti):
    # JSON body
    db_skew = {"Assign": {}}

    # Assign Skew of nodes into JSON body
    for i in range(len(ti)):
        db_skew["Assign"][node_start + i] = {
            "iMETHOD": 1,
            "ANGLE_X": 0,
            "ANGLE_Y": 0,
            "ANGLE_Z": -1 * ((ti[i] - math.pi / 2) * 180 / math.pi)
        }

    return db_skew

def create_grup(seg_grup, seg_elem, seg_nodei, seg_nodej):
    # Deduplication of Structure Names
    group_name = list(set(seg_grup))

    # JSON body
    db_grup = {"Assign": {}}

    # Assign Group into JSON Body
    for i, group in enumerate(group_name, start=1):
        # Write Element and Node lists for each group
        elem_list = []
        nodei_list = []
        nodej_list = []
        node_list = []
        
        for j in range(len(seg_grup)):
            if group == seg_grup[j]:
                elem_list.append(seg_elem[j])
                nodei_list.append(seg_nodei[j])
                nodej_list.append(seg_nodej[j])

        # Deduplicate Node list
        node_list = list(set(nodei_list + nodej_list))

        # Assign Group into JSON Body
        db_grup["Assign"][i] = {
            "NAME": group,
            "N_LIST": node_list,
            "E_LIST": elem_list
        }

    return db_grup

def Alignment(alginData):
    # Number of Lines
    nbLine = len(alginData)
    
    # Declare variables from Input
    type = [None] * nbLine
    lens = [None] * nbLine
    rads = [None] * nbLine
    rade = [None] * nbLine

    # Assign variables
    for i in range(nbLine):
        tempType = alginData[i]["linetype"]
        if tempType == "Straight":
            type[i] = "ST"
            lens[i] = alginData[i]["linelength"]
            rads[i] = 0
            rade[i] = 0
        elif tempType == "Arc":
            type[i] = "AR"
            lens[i] = alginData[i]["linelength"]
            rads[i] = alginData[i]["linerads"]
            rade[i] = 0
        elif tempType == "Clothoid":
            type[i] = "CL"
            lens[i] = alginData[i]["linelength"]
            rads[i] = alginData[i]["linerads"]
            rade[i] = alginData[i]["linerade"]
        elif tempType == "Cubic Parabola":
            type[i] = "CP"
            lens[i] = alginData[i]["linelength"]
            rads[i] = alginData[i]["linerads"]
            rade[i] = alginData[i]["linerade"]

    # Return data
    return [type, lens, rads, rade]

def Segment(Seg_info, Alin_Type, Alin_Len, Node_Start, Elem_Start):
    # Temporary Variables
    tmpIter = 0
    inc = 0
    AlinAcc = []
    
    # Return Variables
    Seg_LenEach = []
    Seg_GrpName = []
    Seg_LenAccu = []
    Seg_AlinNum = []
    Seg_AlinTyp = []
    Seg_LenAlin = []
    Seg_Material = []
    Seg_Sections = []
    Seg_ElemNb = []
    Seg_NodeNbi = []
    Seg_NodeNbj = []

    # Assign Segment Length / Segment Group Name
    for i in range(len(Seg_info)):
        tmpIter = Seg_info[i]["segNumber"]
        for j in range(tmpIter):
            Seg_LenEach.append(Seg_info[i]["seglength"])
            Seg_GrpName.append(Seg_info[i]["strgroup"])
            Seg_Material.append(Seg_info[i]["matlid"])
            Seg_Sections.append(Seg_info[i]["sectid"])
            Seg_ElemNb.append(Elem_Start + inc)
            Seg_NodeNbi.append(Node_Start + inc)
            Seg_NodeNbj.append(Node_Start + inc + 1)
            inc += 1

    # Assign Segment Accumulate Length
    for i in range(len(Seg_LenEach)):
        if i == 0:
            Seg_LenAccu.append(Seg_LenEach[i])
        else:
            Seg_LenAccu.append(Seg_LenAccu[i - 1] + Seg_LenEach[i])

    # Assign Alignment Accumulate Length
    for i in range(len(Alin_Type)):
        if i == 0:
            AlinAcc.append(Alin_Len[i])
        else:
            AlinAcc.append(AlinAcc[i - 1] + Alin_Len[i])

    # Assign Alignment number to each Segments
    for i in range(len(Seg_LenAccu)):
        for j in range(len(AlinAcc)):
            if Seg_LenAccu[i] <= AlinAcc[j]:
                Seg_AlinNum.append(j)
                Seg_AlinTyp.append(Alin_Type[j])
                break

    # Assign Accumulate length in each Alignment
    for i in range(len(Seg_LenAccu)):
        for j in range(len(AlinAcc)):
            if Seg_LenAccu[i] <= AlinAcc[j] and j == 0:
                Seg_LenAlin.append(Seg_LenAccu[i])
                break
            elif Seg_LenAccu[i] <= AlinAcc[j]:
                Seg_LenAlin.append(Seg_LenAccu[i] - AlinAcc[j - 1])
                break

    # Return data
    return [
        Seg_GrpName,
        Seg_LenEach,
        Seg_LenAccu,
        Seg_AlinNum,
        Seg_AlinTyp,
        Seg_LenAlin,
        Seg_Material,
        Seg_Sections,
        Seg_ElemNb,
        Seg_NodeNbi,
        Seg_NodeNbj
    ]

def Straight_Line(Xi, Yi, Ti, AccLen):
    # Calculate Coordinates of Straight Line
    Xj = Xi + math.sin(Ti) * AccLen
    Yj = Yi + math.cos(Ti) * AccLen
    Tj = Ti

    return [Xj, Yj, Tj]

def Arc_Line(Xi, Yi, Ti, AccLen, AlinRs):
    # Calculate Coordinates of Arc Line
    Xri = 0
    Yri = AlinRs * -1

    Trj = AccLen / AlinRs
    Xs = 0
    Ys = 0
    Ts = math.pi / 2

    Xe = math.sin(Trj) * AlinRs + Xri
    Ye = math.cos(Trj) * AlinRs + Yri
    Te = Ts + Trj

    Tse = math.atan2(Xe, Ye) if math.atan2(Xe, Ye) >= 0 else math.atan2(Xe, Ye) + 2 * math.pi

    Tdel = Ti - Ts
    Tro = Tse + Tdel
    Lse = math.sqrt((Xe - Xs) ** 2 + (Ye - Ys) ** 2)
    Xe = Xs + math.sin(Tro) * Lse
    Ye = Ys + math.cos(Tro) * Lse

    Xdel = Xi - Xs
    Ydel = Yi - Ys

    Xe = Xe + Xdel
    Ye = Ye + Ydel
    Te = Te + Tdel

    Xj = Xe
    Yj = Ye
    Tj = Te

    return [Xj, Yj, Tj]

def Clothoid_Line(Xi, Yi, Ti, AccLen, AlinRs, AlinRe, AlinLen):
    
    def fact(n):
        result = 1
        for i in range(1, n + 1):
            result = result * i
        return result

    def Clothoid_Xc(Len, Asqu):
        iter = 10
        Xc = 0
        for i in range(iter):
            Xc += ((-1) ** i / fact(2 * i)) * (Len ** (4 * i + 1) / (4 * i + 1)) * (1 / (4 ** i * Asqu ** (2 * i)))
        return Xc

    def Clothoid_Yc(Len, Asqu):
        iter = 10
        Yc = 0
        for i in range(iter):
            Yc += ((-1) ** i / fact(2 * i + 1)) * (Len ** (4 * i + 3) / (4 * i + 3)) * (1 / (2 ** (2 * i + 1) * Asqu ** (2 * i + 1)))
        return Yc
    
    factor = True if AlinRs == 0 or abs(AlinRs) > abs(AlinRe) else False
    Rs, Re = (0, AlinRe * -1) if factor else (0, AlinRs * -1)

    Ls = 0 if Rs == 0 else (AlinLen * Re) / (Rs - Re)
    Loe = AlinLen + Ls
    Asqu = Loe * Re
    Le = AccLen + Ls if factor else Loe - AccLen

    Xo = Clothoid_Xc(Ls, Asqu)
    Yo = Clothoid_Yc(Ls, Asqu)
    sinT = math.sin(Ls ** 2 / (2 * Asqu))
    cosT = math.cos(Ls ** 2 / (2 * Asqu))
    To = math.atan2(cosT, sinT) if math.atan2(cosT, sinT) >= 0 else math.atan2(cosT, sinT) + 2 * math.pi

    Xom = Xo if factor else Xo * -1
    Yom = Yo
    Tom = To if factor else math.pi - To

    Xdel = Xi - Xom
    Ydel = Yi - Yom
    Tdel = Ti - Tom

    Xe = Clothoid_Xc(Le, Asqu)
    Ye = Clothoid_Yc(Le, Asqu)
    sinT = math.sin(Le ** 2 / (2 * Asqu))
    cosT = math.cos(Le ** 2 / (2 * Asqu))
    Te = math.atan2(cosT, sinT) if math.atan2(cosT, sinT) >= 0 else math.atan2(cosT, sinT) + 2 * math.pi

    Xem = Xe if factor else Xe * -1
    Yem = Ye
    Tem = Te if factor else math.pi - Te

    Xot = Xom + Xdel
    Yot = Yom + Ydel

    Xt = Xem + Xdel
    Yt = Yem + Ydel

    Xot_t = Xt - Xot
    Yot_t = Yt - Yot
    Tot_t = math.atan2(Xot_t, Yot_t) if math.atan2(Xot_t, Yot_t) >= 0 else math.atan2(Xot_t, Yot_t) + 2 * math.pi

    Tot_t_del = Tdel + Tot_t
    Lot = math.sqrt(Xot_t ** 2 + Yot_t ** 2)

    Xj = Xot + math.sin(Tot_t_del) * Lot
    Yj = Yot + math.cos(Tot_t_del) * Lot

    Tj = Tem + Tdel - 2 * math.pi if Tem + Tdel > 2 * math.pi else (Tem + Tdel) if Tem + Tdel >= 0 else (Tem + Tdel) + 2 * math.pi

    return [Xj, Yj, Tj]

def CubicParabola_Line(Xi, Yi, Ti, AccLen, AlinRs, AlinRe, AlinLen):

    def Newton_Raphson(Ri, Li, XLen):
        # Calculate X End coordinates using Newton-Raphson Method
        Xi = Li

        while True:
            fX = Xi + Xi ** 5 / (40 * Ri ** 2 * XLen ** 2) - Li
            fXd = 1 + Xi ** 4 / (10 * Ri ** 2 * XLen ** 2)
            Xj = Xi - fX / fXd
            if 0.9999999 < abs(Xi / Xj) < 1.0000001:
                break
            elif Xi == Xj:
                break
            else:
                Xi = Xj
        return Xj

    def Newton_Raphson_X(Ri, Li):
        # Calculate X End coordinates using Newton-Raphson Method
        Xi = Li

        while True:
            fX = Xi + Xi ** 3 / (40 * Ri ** 2) - Li
            fXd = 1 + (3 * Xi ** 2) / (40 * Ri ** 2)
            Xj = Xi - fX / fXd
            if 0.9999999 < abs(Xi / Xj) < 1.0000001:
                break
            elif Xi == Xj:
                break
            else:
                Xi = Xj
        return Xj

    factor = True if AlinRs == 0 or abs(AlinRs) > abs(AlinRe) else False
    Rx = AlinRe * -1 if AlinRs == 0 else AlinRs * -1
    Len = AccLen if factor else AlinLen - AccLen
    XLen = Newton_Raphson_X(Rx, AlinLen)
    
    if factor:
        Xo, Yo, To = 0, 0, math.pi / 2
    else:
        Xo = Newton_Raphson(Rx, AlinLen, XLen)
        Yo = (Xo ** 3) / (6 * Rx * XLen)
        sinT = math.sin(math.atan((Xo ** 2) / (2 * Rx * XLen)))
        cosT = math.cos(math.atan((Xo ** 2) / (2 * Rx * XLen)))
        To = math.atan2(cosT, sinT) if math.atan2(cosT, sinT) >= 0 else math.atan2(cosT, sinT) + (2 * math.pi)
    
    if factor:
        Xom, Yom, Tom = Xo, Yo, To 
    else:
        Xom, Yom, Tom = -Xo, Yo, math.pi - To

    Xe = Newton_Raphson(Rx, Len, XLen)
    Ye = (Xe ** 3) / (6 * Rx * XLen)
    sinT = math.sin((Xe ** 2) / (2 * Rx * XLen))
    cosT = math.cos((Xe ** 2) / (2 * Rx * XLen))
    Te = math.atan2(cosT, sinT) if math.atan2(cosT, sinT) >= 0 else math.atan2(cosT, sinT) + (2 * math.pi)

    if factor:
        Xem, Yem, Tem = Xe, Ye, Te 
    else:
        Xem, Yem, Tem = -Xe, Ye, math.pi - Te

    Xdel = Xi - Xom
    Ydel = Yi - Yom
    Tdel = Ti - Tom

    Xot = Xom + Xdel
    Yot = Yom + Ydel

    Xt = Xem + Xdel
    Yt = Yem + Ydel

    Xot_t = Xt - Xot
    Yot_t = Yt - Yot
    Tot_t = math.atan2(Xot_t, Yot_t) if math.atan2(Xot_t, Yot_t) >= 0 else math.atan2(Xot_t, Yot_t) + (2 * math.pi)

    Tot_t_del = Tdel + Tot_t
    Lot = math.sqrt(Xot_t ** 2 + Yot_t ** 2)

    Xj = Xot + math.sin(Tot_t_del) * Lot
    Yj = Yot + math.cos(Tot_t_del) * Lot
    Tj = Tem + Tdel - (2 * math.pi) if Tem + Tdel > (2 * math.pi) else Tem + Tdel if Tem + Tdel >= 0 else Tem + Tdel + (2 * math.pi)

    return [Xj, Yj, Tj]

def MidasAPI(method, command, body=None):
    base_url = "base_url_here"
    mapi_key = "your_api_key_here"

    url = base_url + command
    headers = {
        "Content-Type": "application/json",
        "MAPI-Key": mapi_key
    }

    if method == "POST":
        response = requests.post(url=url, headers=headers, json=body)
    elif method == "PUT":
        response = requests.put(url=url, headers=headers, json=body)
    elif method == "GET":
        response = requests.get(url=url, headers=headers)
    elif method == "DELETE":
        response = requests.delete(url=url, headers=headers)

    print(method, command, response.status_code)
    return response.json()

# Before using this code, please update the base_url and mapi_key in MidasAPI functions

# Alignment Data
# |Line           |Length |Radius Start/End                               |
# ------------------------------------------------------------------------
# |Straight       |L > 0  |-                                              |
# |Arc            |L > 0  |Rs ≠ 0                                         |
# |Clothoid       |L > 0  |Rs ≠ 0 and Re ≠ 0, has same sign convention    |
# |Cubic Parabola |L > 0  |(Rs ≠ 0 and Re = 0) or (Rs = 0 and Re ≠ 0)     |
alignment_data = [
    {"id":1, "linetype":"Straight", "linelength":30, "linerads":0, "linerade":0},
    {"id":2, "linetype":"Arc", "linelength":30, "linerads":100, "linerade":0},
    {"id":3, "linetype":"Clothoid", "linelength":30, "linerads":100, "linerade":30},
    {"id":4, "linetype":"Cubic Parabola", "linelength":30, "linerads":30, "linerade":0}
]

# Segment Data
# |Length           |Nb             |Structure Group Name   |Material ID        |Section ID         |
# ---------------------------------------------------------------------------------------------------
# |Real value > 0   |Integer > 0    |String less 90 char.   |Integer, 1~999,999 |Integer, 1~999,999 |
segment_data = [
    {"id":1, "seglength":1, "segNumber":120, "strgroup":"Seg1", "matlid":1, "sectid":1},
]

# Start Node and Element ID
start_node_id = 1001
start_elem_id = 1001

# Create Layout
Xi, Yi, dbNODE, dbELEM, dbSKEW, dbGRUP = CreateLayout(start_node_id, start_elem_id, alignment_data, segment_data)

# Print Coordinates
for index, value in enumerate(Xi):
    print("index = ", index+1,  "/ x = ", f'{Xi[index]:.3f}', "/ y = ",f'{Yi[index]:.3f}')

# Request to MIDAS API
resNode = MidasAPI("POST", "/db/node", dbNODE)
resElem = MidasAPI("POST", "/db/elem", dbELEM)
resSkew = MidasAPI("POST", "/db/skew", dbSKEW)
resGrup = MidasAPI("POST", "/db/grup", dbGRUP)