#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Jun 23 11:20:28 2021

@author: dave
"""

import os
from os.path import join as pjoin

import numpy as np
import pandas as pd

# from wetb.prepost import misc
from wetb.hawc2 import (HTCFile, AEFile, PCFile, StFile)

from wetb.bladed.readprj import ReadBladedProject


# TODO: move to misc?
def get_circular_inertia(d1, t, m):
    d2 = d1 - 2*t
    A = np.pi/4*(d1**2-d2**2)
    # area moment of inertia x=y
    I_g = np.pi/64*(d1**4-d2**4)
    # mass moment of inertia
    I_m = I_g/A*m
    return A, I_g, I_m


# TODO: move to HTCFile?
def add_c2_arr_htc(htc, c2_arr, body):
    """add a c2_def in the form of an array to a htc c2_def section. Needs to
    be an existing body, and the current c2_def will be overwritten.
    """
    nr_nodes = c2_arr.shape[0]
    c2_def = htc['new_htc_structure'].get_subsection_by_name(body)['c2_def']

    keys = ['sec'] + [f'sec__{i}' for i in range(2, nr_nodes+1)]

    c2_def['nsec'].values[0] = nr_nodes
    for i, key in enumerate(keys):
        # control the formatting, otherwise it looks very messy in the htc file
        # this we do by converting the values to strings
        vals_fmt = [f'{k: 15.07f}' for k in c2_arr[i,:].tolist()]
        # node nr is stripped from leading spaces, so it doesn't help adding them
        values = [i+1] + vals_fmt
        if key in c2_def:
            c2_def[key].values[:] = values[:]
        else:
            c2_def.add_line(name=key, values=values)

    return htc


# TODO: move to HTCFile?
def get_c2def(htc, body_name):
    """
    """

    # select the c2def section from the shaft
    c2_def = htc['new_htc_structure'].get_subsection_by_name(body_name)['c2_def']
    # safe all c2_def positions in one array
    c2_arr = np.zeros((len(c2_def.keys())-1,4))
    # iterate over all nsec lines, but ignore the line that gives the number of points
    for i, sec in enumerate(c2_def.keys()[1:]):
        c2_arr[i,:] = c2_def[sec].values[1:]

    return pd.DataFrame(c2_arr, columns=['x', 'y', 'z', 'twist'])


class Convert2Hawc(ReadBladedProject):
    """Based on the BLADED user manual v4.8
    """

    def __init__(self, fname):
        super().__init__(fname)

    def get_pc(self, setname_filter=None):
        # ---------------------------------------------------------------------
        # we ignore the airfoils blocks at the start of the ADAT sub-section
        # ---------------------------------------------------------------------
        # nr_airfoils = prj.get_key('ADAT', 'NFOILS')[0,0]
        # airfoil_keys = ['NFOIL', 'FTYPE', 'NTHICK', 'NREYN']
        # airfoils = {}
        # for k in range(nr_airfoils):
        #     key_app = ''
        #     if k > 0:
        #         key_app = f'__{k+1}'
        #     airfoils[f'NFOIL_{k+1}'] = {}
        #     for key in airfoil_keys:
        # ---------------------------------------------------------------------

        # total number of pc sets
        nsets = self.get_key('ADAT', 'NSETS')[0,0]
        key_app = [''] + [f'__{k}' for k in range(2, nsets+1)]

        # FIXME: are all sets always named consistantly? Meaning only the
        # thickness changes in the setname, and the rest remains the same?
        # as a work-around: ignore sets that contains setname_filter in SETNAME
        if setname_filter is not None:
            drop = []
            for i, ka in enumerate(key_app):
                setname = self.get_key('ADAT', f'SETNAME{ka}')[0,0]
                if setname.find(setname_filter) > -1:
                    drop.append(ka)
            key_app = set(key_app) - set(drop)
            # update nsets
            nsets = len(key_app)

        # initiate PCFile objects
        pc = PCFile()
        tc_arr = np.ndarray((nsets), dtype=np.float32)
        pc_lst = []

        for i, ka in enumerate(key_app):

            # and assure cm is around quarter chord point
            assert self.get_key('ADAT', 'XA')[0,0]==25

            # prj.get_key('ADAT', f'SETNAME{ka}')
            # prj.get_key('ADAT', f'SETNB{ka}')
            # prj.get_key('ADAT', f'COMMENTS{ka}')
            # prj.get_key('ADAT', f'DATE{ka}')
            # prj.get_key('ADAT', f'REYN{ka}')
            # prj.get_key('ADAT', f'DEPL{ka}')
            # prj.get_key('ADAT', f'NALPHA{ka}')
            # prj.get_key('ADAT', f'NVALS{ka}')

            tc_arr[i] = self.get_key('ADAT', f'THICK{ka}')
            shape = (self.get_key('ADAT', f'NALPHA{ka}')[0,0], 4)
            pcs = np.ndarray(shape, dtype=np.float32)
            pcs[:,0] = self.get_key('ADAT', f'ALPHA{ka}')[0,:]
            pcs[:,1] = self.get_key('ADAT', f'CL{ka}')[0,:]
            pcs[:,2] = self.get_key('ADAT', f'CD{ka}')[0,:]
            pcs[:,3] = self.get_key('ADAT', f'CM{ka}')[0,:]
            pc_lst.append(pcs)

        # in BGEOM the airfoil number is referred to and not the thickness
        # sort properly on thickness
        isort = tc_arr.argsort()
        pc.pc_sets = {1:(tc_arr[isort], [pc_lst[k] for k in isort])}

        return pc

    def get_ae(self):
        # BGEOMMB defines for each element the start and end point. We only
        # need half that of course.
        chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2]
        # FIXME: is it radius or curved lenght
        radius = self.get_key('BGEOMMB', 'RJ')[0,0::2]
        thickness = self.get_key('BGEOMMB', 'BTHICK')[0,0::2]
        # FIXME: multiple pc sets can be defined but are ignored for now.
        pc_set_id = np.ones(chord.shape)

        ae = AEFile()
        ae.add_set(radius, chord, thickness, pc_set_id, set_id=None)

        return ae

    def get_blade_c2_def(self):

        # BLADENAME
        # DISTMODE	RJ
        # NBE	100
        # FOIL
        # MOVING

        # CE_X
        # CE_Y
        diam = self.get_key('RCON', 'DIAM')[0,0]
        cone = self.get_key('RCON', 'CONE')[0,0]
        tilt = self.get_key('RCON', 'TILT')[0,0]

        # in BLADED all is wrt LE, so we need chord
        chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2]

        # c2_def = htc['new_htc_structure'].get_subsection_by_name('blade1')['c2_def']
        twist = self.get_key('BGEOMMB', 'TWIST')[0,0::2]

        # FIXME: what is the difference between RJ and DIST?
        # radius can be given eiter as curved length or radius
        # Distance: This can be entered as a distance along the blade or as a
        # distance along the blade root Z-axis. Select the appropriate option
        # at the base of the screen.
        dist = self.get_key('BGEOMMB', 'DIST')[0,0::2]
        rj = self.get_key('BGEOMMB', 'RJ')[0,0::2]
        # reference frame is EC, that seems to be indicated on the plots of the
        # blade rotor coordinates/pitch/axis tc (fig 4-1 and 4-2)
        ref_x = self.get_key('BGEOMMB', 'REF_X')[0,0::2]
        ref_y = self.get_key('BGEOMMB', 'REF_Y')[0,0::2]
        # EC wrt to LE in percentage of chord
        ce_x = self.get_key('BGEOMMB', 'CE_X')[0,0::2]
        ce_y = self.get_key('BGEOMMB', 'CE_Y')[0,0::2]

        # HAWC2 variables
        theta = twist*-1
        # theta_d = theta*180/np.pi
        ea_x = (50-ce_y)*0.01*chord
        ea_y = ce_x*0.01*chord

        # safe all c2_def positions in one array
        c2_arr = np.zeros((len(twist),4))
        c2_arr[:,0] = -1*(ref_y + ((ea_x*np.cos(theta) - ea_y*np.sin(theta))))
        c2_arr[:,1] =    (ref_x - ((ea_x*np.sin(theta) + ea_y*np.cos(theta))))
        # assume DIST is curved length
        c2_arr[:,2] = rj #dist
        c2_arr[:,3] = theta*180/np.pi

        return c2_arr

    def get_blade_st(self):
        # BMASSMB
        # BSTIFFMB

        # in HAWC2 structural pitch is relative to the twist
        twist = self.get_key('BGEOMMB', 'TWIST')[0,0::2]
        dist = self.get_key('BGEOMMB', 'DIST')[0,0::2]
        chord = self.get_key('BGEOMMB', 'CHORD')[0,0::2]
        thickness = self.get_key('BGEOMMB', 'BTHICK')[0,0::2]

        # CG location, in percentage chord wrt LE
        cm_x = self.get_key('BMASSMB', 'CM_X')[0,0::2]
        cm_y = self.get_key('BMASSMB', 'CM_Y')[0,0::2]
        mass = self.get_key('BMASSMB', 'MASS')[0,0::2]
        siner = self.get_key('BMASSMB', 'SINER')[0,0::2]

        # Radii of gyration ratio: the radius of gyration of mass about y_m
        # divided by the radius of gyration of mass about x_m. This defaults to
        # the relative profile thickness but can be over-written by un-checking
        # the “Use default radii of gyration ratio” checkbox.
        rgratio = self.get_key('BMASSMB', 'RGRATIO')[0,0::2]
        # FIXME: DEF_RGRATIO but which value belongs to which option?
        # assume 0 means "default is off"
        def_rgratio = self.get_key('BMASSMB', 'DEF_RGRATIO')[0,0]

        # FIXME: BETA_M: figure 3-2 from v4.8: mass axis orientation, radians?
        beta_m = self.get_key('BMASSMB', 'BETA_M')[0,0::2]
        # Mass axis orientation: the orientation of the principle axis of inertia.
        # This defaults to the orientation of aerodynamic twist, but can be
        # over-written by un-checking the “Use default mass axis orientation”
        # checkbox. (See diagram below)
        # FIXME: DEF_BETA_M but which value belongs to which option?
        # assume 0 means "default is off"
        def_beta_m = self.get_key('BMASSMB', 'DEF_BETA_M')[0,0]

        # DEF_RGRATIO	0
        # DEF_BETA_M	0

        # RADPOS
        # PITCHPOS

        # DEF_RGRATIO	0
        # DEF_BETA_M	0
        # ICEDBLADES	 0, 0, 0
        # ICEDENSITY	 700
        # TIPCHORD	 0
        # NPOINT	3
        # RADPOS	 .2, 30.12345, 40.12345
        # PITCHPOS	 .2, 30.1, 40.1
        # PM_X	 0, 0, 0
        # PM_Y	 20, 10, 20
        # PMASS	 1000, 30, 1

        # elastic axis, in percentage chord wrt LE
        ce_x = self.get_key('BGEOMMB', 'CE_X')[0,0::2]
        ce_y = self.get_key('BGEOMMB', 'CE_Y')[0,0::2]

        # 3.5 Blade stiffness distribution (page 19)
        # The stiffness must be defined about the principal axis of inertia at
        # each blade station (see 3.1). The stiffness is the product of Young’s
        # Modulus for the material and the second moment of area for the xp or
        # yp directions as appropriate. The principal axis orientation is defined
        # as an input, and defaults to the aerodynamic twist. In this case it is
        # assumed to be parallel and perpendicular to the chord line. If the
        # principal axis orientation is different from the aerodynamic twist, click
        # the Use default principal axis orientation to off. (see diagram below)
        eiflap = self.get_key('BSTIFFMB', 'EIFLAP')[0,0::2]
        eiedge = self.get_key('BSTIFFMB', 'EIEDGE')[0,0::2]
        beta_s = self.get_key('BSTIFFMB', 'BETA_S')[0,0::2]
        gj = self.get_key('BSTIFFMB', 'GJ')[0,0::2]
        cs_x = self.get_key('BSTIFFMB', 'CS_X')[0,0::2]
        cs_y = self.get_key('BSTIFFMB', 'CS_Y')[0,0::2]
        gaflap = self.get_key('BSTIFFMB', 'GAFLAP')[0,0::2]
        gaedge = self.get_key('BSTIFFMB', 'GAEDGE')[0,0::2]
        # FIXME: DEF_BETA_S probably says what the frame of reference is
        # assume 0 means "default is off"
        # self.get_key('BMASSMB', 'DEF_BETA_S')[0,0::2]

        # for plotting purposes
        sel = beta_m < -1
        beta_m[sel] += np.pi
        sel = beta_s < -1
        beta_s[sel] += np.pi

        # calculate the curved length
        # c2_arr = self.get_blade_c2_def()
        # curved_len_arr = curved_length(c2_arr)

        ea_x = (50-ce_y)*0.01*chord
        ea_y = ce_x*0.01*chord
        cg_x = (50-cm_y)*0.01*chord
        cg_y = cm_x*0.01*chord

        # mass moments
        Ipm_y = siner + (mass*(cg_x-ea_x)**2)
        ri_y = np.sqrt( (Ipm_y / (1+rgratio**2)) / mass )
        ri_x = rgratio * ri_y
        # in BLADED rgratio = y/x, but in HAWC2 it x=y so we swap them

        # just choose E, G so the rest follows
        E, G = 1e10, 1e09
        # however, we don't know EA (probably because stiff?), so assume A is
        # a full box based on chord length and maximum airfoil thickness
        # this will make the extensional stiffness EA quite large, but the user
        # would still end up with a physical meaningful value
        A = chord*chord*thickness/100

        starr = np.ndarray((len(dist),19))
        starr[:,0] = dist
        starr[:,1] = mass
        starr[:,2] = cg_x
        starr[:,3] = cg_y
        # radius of gyration
        starr[:,4] = ri_x
        starr[:,5] = ri_y
        # shear center
        starr[:,6] = (50-cs_y)*0.01*chord
        starr[:,7] = cs_x*0.01*chord
        # E, G: choose
        starr[:,8] = E
        starr[:,9] = G
        starr[:,10] = eiflap/E
        starr[:,11] = eiedge/E
        starr[:,12] = gj/G
        # shear factor
        starr[:,13] = gaflap/(G*A)
        starr[:,14] = gaedge/(G*A)
        starr[:,15] = A
        starr[:,16] = -1*(beta_s - twist) # structural pitch
        starr[:,17] = ea_x
        starr[:,18] = ea_y

        st = StFile()
        st.main_data_sets = {1:{1:starr}}
        # st.cols = wetb.hawc2.st_file.stc.split()
        return st

    def get_tower_c2_def_st(self):
        """Ignores nodes that are not at the centerline

        Returns
        -------
        c2_arr : TYPE
            DESCRIPTION.
        st : TYPE
            DESCRIPTION.

        """

        t_n_e = int(self.get_key('TGEOM', 'NTE'))
        t_n_n = int(self.get_key('TGEOM', 'NTS'))
        t_el_mat = self.get_key('TGEOM', 'ELSTNS')
        t_n_coord = self.get_key('TGEOM', 'TCLOCAL')
        # tdiam = self.get_key('TGEOM', 'SEAL')
        # tdiam = self.get_key('TGEOM', 'HYDRO')
        towmgt = self.get_key('TGEOM', 'TOWMGT')
        # MATERIAL, NOMATS
        material = self.get_key('TMASS', 'MATERIAL')[0,0::2]
        nomats = self.get_key('TMASS', 'NOMATS')

        # Index of tower nodes (nodes at zero x-y positions)
        t_a = np.where((abs(t_n_coord[:,0])+abs(t_n_coord[:,1])) == 0)[0]
        t_nodes = t_n_coord[t_a]
        # sort index according to z position of the nodes
        n_ind = t_a[np.argsort(t_nodes[:,2])]
        t_nodes = t_n_coord[n_ind]
        # Index for tower elemetns
        # Elemet indexes which does not contain both tower nodes
        ind_not = [i for i,j in enumerate(t_el_mat) if not (j[0]-1) in n_ind
                   or not (j[1]-1) in n_ind]
        # The elemetn indexes excluding the ind_not
        ind_init = np.array([i for i in range(t_el_mat.shape[0]) if not i in ind_not])
        sort_el = []
        for i in n_ind[:-1]:
            sort_el.append(np.where(t_el_mat[ind_init][:,0] == i+1)[0][0])
        ind_el = ind_init[sort_el]

        # safe all c2_def positions in one array
        c2_arr = np.zeros((t_nodes.shape[0],4))
        c2_arr[:,0:2] = t_nodes[:,:-1].copy()
        c2_arr[:,2] = -t_nodes[:,-1] + t_nodes[0,-1] # It is minus for HAWC2 model
        c2_arr[:,3] = 0.0
        # element properties
        t_d =  self.get_key('TGEOM', 'TDIAM')[ind_el]
        material = self.get_key('TMASS', 'MATERIAL')[0,0::2][ind_el]
        t_thick = self.get_key('TMASS', 'WALLTHICK')[ind_el]
        towmgt = self.get_key('TGEOM', 'TOWMGT')[ind_el]
        # Mass
        t_m = self.get_key('TMASS', 'TOWM')[ind_el]
        t_p_m = self.get_key('TMASS', 'TOWPMI')[ind_el] # point mass for elements and nodes
        t_flood = self.get_key('TMASS', 'FLOODED')[ind_el]
        # material props from prj file
        name_mat = np.unique(material)
        mat_prop = {}
        for i in name_mat:
            mat_prop[i] = self.get_key('TMASS', "%s"%i) # rho, E, G

        t_mat = np.array([mat_prop[i][0] for i in material])

        if np.sum(t_thick[1:,0] - t_thick[:-1,1]) > 1e-9:
            print('Diameters are not continous along the tower')
        if (name_mat.shape[0] > 1):
            print('There are %i materials in tower!' %name_mat.shape[0])
        d = np.zeros(t_d.shape[0]+1)
        t = np.zeros(t_thick.shape[0]+1)
        mat = np.zeros((t_mat.shape[0]+1,t_mat.shape[1]))
        d[1:] = t_d[:,1]
        t[1:] = t_thick[:,1]
        mat[1:,:] = t_mat.copy()
        d[0] = t_d[0,1]
        t[0] = t_thick[0,1]
        mat[0,:] = t_mat[0,:].copy()

        A, I_g, I_m = get_circular_inertia(d, t, mat[:,0])
        # FIXME: MAYBE ANOTHER FIX FOR DIFFERENT MATERIALS
        # return c2_arr
        starr = np.zeros((c2_arr.shape[0],19))
        starr[:,0] = -c2_arr[:,2] #
        starr[:,1] = A*mat[:,0]
        starr[:,2] = 0.0 # no cg offset
        starr[:,3] = 0.0 # no cg offset
        # radius of gyration
        starr[:,4] = np.sqrt(I_m/mat[:,0]) # ri_x = (I_m/m)**0.5 = (I_g/A)**0.5
        starr[:,5] = np.sqrt(I_m/mat[:,0]) # ri_y = (I_m/m)**0.5 = (I_g/A)**0.5
        # shear center
        starr[:,6] = 0.0 # no shear center offset
        starr[:,7] = 0.0 # no shear center offset
        # E, G: choose
        starr[:,8] = mat[:,1]
        starr[:,9] = mat[:,2]
        starr[:,10] = I_g
        starr[:,11] = I_g
        starr[:,12] = I_g*2
        # shear factor
        starr[:,13] = 3/4 # for circular section check it again
        starr[:,14] = 3/4 # for circular section check it again
        starr[:,15] = A
        starr[:,16] = 0.0 # structural pitch
        starr[:,17] = 0.0
        starr[:,18] = 0.0

        st = StFile()
        st.main_data_sets = {1:{1:starr}}

        return c2_arr, st

    def get_hub(self):
        # Blade root length, in blade root coordinates (with tilt and cone)
        # see figure 4-1 in Blade User Manual 4.8
        root = self.get_key('RCON', 'ROOT')[0,0]

        # Hub mass: the mass of the hub, including the spinner and any blade
        #   root section.
        # Hub mass centre: the distance from the intersection of the shaft and
        #   blade axes to the centre of mass of the hub, in a direction measured
        #   away from the tower.
        # Moments of inertia: the moment of inertia of the hub mass about the
        #   shaft axis must be defined.
        # The inertia about an axis perpendicular to the shaft may also be
        #   entered with its origin about the hub centre of mass.

        hubmas = self.get_key('RMASS', 'HUBMAS')[0,0]
        hubine = self.get_key('RMASS', 'HUBINE')[0,0]
        # FIXME: which axis is this? Would that translate to both xx and yy in H2?
        hubine2 = self.get_key('RMASS', 'HUBINE2')[0,0]

        # Enter the Spinner diameter. This is the diameter of any spinner or
        # nose-cone, within which the blades themselves experience no
        # aerodynamic forces.
        spind = self.get_key('RCON', 'SPIND')[0,0]

        cmass_hub = {'x':0, 'y':0, 'z':0, 'm':hubmas,
                     'Ixx':0, 'Iyy':0, 'Izz':hubine}

        return cmass_hub, root

    def get_drivertain(self, len_shaft):

        # TODO: brake
        # MSTART BRAKE

        # DTRAIN
        # dtrain = self.get_key('DTRAIN')
        ginert = self.get_key('DTRAIN', 'GINERT')[0,0] # generator inertia
        gratio = self.get_key('DTRAIN', 'GRATIO')[0,0]
        # The additional inertia of the high speed shaft may also be specified
        # along with the inertia of the gearbox which is referred to the HSS.
        gbxinert = self.get_key('DTRAIN', 'GBXINERT')[0,0] # gearbox inertia
        # FIXME: brake position?
        bpos = self.get_key('DTRAIN', 'BPOS')[0,0]
        # LSS seems to have a DOF indicator in LSSDOF, and has 6 elements
        # 4th element is torsion it seems
        klss = self.get_key('DTRAIN', 'KLSS')[0,3]
        # FIXME: what does this damping value mean? Simply C in the dyn system
        # theta_dt_dt*I + C*theta_dt + K*theta = 0 ?
        dlss = self.get_key('DTRAIN', 'DLSS')[0,3]
        # HSS only has one DOF
        khss = self.get_key('DTRAIN', 'KHSS')[0,0]
        dhss = self.get_key('DTRAIN', 'DHSS')[0,0]

        # Convert torsional stiffness definition into a beam stiffness
        # that applies for the considered shaft length (c2_def)
        G = 80e9 # typical steel value
        Ip = klss*len_shaft/G
        starr = np.zeros((2,19))
        starr[:,0] = [0, len_shaft] # st file is always normalised
        starr[:,1] = 0.5 # very light but not too light
        starr[:,2] = 0.0 # no cg offset
        starr[:,3] = 0.0 # no cg offset
        # radius of gyration: very light but not too light
        starr[:,4] = 0.001 #ri_x
        starr[:,5] = 0.001 #ri_y
        # shear center
        starr[:,6] = 0.0 # no shear center offset
        starr[:,7] = 0.0 # no shear center offset
        # E, G: choose
        starr[:,8] = 1e18
        starr[:,9] = G
        starr[:,10] = Ip/2 # area inertia's
        starr[:,11] = Ip/2
        starr[:,12] = Ip
        # shear factor
        starr[:,13] = 1000 # stiff in shear
        starr[:,14] = 1000
        starr[:,15] = 10 # cross-section area
        starr[:,16] = 0.0 # structural pitch
        starr[:,17] = 0.0 # elastic axis
        starr[:,18] = 0.0
        st = StFile()
        st.main_data_sets = {1:{1:starr}}

        # FIXME: INERTIATOCLUTCH??
        inertiatoclutch = self.get_key('DTRAIN', 'INERTIATOCLUTCH')

        # FIXME: what is the location of these inertia's?
        # FIXME: what is the mass? It seems the mass is put elsewhere?

        # cmass_gbx = {'Izz':Izz}
        # cmass_gen = {'x':x, 'y':y, 'z':z, 'm':nacmas,
        #              'Ixx':Ixx, 'Iyy':Iyy, 'Izz':Izz}

        cmass_lss = {'x':0, 'y':0, 'z':0, 'm':0,'Ixx':0, 'Iyy':0, 'Izz':ginert}
        # convert HSS inertia to LSS
        cmass_hss = {'x':0, 'y':0, 'z':0, 'm':0,'Ixx':0, 'Iyy':0,
                     'Izz':gbxinert*gratio*gratio}

        return cmass_lss, cmass_hss, st

    def get_nacelle(self):

        # also get the nacell concentrated mass
        nacmas = self.get_key('NMASS', 'NACMAS')[0,0]
        # includes the nacelle structure and all the machinery within it.
        # It does not include the rotor blades and hub. If the Direct Drive
        # (see 4.1) option is selected, the mass of the generator (see 4.2)
        # is also excluded.

        # position of cg relative to tower top, nacelle mass coord system
        # based on figure 4-7 of bladed manual 4.8
        # FIXME: is this correct? z means SS, and at the edge of the nacelle?
        nmx = self.get_key('NMASS', 'NMX')[0,0] # width?
        nmy = self.get_key('NMASS', 'NMY')[0,0] # length?
        nmz = self.get_key('NMASS', 'NMZ')[0,0] # height?
        iyaw = self.get_key('NMASS', 'IYAW')[0,0]
        inod = self.get_key('NMASS', 'INOD')[0,0]
        iroll = self.get_key('NMASS', 'IROLL')[0,0]

        # MSTART NGEOM
        # NACW	 x.000
        # NACL	 x.000
        # NACH	 x.000
        # NACZ	 0
        # NACCD	 x.0
        # NAERO	N

        # height of the nacelle/towertop: the BLADED model does not have to
        # have consistent shaft length with tilt angle arriving at the rotor
        # center, so adjust towertop length in HAWC2 accordingly

        # Tower height: from the ground or sea surface to the yaw bearing
        # (only needed if the tower itself has not been defined)
        towht = self.get_key('RCON', 'TOWHT')[0,0]

        # Hub height: from the ground to the centre of the rotor
        # (i.e. the intersection of the blade and shaft axes)
        height = self.get_key('RCON', 'HEIGHT')[0,0]

        # horizontal distance between rotor center and tower center line
        ovrhng = self.get_key('RCON', 'OVRHNG')[0,0]
        tilt = self.get_key('RCON', 'TILT')[0,0]

        # FOR HAWC2, WE HAVE
        len_shaft = ovrhng / np.cos(tilt)
        len_towertop = height - towht - len_shaft*np.sin(tilt)
        assert len_towertop > 0
        # assert len_towertop + len_shaft*np.sin(tilt) + towht - height == 0

        # concentrated mass in HAWC2
        # 1  : nodenr
        # 2-4: x, y, z offset
        # 5  : mass
        # 6-8: Ixx, Iyy, Izz
        # 9-11: Ixy, Ixz, Iyz (optional)
        # in HAWC2 we would be in the tower top coordinate system, and is
        # usually the same as the global coordinate system
        x = nmx
        y = -nmy # move upwind wrt tower
        z = -nmz # move upward
        Ixx = inod
        Iyy = iroll
        Izz = iyaw

        cmass_nacelle = {'x':x, 'y':y, 'z':z, 'm':nacmas,
                         'Ixx':Ixx, 'Iyy':Iyy, 'Izz':Izz}

        return cmass_nacelle, len_shaft, len_towertop

    def get_towershadow_drag(self):
        # TODO: tower shadow and tower aerodrag

        # MSTART TOWSDW
        # TSMODEL	x
        # TDCORR	 x
        # MAXDEF	 x.x
        # WIDTH	 x
        # LREF	 x.x

        return

    def get_control(self):

        # MSTART CONTROL

        # MSTART PCOEFF
        # TSRMIN	 x
        # TSRMAX	 x
        # TSRSTP	 x.x
        # PITCH	-x.0E-00
        # PITCH_END	-x.0E-00
        # PITCH_STEP	 0
        # OMEGA	 x.654321

        # MSTART IDLING

        # AEROINFO

        # pitch actuator rate limits
        pitch_actuator = self.xmlroot.PitchActuator
        if pitch_actuator.SetpointTrajectory.HasRateLimits:
            # pitch_actuator.SetpointTrajectory.LowerRateLimit
            # pitch_actuator.SetpointTrajectory.UpperRateLimit
            print(pitch_actuator.SetpointTrajectory.UpperRateLimit*30/np.pi)

        if pitch_actuator.SetpointTrajectory.HasAccelerationLimits:
            # pitch_actuator.SetpointTrajectory.LowerAccelerationLimit
            # pitch_actuator.SetpointTrajectory.UpperAccelerationLimit
            print(pitch_actuator.SetpointTrajectory.UpperAccelerationLimit*30/np.pi)

        # pitch_actuator.PositionResponse.values() # 1st or 2nd order
        # pitch_actuator.PositionResponse.Frequency
        # pitch_actuator.PositionResponse.Damping

        # pitch_actuator.RateResponse.values() # 1st or 2nd order
        # pitch_actuator.RateResponse.LagTime
        # pitch_actuator.PositionResponse.Damping

        # and more: bearing friction, etc

        return

    def convert(self, fname_htc):

        fname_prj = os.path.basename(fname_htc).replace('.htc', '')

        # for convenience, we start from an htc template that otherwise has the
        # right structure, but has only 2 nodes for each body.
        # assume it is in the same folder as where the HTC will be written to
        basepath = os.path.dirname(fname_htc)
        htc = HTCFile(pjoin(basepath, 'template.htc'))

        # prj = self.PRJ_to_HAWC(pjoin(version, fname_prj))
        # plot_bladed(prj)

        # cone = prj.get_key('RCON', 'CONE')
        # Lateral offset: the horizontal offset between the shaft and tower axes.

        # Brake
        # steps = prj.get_key('BRAKE', 'STEPS')
        # TORQUE = prj.get_key('BRAKE', 'TORQUE')
        # TIME = prj.get_key('BRAKE', 'TIME')

        # -------------------------------------------------------------------------
        # BLADES
        # extract blade geometry, and add to c2_def section in the htc file
        c2_blade = self.get_blade_c2_def()
        htc = add_c2_arr_htc(htc, c2_blade, 'blade1')

        # -------------------------------------------------------------------------
        # NACELLE
        cmass_nacelle, len_shaft, len_towertop = self.get_nacelle()
        # set towertop length
        # nacelle = htc['new_htc_structure'].get_subsection_by_name('towertop')
        c2_tt = np.zeros((2,4))
        c2_tt[1,2] = -len_towertop
        htc = add_c2_arr_htc(htc, c2_tt, 'towertop')

        # -------------------------------------------------------------------------
        # attach nacelle mass/inertia to towertop, first node (yaw bearing)
        towertop = htc['new_htc_structure'].get_subsection_by_name('towertop')
        values = [v for k,v in cmass_nacelle.items()]
        cmass = towertop.add_line('concentrated_mass', [1] + values)
        key = cmass.location().split('/')[-1]
        towertop[key].comments = 'nacelle mass (inc rotating) and inertia (non-rotating)'

        # -------------------------------------------------------------------------
        # set shaft length
        c2_s = np.zeros((2,4))
        c2_s[1,2] = len_shaft
        htc = add_c2_arr_htc(htc, c2_s, 'shaft')

        # -------------------------------------------------------------------------
        # generator and gearbox inertia's, shaft torsional flexibility
        cmass_lss, cmass_hss, st_shaft = self.get_drivertain(len_shaft)
        st_shaft.save(pjoin(basepath, f'{fname_prj}_shaft.st'))

        shaft = htc['new_htc_structure'].get_subsection_by_name('shaft')
        # LSS inertia
        values = [v for k,v in cmass_lss.items()]
        cmass = shaft.add_line('concentrated_mass', [2] + values)
        key = cmass.location().split('/')[-1]
        shaft[key].comments = 'inertia LSS'
        # Inertia of the HSS is already expressed wrt LSS
        values = [v for k,v in cmass_hss.items()]
        cmass = shaft.add_line('concentrated_mass', [2] + values)
        gratio = self.get_key('DTRAIN', 'GRATIO')[0,0]
        key = cmass.location().split('/')[-1]
        shaft[key].comments = f'inertia HSS, expressed in LSS, GBR={gratio}'

        # -------------------------------------------------------------------------
        # set tilt angle
        tilt = self.get_key('RCON', 'TILT')[0,0]
        ori = htc['new_htc_structure'].orientation
        # tilt is on the 2nd relative block, and set angle on 2nd set of eulerang
        # check comments of the htc file:
        # print(ori['relative__2']['mbdy2_eulerang__2'].comments)
        ori['relative__2']['mbdy2_eulerang__2'].values = [tilt*180/np.pi, 0, 0]
        ori['relative__2']['mbdy2_eulerang__2'].comments = 'tilt angle'

        # coning angle
        cone = self.get_key('RCON', 'CONE')[0,0]
        # coning is on 3, 4 and 5 (all hub orientations)
        for k in range(3,6):
            # print(ori[f'relative__{k}']['mbdy2_eulerang__3'].comments)
            ori[f'relative__{k}']['mbdy2_eulerang__3'].values = [-cone*180/np.pi, 0, 0]
            ori[f'relative__{k}']['mbdy2_eulerang__3'].comments = 'cone angle'

        # TODO: blade mounting sweep and cone as well??

        # -------------------------------------------------------------------------
        # HUB mass as concentrated mass on the shaft end
        # FIXME: make sure we can get the hub root loads including these inertia's
        cmass_hub, hublen = self.get_hub()
        values = [v for k,v in cmass_hub.items()]
        cmass = shaft.add_line('concentrated_mass', [2] + values)
        key = cmass.location().split('/')[-1]
        shaft[key].comments = 'Hub inertia and mass'

        # -------------------------------------------------------------------------
        # hub lenght
        c2_hub = np.zeros((2,4))
        c2_hub[1,2] = hublen
        htc = add_c2_arr_htc(htc, c2_hub, 'hub1')

        # -------------------------------------------------------------------------
        # TOWER ST FILE and C2_DEF section
        # WARNING: only includes nodes that are at centerline, side-elements are
        # ignored
        # TODO: compare between TGEOM/TMASS and TSTIFF approaches, it seems that
        # the shear stiffness is not the same, see check_tower()
        c2_tow, st = self.get_tower_c2_def_st()
        st.save(pjoin(basepath, f'{fname_prj}_tower.st'))
        htc = add_c2_arr_htc(htc, c2_tow, 'tower')

        # hub height
        htc.wind.center_pos0.values = [0, 0, -self.get_key('RCON', 'HEIGHT')[0,0]]

        # -------------------------------------------------------------------------
        # set all file names correct
        tower = htc['new_htc_structure'].get_subsection_by_name('tower')
        tower.timoschenko_input.filename.values[0] = f'data/{fname_prj}_tower.st'

        shaft = htc['new_htc_structure'].get_subsection_by_name('shaft')
        shaft.timoschenko_input.filename.values[0] = f'data/{fname_prj}_shaft.st'

        blade1 = htc['new_htc_structure'].get_subsection_by_name('blade1')
        blade1.timoschenko_input.filename.values[0] = f'data/{fname_prj}_blade.st'

        for body_name in ['towertop', 'hub1']:
            body = htc['new_htc_structure'].get_subsection_by_name(body_name)
            body.timoschenko_input.filename.values[0] = 'data/template.st'

        htc['aero'].ae_filename.values[0] = f'data/{fname_prj}.ae'
        htc['aero'].pc_filename.values[0] = f'data/{fname_prj}.pc'

        # -------------------------------------------------------------------------
        # SAVE HTC FILE
        htc.save(pjoin(basepath, f'{fname_htc}'))

        # -------------------------------------------------------------------------
        # other data files

        # Blade st file
        st = self.get_blade_st()
        st.save(pjoin(basepath, f'{fname_prj}_blade.st'))

        # extract profile coefficients and save to pc file
        pc = self.get_pc(setname_filter='cleanVG')
        pc.save(pjoin(basepath, f'{fname_prj}.pc'))

        # extract aerodynamic layout, and safe to ae file
        ae = self.get_ae()
        ae.save(pjoin(basepath, f'{fname_prj}.ae'))

        # # BLADE c2_def
        # c2_def = htc['new_htc_structure'].get_subsection_by_name('blade1')['c2_def']
        # # iterate over all nsec lines, but ignore the line that gives the number of points
        # for i, sec in enumerate(c2_def.keys()[1:]):
        #     c2_arr[i,:] = c2_def[sec].values[1:]

        # curvedlength = np.zeros(len(c2_arr[:,2]))
        # for i in range(1,len(c2_arr[:,2])):
        #     curvedlength[i]=curvedlength[i-1]+np.linalg.norm(c2_arr[i,0:3]-c2_arr[i-1,0:3])

        # c2_arr = np.zeros((len(c2_def.keys())-1,4))
        # # iterate over all nsec lines, but ignore the line that gives the number of points
        # for i, sec in enumerate(c2_def.keys()[1:]):
        #     c2_arr[i,:] = c2_def[sec].values[1:]
