import sys
sys.dont_write_bytecode = True

import math
from adcptool.vector import Vector 

#
# handy functions
#

def compute_direction_angle(vec):
    ''' compute the direction angle based on direction vector vector 
    '''
    
    # two points --> direction angle
    dx = vec.x
    dy = vec.y

    # handling the quadrants
    if dy == 0 and dx == 0:
#        raise NotImplementedError('zero length vectors dont have any direction')
        return 0
    if dy == 0 and dx > 0:
        # "0"
        return 0.5 * math.pi
    elif dy == 0 and dx < 0:
        # "0"
        return 1.5 * math.pi
    elif dy < 0 and dx < 0:
        # II + III
        return math.atan(dx/dy) + math.pi
    elif dy < 0 and dx > 0:
        # IV
        return math.atan(dx/dy) + math.pi
    else:
        # I
        return math.atan(dx/dy)
    
def unitconversion(unit,p):
    ''' return the conversion factor from [unit]^p to [meters]^p
    '''
    # factors from X to meter
    d = dict(cm=0.01, ft=0.3048)
    return math.pow(d[unit],p)

def print_type(o):
    ''' print out the type of the given thing. useful for debugging.
    '''
    print(type(o).__name__)


#
# the actual objects
#

class ProcessedProfileObj():
    ''' holds a stack of finished (processed) ensembles
        (and some extra data)
    '''

    #
    # methods needed for defining the profile itself
    def compute_direction_vector(self, a, b=False):
        if b != False:
            # two vectors --> direction vector
            return (b - a).norm()
        else:
            # angle --> direction vector
            return Vector(math.cos(a), math.sin(a))


    def compute_direction_angle(self, vec):
        # two points --> direction angle
        dx = vec.x
        dy = vec.y
    
        # handling the quadrants
        if dy == 0 and dx > 0:
            # "0"
            return 0.5 * math.pi
        elif dy == 0 and dx < 0:
            # "0"
            return 1.5 * math.pi
        elif dy < 0 and dx < 0:
            # II + III
            return math.atan(dx/dy) + math.pi
        elif dy < 0 and dx > 0:
            # IV
            return math.atan(dx/dy) + math.pi
        else:
            # I
            return math.atan(dx/dy)

    def update_velocities(self, vm=None, vgm=None):
        ''' write back modified velocities into this object.
        
        arguments:
        vm ...    velocity matrix. a 3d numpy array 
        vgm ...    good value matrix belonging to vm. boolean numpy array, True if value is good, bad otherwise. 
                should match the masking in this object. 
        '''
        
        # if (vm.all() == None) or (vgm.all() ==  None):
        if (vm.all() == None) | (vgm.all() ==  None):
            # only update vepth averaged velocities.
            for i in range(len(self.ensembles)):
                if not self.ensembles[i].void: 
                    self.ensembles[i].velocity = self.ensembles[i].comp_avg_velocity()
            
        else: 
            (rows, columns, dimension) = vm.shape
            
            # velocities in cells
            for i in range(columns):
                if not self.ensembles[i].void: # for each ensemble
                    for r in range(len(self.ensembles[i].cells)):
                        self.ensembles[i].cells[r].velocity = Vector(vm[r,i].tolist())
            
                    # depth averaged velocities in ensembles
                    self.ensembles[i].velocity = self.ensembles[i].comp_avg_velocity()
            
        
        
    def __init__(self, rawprofile, processing_settings, global_profile_position = False):
        ''' object to store the processed ADCP data.

            attributes for this object:
                self.start_dir            direction of the profile, given in [rad], starting from north, clockwise
                self.start_dir_vec        unit vector version of the above, in X/Y coordinates
                self.start_pos            Vector() in X/Y/Z where the profile starts
                self.start_offset        distance from the first ensemble to the start_pos
                                        (its a Vector() too; useful if there is no start_dir given and when computing absolute coordinates)
                self.ensembles            list where the ProcessedEnsembleObj()'s will be stored
        '''
        
        self._cfg = processing_settings # saved for later usage
        
        #
        # define the profile which will eventually be used

        if global_profile_position != False:
            self.start_pos = global_profile_position['start']


            if 'offset' in global_profile_position:
                self.start_offset = global_profile_position['offset']
            else:
                self.start_offset =  Vector(0,0)


            if 'dir' in global_profile_position:
                # angle is defined, compute direction vector
                self.start_dir     = global_profile_position['dir']
                self.start_dir_vec = self.compute_direction_vector(self.start_dir)

            elif 'end' in global_profile_position:
                # 2nd point was given, compute angle and direction vector
                self.start_dir_vec = self.compute_direction_vector(global_profile_position['start'], global_profile_position['end'])
                self.start_dir     = self.compute_direction_angle(self.start_dir_vec)

            else:
                # no hint was given, use last ensemble as 2nd point and then do the same as above
                endpos_n = rawprofile.ensembles[len(rawprofile.ensembles)-1].total_distance_traveled_north
                endpos_e = rawprofile.ensembles[len(rawprofile.ensembles)-1].total_distance_traveled_east

                self.start_dir_vec = (Vector(endpos_e, endpos_n) - self.start_offset).norm()
                self.start_dir     = self.compute_direction_angle(self.start_dir_vec)


        #
        # process ensembles

        self.ensembles = []

        for ensemble in rawprofile.ensembles:
            self.ensembles.append(ProcessedEnsembleObj(ensemble, self.start_pos, self.start_dir, self.start_dir_vec, self.start_offset, processing_settings))
            
        self.cell_height = rawprofile.depth_cell_length * 0.01 # in the ascii file its always in cm, here its always in m, so this can be hard coded.


class ProcessedEnsembleObj():
    '''
    object to hold the finished (processed) ensemble, ready for the output

    attributes:
        self.velocity    a Vector() that holds the velocity in U/V coordinates
        sel.position    a Vector() that holds the position of the ensemble in X/Y/Z
        (currently we have self.position_{a,b,c} for different methods to store the things)
    '''



    def comp_avg_velocity(self, method=0):
        ''' compute and return depth averaged velocity
        kwargs:
        method    int        define method for averaging. 0 is the only supported one    
        '''
        if method == 0:
            sum_v_comp = Vector(0,0,0)
            for i in range(len(self.cells)):
                sum_v_comp += self.cells[i].velocity
                
            return  sum_v_comp / len(self.cells)
        
        else:
            raise Exception('method no {} not implemented'.format(method))


    def __init__(self, ensemble, profile_start, profile_dir, profile_dir_vec, profile_offset, settings):

        #
        # velocity processing

        # its the conversion factor from the unit defined in the raw data to meters.
        conversionfactor = unitconversion(ensemble.measurement_units, 1)

        if ensemble.number_of_good_cells != 0:
            # sometimes there is no good cell and hence no velocity,
            # (needed if someone wants to skip these)
            self.void = False
            
            # process the velocity in each cell
            if 'dimensions' not in settings or settings['dimensions'] == 3:
                self.cells = []
                for c in ensemble.cells:
                    self.cells.append(ProcessedCellObj(
                        c, profile_start, profile_dir, profile_dir_vec,
                        profile_offset, settings, conversionfactor))
                    
            # compute the averaged velocity
            if 'dimensions' not in settings or settings['dimensions'] == 2:
                self.velocity = Vector()
                # get averaged velocity (should be a Vector object)
                avg_method = settings['avg_method'] if 'avg_method' in settings else 0
                self.velocity = self.comp_avg_velocity(avg_method)

                # rotation
                if 'uv_rot' in settings and settings['uv_rot']:
                    #trans_uvrot(profile_dir)
                    base_rot = math.pi / 2
                    self.velocity = self.velocity.rot(base_rot + profile_dir)


        else:
            self.void = True

        #
        # position processing


        # processing the 4 depths and averaging
        self.four_depths = []
        self.four_depths += ensemble.depth_reading

        self.depth = sum(ensemble.depth_reading) / 4

        # z-value of the river ground for this ensemble
        #height = Vector(0,0, profile_start.z - self.depth)
        height = Vector(0,0, profile_start.z)

        dmg = ensemble.total_distance_made_good
        self.dmg = dmg

        if settings['proj_method'] == 1:

            # position on the profile (based on total distance made good)
            self.position = profile_dir_vec * dmg  + profile_start + height

        elif settings['proj_method'] == 2:
            # position according to movement data
            self.position = Vector(
                                ensemble.total_distance_traveled_east,
                                ensemble.total_distance_traveled_north) \
                                - profile_offset  + profile_start  + height


        elif settings['proj_method'] == 3:
            # position from movement data, projected on the profile
            self.position = Vector(
                                ensemble.total_distance_traveled_east,
                                ensemble.total_distance_traveled_north).proj(profile_dir_vec) \
                                - profile_offset  + profile_start  + height
            #p3 = (p2-profile_start).proj(profile_dir_vec) + profile_start

        else:
            raise NotImplementedError('you specified an invalid (or no) proj_method!')




class ProcessedCellObj():
    def __init__(self, cell, profile_start, profile_dir, profile_dir_vec, profile_offset, settings, conversionfactor):
        '''
            object to hold a finished (processed) cell , ready for the output

            attributes:
                self.velocity      a Vector() that holds the velocity in u/v/w coordinates
                self.z_position    a Vector() that holds the position of the cell in x/y/z coords
        '''
        
        # it's a measured cell
        self.artificial = False
        
        
        #
        # velocity processing again
        self.velocity = Vector()

        # switch coordinates
#        self.velocity.x = cell.velocity_comp.y    # east value
#        self.velocity.y = cell.velocity_comp.x    # north value
#        self.velocity.z = cell.velocity_comp.z    # up value
        
        self.velocity = Vector(cell.velocity_comp.x, cell.velocity_comp.y, cell.velocity_comp.z)
        


        # rotation
        if 'uv_rot' in settings and settings['uv_rot']:
#            base_rot = math.pi / 2
            self.velocity = self.velocity.rot(profile_dir)
        
        # unit conversion
        self.velocity *= conversionfactor

        #
        # position processing

        self.z_position = profile_start[2] - (cell.depth)
        if 'flip_z' in settings and settings['flip_z']:
            self.z_position *= -1


class ArtificialCellObj():
    def __init__(self, position, velocitiy):
        '''
        like ProcessedCellObj but created from extrapolated data.
        '''
        
        self.artificial = True
        self.z_position = position
        self.velocity = velocitiy
        
        