'''
Generic class and utility funtions for handling MOM6 grids.
'''

import logging, os
import numpy
import xarray as xr

from .. import utils
from .. import spherical

class MOM6(object):

    def __init__(self):
        self._grid_type = "MOM6"
        self.mom6_grid = dict()
        self.initMOM6 = False

        # Generic metadata
        self.mom6_grid['netcdf_info'] = dict()
        self.mom6_grid['netcdf_info']['grid_version']  = '0.2' # taken from make_solo_mosaic
        self.mom6_grid['netcdf_info']['code_version']  = utils.get_git_repo_version_info() ### for production
        argv = ["gridtools.grid.mom6.convert_ROMS_to_MOM6 + kwargs"]
        self.mom6_grid['netcdf_info']['history_entry'] = utils.get_history_entry(argv)

    def getGrid(self):
        '''Returns the converted grid as an xarray data structure.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        '''

        ds = xr.Dataset()

        ds['tile'] = self.mom6_grid['netcdf_info']['tile_str']

        if 'lon' in self.mom6_grid['supergrid'].keys():
            lonGrid = self.mom6_grid['supergrid']['lon']
            ds['x'] = (('nyp','nxp'), lonGrid)
            ds['x'].attrs['standard_name'] = 'geographic_longitude'
            ds['x'].attrs['units'] = 'degrees_east'
            ds['x'].attrs['sha256'] = utils.sha256sum( lonGrid )

            latGrid = self.mom6_grid['supergrid']['lat']
            ds['y'] = (('nyp','nxp'), latGrid)
            ds['y'].attrs['standard_name'] = 'geographic_latitude'
            ds['y'].attrs['units'] = 'degrees_north'
            ds['y'].attrs['sha256'] = utils.sha256sum( latGrid )

            ds.tile.attrs['geometry'] = "spherical"
        else:
            xGrid = self.mom6_grid['supergrid']['x']
            ds['x'] = (('nyp','nxp'), xGrid)
            ds['x'].attrs['standard_name'] = 'geographic_longitude'
            ds['x'].attrs['units'] = 'meters'
            ds['x'].attrs['sha256'] = utils.sha256sum( xGrid )

            yGrid = self.mom6_grid['supergrid']['y']
            ds['y'] = (('nyp','nxp'), yGrid)
            ds['y'].attrs['standard_name'] = 'geographic_latitude'
            ds['y'].attrs['units'] = 'meters'
            ds['y'].attrs['sha256'] = utils.sha256sum( yGrid )

            ds.tile.attrs['geometry'] = "cartesian"

        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['dx'] = (('nyp', 'nx'), self.mom6_grid['supergrid']['dx'].data)
        ds['dx'].attrs['units'] = 'meters'
        ds['dx'].attrs['sha256'] = utils.sha256sum( self.mom6_grid['supergrid']['dx'] )
        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['dy'] = (('ny', 'nxp'), self.mom6_grid['supergrid']['dy'].data)
        ds['dy'].attrs['units'] = 'meters'
        ds['dy'].attrs['sha256'] = utils.sha256sum( self.mom6_grid['supergrid']['dy'] )
        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['area'] = (('ny','nx'), self.mom6_grid['supergrid']['area'].data)
        ds['area'].attrs['units'] = 'meters^2'
        ds['area'].attrs['sha256'] = utils.sha256sum( self.mom6_grid['supergrid']['area'] )
        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['angle_dx'] = (('nyp','nxp'), self.mom6_grid['supergrid']['angle'].data)
        ds['angle_dx'].attrs['units'] = 'radians'
        ds['angle_dx'].attrs['sha256'] = utils.sha256sum( self.mom6_grid['supergrid']['angle'] )

        self._add_global_attributes(ds)

        return ds

    def setup_MOM6_grid(self, **kwargs):
        '''Create an empty MOM6 data structure.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.

        **Keyword arguments**:

            * *tileName* (``string``) -- name to assign to the solo tile. Default: "tile1"
            * *inputDirectory* (``string``) -- absolute or relative path to write model input files. Default: "INPUT"
            * *relativeToINPUTDir* (``string``) -- absolute or relative path for mosaic files to the INPUT directory.
              Default: "./" which refers to the "INPUT" directory.
            * *supergridName* (``string``) -- name to assign to the supergrid filename. Default: "ocean_hgrid.nc"
            * *topographyFilename* (``string``) -- filename used to write topographic grid. Default: "ocean_topog.nc"
            * *mosaicFilename* (``string``) -- filename for mosaic file. Default: "ocean_mosaic.nc"
            * *landmaskFilename* (``string``) -- filename used to write the land mask. Default: "land_mask.nc"
            * *oceanmaskFilename* (``string``) -- filename used to write the ocean mask. Default: "ocean_mask.nc"

        :return: none
        :rtype: none
        '''

        # Check and set any defaults to kwargs
        utils.checkArgument(kwargs, 'tileName', "tile1")
        utils.checkArgument(kwargs, 'inputDirectory', "INPUT")
        utils.checkArgument(kwargs, 'relativeToINPUTDir', "./")
        utils.checkArgument(kwargs, 'supergridFilename', "ocean_hgrid.nc")
        utils.checkArgument(kwargs, 'topographyFilename', "ocean_topog.nc")
        utils.checkArgument(kwargs, 'mosaicFilename', "ocean_mosaic.nc")
        utils.checkArgument(kwargs, 'landmaskFilename', "land_mask.nc")
        utils.checkArgument(kwargs, 'oceanmaskFilename', "ocean_mask.nc")

        self.tile_str = kwargs['tileName']
        self.atmos_tile = 'atmos_mosaic_' + self.tile_str
        self.land_tile  =  'land_mosaic_' + self.tile_str
        self.ocean_tile = 'ocean_mosaic_' + self.tile_str

        self.mom6_grid['filenames'] = dict()

        # Always use './' as the directory for files, since FMS always initializes from the
        # "INPUT" directory
        self.mom6_grid['filenames']['directory']            = kwargs['relativeToINPUTDir']

        self.mom6_grid['filenames']['supergrid']            = kwargs['supergridFilename']
        self.mom6_grid['filenames']['topography']           = kwargs['topographyFilename']
        self.mom6_grid['filenames']['mosaic']               = kwargs['mosaicFilename']
        self.mom6_grid['filenames']['land_mask']            = kwargs['landmaskFilename']
        self.mom6_grid['filenames']['ocean_mask']           = kwargs['oceanmaskFilename']
        self.mom6_grid['filenames']['atmos_land_exchange']  = '%sX%s.nc' % (self.atmos_tile, self.land_tile)
        self.mom6_grid['filenames']['atmos_ocean_exchange'] = '%sX%s.nc' % (self.atmos_tile, self.ocean_tile)
        self.mom6_grid['filenames']['land_ocean_exchange']  = '%sX%s.nc' % (self.land_tile, self.ocean_tile)

        # Moved parts into __init__
        #self.mom6_grid['netcdf_info'] = dict()
        self.mom6_grid['netcdf_info']['tile_str']      = self.tile_str
        # No longer needed
        #self.mom6_grid['netcdf_info']['string_length'] = 255
        #self.mom6_grid['netcdf_info']['grid_version']  = '0.2' # taken from make_solo_mosaic
        #self.mom6_grid['netcdf_info']['code_version']  = '$Name: tikal $' ### for testing
        #self.mom6_grid['netcdf_info']['code_version']  = get_git_repo_version_info() ### for production
        #self.mom6_grid['netcdf_info']['code_version']  = 'TODO'
        #argv = ["gridtools.grid.mom6.convert_ROMS_to_MOM6 + kwargs"]
        #self.mom6_grid['netcdf_info']['history_entry'] = get_history_entry(argv)
        #self.mom6_grid['netcdf_info']['history_entry'] = 'TODO'

        self.mom6_grid['supergrid'] = dict()
        self.mom6_grid['cell_grid'] = dict()

        self.initMOM6 = True
        return

    def convert_ROMS_to_MOM6(self, roms_grid, **kwargs):
        '''Convert the ROMS grid data into a skeleton MOM6 grid, mainly by
        merging the four sets of point locations from the ROMS grid
        into a single supergrid for MOM6.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        '''

        # Determine values from other possible arguments
        minimum_depth = 0.0
        masking_depth = -99999.0
        maximum_depth = -99999.0
        if 'MINIMUM_DEPTH' in kwargs.keys():
            minimum_depth = kwargs['MINIMUM_DEPTH']
        if 'MAXIMUM_DEPTH' in kwargs.keys():
            maximum_depth = kwargs['MAXIMUM_DEPTH']
        if 'MASKING_DEPTH' in kwargs.keys():
            masking_depth = kwargs['MASKING_DEPTH']

        # MINIMUM_DEPTH < MASKING_DEPTH, if MASKING_DEPTH is undefined, set it to MINIMUM_DEPTH
        if minimum_depth < masking_depth:
            masking_depth = minimum_depth
        if masking_depth < -99990.0:
            masking_depth = minimum_depth

        ny, nx = roms_grid['rho']['lon'].shape # trimmed
        self.mom6_grid['cell_grid']['nx'] = nx
        self.mom6_grid['cell_grid']['ny'] = ny

        # Double the size of the *trimmed* ROMS grid to merge the four
        # sets of points.
        nx *= 2
        ny *= 2

        self.mom6_grid['supergrid']['nx'] = nx
        self.mom6_grid['supergrid']['ny'] = ny

        if roms_grid['metadata']['is_spherical']:
            copy_fields = ['lon', 'lat']
        else:
            copy_fields = ['x', 'y']

        # Copy points from ROMS grid
        for field in copy_fields:
            self.mom6_grid['supergrid'][field] = numpy.zeros((ny+1, nx+1))
            self.mom6_grid['supergrid'][field][ ::2, ::2] = roms_grid['psi'][field] # outer
            self.mom6_grid['supergrid'][field][1::2,1::2] = roms_grid['rho'][field] # inner
            self.mom6_grid['supergrid'][field][1::2, ::2] = roms_grid[ 'u' ][field] # between e/w
            self.mom6_grid['supergrid'][field][ ::2,1::2] = roms_grid[ 'v' ][field] # between n/s

        # Create an ocean and land mask, any land mask depths are zeroed out.
        # 0 = land, 1 = water, but sometimes some huge number indicates
        # "missing" values, which we'll assume to be water
        mask = roms_grid['rho']['mask']
        #mask[mask != 0] = 1
        mask = numpy.where(mask != 0, 1, 0)
        #self.mom6_grid['cell_grid']['depth'] = roms_grid['rho']['h'] * mask
        # TODO: This can be improved instead of just clobbering land points with masking_depth
        self.mom6_grid['cell_grid']['depth'] = xr.where(mask, roms_grid['rho'][kwargs['topographyVariable']], masking_depth)
        self.mom6_grid['cell_grid']['ocean_mask'] = mask
        self.mom6_grid['cell_grid']['land_mask'] = numpy.logical_not(mask)

        return

    def approximate_MOM6_grid_metrics(self):
        '''Fill in missing MOM6 supergrid metrics by computing best guess values.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        '''

        nx = self.mom6_grid['supergrid']['nx']
        ny = self.mom6_grid['supergrid']['ny']

        # Declare shapes
        self.mom6_grid['supergrid']['dx']    = numpy.zeros((ny+1,nx  ))
        self.mom6_grid['supergrid']['dy']    = numpy.zeros((ny,  nx+1))
        self.mom6_grid['supergrid']['angle'] = numpy.zeros((ny+1,nx+1))
        self.mom6_grid['supergrid']['area']  = numpy.zeros((ny,  nx  ))

        # Rebuild to use generic routines; for now these are copies as well
        if 'lat' in self.mom6_grid['supergrid']:
            self._fill_in_MOM6_supergrid_metrics_spherical()
        else:
            self._fill_in_MOM6_supergrid_metrics_cartesian()

        self._calculate_MOM6_cell_grid_area()

        return

    # FILE WRITING FUNCTIONS / OUTPUT

    def write_MOM6_topography_file(self, grd, **kwargs):
        """Save the MOM6 ocean topography grid in a separate file.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Define target file
        destinationFile = os.path.join(kwargs['inputDirectory'], kwargs['topographyFilename'])
        if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
            msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
            grd.printMsg(msg, logging.WARNING)
            return

        ds = xr.Dataset()

        #nx = mom6_grid['cell_grid']['nx']
        #ny = mom6_grid['cell_grid']['ny']
        #with netCDF4.Dataset(mom6_grid['filenames']['topography'], 'w', format='NETCDF3_CLASSIC') as topog_ds:
        #    # Dimensions
        #    topog_ds.createDimension('nx', nx)
        #    topog_ds.createDimension('ny', ny)
        #    topog_ds.createDimension('ntiles', 1)

        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['depth'] = (('ny', 'nx'), kwargs['topographyGrid'].data)
        ds['depth'].attrs['units'] = 'meters'
        ds['depth'].attrs['standard_name'] = 'topographic depth at Arakawa C h-points'
        ds['depth'].attrs['sha256'] = utils.sha256sum( kwargs['topographyGrid'] )

        #    # Variables & Values
        #    hdepth = topog_ds.createVariable('depth', 'f4', ('ny','nx',))
        #    hdepth.units = 'm'
        #    hdepth[:] = mom6_grid['cell_grid']['depth']

        # Global attributes
        self._add_global_attributes(ds)

        # Perform write
        ds.to_netcdf(destinationFile, encoding=grd.removeFillValueAttributes(data=ds))

        return

    def write_MOM6_solo_mosaic_file(self, grd, **kwargs):
        """Write the "solo mosaic" file, which describes to the FMS infrastructure
         where to find the grid file(s).  Based on tools in version 5 of MOM
         (http://www.mom-ocean.org/).

        **Keyword arguments**:

        * *intType* (``str``) -- The default type to be written out to for the FMS
          coupler tile files.  Default: int32

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Define target file
        destinationFile = os.path.join(kwargs['inputDirectory'], kwargs['mosaicFilename'])
        if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
            msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
            grd.printMsg(msg, logging.WARNING)
            return

        ds = xr.Dataset()

        # NOTE: This function is very basic, since we're skipping the
        # finding of "contact regions" between the tiles that the real
        # make_solo_mosaic tool performs.  It's not needed right now,
        # since we only have one (regional) tile, but I think this feature
        # will be needed if we ever use a tripolar grid.

        #with netCDF4.Dataset(mom6_grid['filenames']['mosaic'], 'w', format='NETCDF3_CLASSIC') as mosaic_ds:
        #    # Dimenisons
        #    mosaic_ds.createDimension('ntiles', 1)
        #    mosaic_ds.createDimension('string', mom6_grid['netcdf_info']['string_length'])

        #   # Variables & Values
        #   hmosaic = mosaic_ds.createVariable('mosaic', 'c', ('string',))
        #   hmosaic.standard_name = 'grid_mosaic_spec'
        #   hmosaic.children = 'gridtiles'
        #   hmosaic.contact_regions = 'contacts'
        #   hmosaic.grid_descriptor = ''
        #   dirname,filename = os.path.split(mom6_grid['filenames']['mosaic'])
        #   filename,ext = os.path.splitext(filename)
        #   hmosaic[:len(filename)] = filename
        ds['mosaic'] = kwargs['mosaicFilename']
        ds['mosaic'].attrs['standard_name'] = 'grid_mosaic_spec'
        ds['mosaic'].attrs['children'] = 'gridtiles'
        ds['mosaic'].attrs['contact_regions'] = 'contacts'
        ds['mosaic'].attrs['grid_descriptor'] = ''

        #   hgridlocation = mosaic_ds.createVariable('gridlocation', 'c', ('string',))
        #   hgridlocation.standard_name = 'grid_file_location'
        #   this_dir = mom6_grid['filenames']['directory']
        #   hgridlocation[:len(this_dir)] = this_dir
        ds['gridlocation'] = kwargs['relativeToINPUTDir']
        ds['gridlocation'].attrs['standard_name'] = 'grid_file_location'

        #   hgridfiles = mosaic_ds.createVariable('gridfiles', 'c', ('ntiles', 'string',))
        #   hgridfiles[0, :len(mom6_grid['filenames']['supergrid'])] = mom6_grid['filenames']['supergrid']
        ds['gridfiles'] = (('ntiles',), [kwargs['oceanGridFilename']])

        #   hgridtiles = mosaic_ds.createVariable('gridtiles', 'c', ('ntiles', 'string',))
        #   hgridtiles[0, :len(mom6_grid['netcdf_info']['tile_str'])] = mom6_grid['netcdf_info']['tile_str']
        ds['gridtiles'] = (('ntiles',), [kwargs['tileName']])

        # Global attributes
        self._add_global_attributes(ds)

        ds.to_netcdf(destinationFile, encoding=grd.removeFillValueAttributes(data=ds,\
            stringVars={'mosaic': 255, 'gridlocation': 255, 'gridfiles': 255, 'gridtiles': 255}))

        return

    def write_MOM6_land_mask_file(self, grd, **kwargs):
        """Write the land mask file.  Based on 'make_quick_mosaic' tool in version
        5 of MOM (http://www.mom-ocean.org/).

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Define target file
        destinationFile = os.path.join(kwargs['inputDirectory'], kwargs['landmaskFilename'])
        if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
            msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
            grd.printMsg(msg, logging.WARNING)
            return

        ds = xr.Dataset()

        #nx = mom6_grid['cell_grid']['nx']
        #ny = mom6_grid['cell_grid']['ny']
        #with netCDF4.Dataset(mom6_grid['filenames']['land_mask'], 'w', format='NETCDF3_CLASSIC') as land_mask_ds:
        #    # Dimenisons (of grid cells, not supergrid)
        #    land_mask_ds.createDimension('nx', nx)
        #    land_mask_ds.createDimension('ny', ny)

        #    # Variables & Values
        #    hmask = land_mask_ds.createVariable('mask', 'd', ('ny', 'nx'))
        #    hmask.standard_name = 'land fraction at T-cell centers'
        #    hmask.units = 'none'
        #    hmask[:] = mom6_grid['cell_grid']['land_mask']

        # Find land mask points using settings from kwargs
        landMask = self._generate_mask('land', grd, **kwargs)

        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['mask'] = (('ny', 'nx'), landMask.data)
        ds['mask'].attrs['standard_name'] = 'land fraction at T-cell centers'
        ds['mask'].attrs['units'] = 'none'
        ds['mask'].attrs['sha256'] = utils.sha256sum( ds['mask'] )

        if 'supergrid' in self.mom6_grid:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['x'] = (('ny', 'nx'), self.mom6_grid['supergrid']['x'][1::2,1::2].data)
        else:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['x'] = (('ny', 'nx'), grd.grid['x'][1::2,1::2].data)
        ds['x'].attrs['sha256'] = utils.sha256sum( ds['x'] )
        ds['x'].attrs['standard_name'] = 'longitude'
        ds['x'].attrs['units'] = 'degrees_east'
        if 'supergrid' in self.mom6_grid:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['y'] = (('ny', 'nx'), self.mom6_grid['supergrid']['y'][1::2,1::2].data)
        else:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['y'] = (('ny', 'nx'), grd.grid['y'][1::2,1::2].data)
        ds['y'].attrs['sha256'] = utils.sha256sum( ds['y'] )
        ds['y'].attrs['standard_name'] = 'latitude'
        ds['y'].attrs['units'] = 'degrees_north'

        # Global attributes
        self._add_global_attributes(ds)

        ds.to_netcdf(destinationFile, encoding=grd.removeFillValueAttributes(data=ds))

        return

    def write_MOM6_ocean_mask_file(self, grd, **kwargs):
        """Write the ocean mask file.  Based on 'make_quick_mosaic' tool in version
        5 of MOM (http://www.mom-ocean.org/).

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Define target file
        destinationFile = os.path.join(kwargs['inputDirectory'], kwargs['oceanmaskFilename'])
        if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
            msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
            grd.printMsg(msg, logging.WARNING)
            return

        ds = xr.Dataset()

        #nx = mom6_grid['cell_grid']['nx']
        #ny = mom6_grid['cell_grid']['ny']
        #with netCDF4.Dataset(mom6_grid['filenames']['ocean_mask'], 'w', format='NETCDF3_CLASSIC') as ocean_mask_ds:
        #    # Dimenisons (of grid cells, not supergrid)
        #    ocean_mask_ds.createDimension('nx', nx)
        #    ocean_mask_ds.createDimension('ny', ny)

        #    # Variables & Values
        #    hmask = ocean_mask_ds.createVariable('mask', 'd', ('ny', 'nx'))
        #    hmask.standard_name = 'ocean fraction at T-cell centers'
        #    hmask.units = 'none'
        #    hmask[:] = mom6_grid['cell_grid']['ocean_mask']

        # Find land mask points using settings from kwargs
        oceanMask = self._generate_mask('ocean', grd, **kwargs)

        # xarray=0.19.0 requires unpacking of Dataset variables by using .data
        ds['mask'] = (('ny', 'nx'), oceanMask.data)
        ds['mask'].attrs['standard_name'] = 'ocean fraction at T-cell centers'
        ds['mask'].attrs['units'] = 'none'
        ds['mask'].attrs['sha256'] = utils.sha256sum( ds['mask'] )

        if 'supergrid' in self.mom6_grid:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['x'] = (('ny', 'nx'), self.mom6_grid['supergrid']['x'][1::2,1::2].data)
        else:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['x'] = (('ny', 'nx'), grd.grid['x'][1::2,1::2].data)
        ds['x'].attrs['sha256'] = utils.sha256sum( ds['x'] )
        ds['x'].attrs['standard_name'] = 'longitude'
        ds['x'].attrs['units'] = 'degrees_east'
        if 'supergrid' in self.mom6_grid:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['y'] = (('ny', 'nx'), self.mom6_grid['supergrid']['y'][1::2,1::2].data)
        else:
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['y'] = (('ny', 'nx'), grd.grid['y'][1::2,1::2].data)
        ds['y'].attrs['sha256'] = utils.sha256sum( ds['y'] )
        ds['y'].attrs['standard_name'] = 'latitude'
        ds['y'].attrs['units'] = 'degrees_north'

        # Global attributes
        self._add_global_attributes(ds)

        ds.to_netcdf(destinationFile, encoding=grd.removeFillValueAttributes(data=ds))

        return

    def write_MOM6_exchange_grid_files(self, grd, **kwargs):
        """Write three exchange grid files.
        Based on 'make_quick_mosaic' tool in version 5 of MOM (http://www.mom-ocean.org/).

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Exchange grids to write
        exchangeGrids = [
            ('atmos', 'land'),
            ('atmos', 'ocean'),
            ('land', 'ocean')
        ]

        # Calculate cell_grid area (if needed) - this happens if
        # this is called separately from the ROMS to MOM6 conversion tool.
        if not(self.initMOM6):
            self.setup_MOM6_grid(**kwargs)
            # Copy supergrid area to internal variable
            self.mom6_grid['supergrid']['area'] = grd.grid['area']
            self._calculate_MOM6_cell_grid_area()

        # Loop for the three types
        for name1, name2 in exchangeGrids:

            ds = xr.Dataset()

            # calculate the exchange grid for name1 X name2

            #mask = mom6_grid['cell_grid'][name2 + '_mask']
            mask = self._generate_mask(name2, grd, **kwargs)

            tile_cells_j, tile_cells_i = numpy.where(mask == 1)
            tile_cells = numpy.column_stack((tile_cells_i, tile_cells_j)) + 1 # +1 converts from Python indices to Fortran
            #print(type(self.mom6_grid['cell_grid']['area']), type(mask))

            # In xarray, to pull cell values out for matching mask, the variable and mask have to appear in the same
            # dataset.
            dsCombined = xr.Dataset()
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            dsCombined['area'] = (('ny','nx'), self.mom6_grid['cell_grid']['area'].data)
            dsCombined['mask'] = mask
            dsCombined['sha256'] = utils.sha256sum( self.mom6_grid['cell_grid']['area'] )
            idx = numpy.nonzero(mask==1)

            #breakpoint()
            #xgrid_area = self.mom6_grid['cell_grid']['area'][mask == 1]
            xgrid_area = dsCombined['area'][idx[0], idx[1]]
            ncells = len(xgrid_area)
            tile_dist = numpy.zeros((ncells,2))

            # write out exchange grid file

            filename_key = '%s_%s_exchange' % (name1, name2)
            filename = self.mom6_grid['filenames'][filename_key]
            # Define target file
            destinationFile = os.path.join(kwargs['inputDirectory'], filename)
            if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
                msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
                grd.printMsg(msg, logging.WARNING)
                continue

            #with netCDF4.Dataset(filename, 'w', format='NETCDF3_CLASSIC') as exchange_ds:
            #   exchange_ds.createDimension('string', mom6_grid['netcdf_info']['string_length'])
            #   exchange_ds.createDimension('ncells', ncells)
            #   exchange_ds.createDimension('two', 2)

            contact_str = '{0}_mosaic:{2}::{1}_mosaic:{2}'.format(name1, name2, kwargs['tileName'])
            #   hcontact = exchange_ds.createVariable('contact', 'c', ('string',))
            #   hcontact.standard_name = 'grid_contact_spec'
            #   hcontact.contact_type = 'exchange'
            #   hcontact.parent1_cell = 'tile1_cell'
            #   hcontact.parent2_cell = 'tile2_cell'
            #   hcontact.xgrid_area_field = 'xgrid_area'
            #   hcontact.distant_to_parent1_centroid = 'tile1_distance'
            #   hcontact.distant_to_parent2_centroid = 'tile2_distance'
            #   hcontact[0:len(contact_str)] = contact_str
            ds['contact'] = contact_str
            ds['contact'].attrs['standard_name'] = 'grid_contact_spec'
            ds['contact'].attrs['contact_type'] = 'exchange'
            ds['contact'].attrs['parent1_cell'] = 'tile1_cell'
            ds['contact'].attrs['parent2_cell'] = 'tile2_cell'
            ds['contact'].attrs['xgrid_area_field'] = 'xgrid_area'
            ds['contact'].attrs['distant_to_parent1_centroid'] = 'tile1_distance'
            ds['contact'].attrs['distant_to_parent2_centroid'] = 'tile2_distance'

            #   htile1_cell = exchange_ds.createVariable('tile1_cell', 'i', ('ncells', 'two'))
            #   htile1_cell.standard_name = 'parent_cell_indices_in_mosaic1'
            #   htile1_cell[:] = tile_cells
            ds['tile1_cell'] = (('ncells', 'two'), tile_cells)
            ds['tile1_cell'].attrs['standard_name'] = 'parent_cell_indices_in_mosaic1'

            #   htile2_cell = exchange_ds.createVariable('tile2_cell', 'i', ('ncells', 'two'))
            #   htile2_cell.standard_name = 'parent_cell_indices_in_mosaic2'
            #   htile2_cell[:] = tile_cells
            ds['tile2_cell'] = (('ncells', 'two'), tile_cells)
            ds['tile2_cell'].attrs['standard_name'] = 'parent_cell_indices_in_mosaic2'

            #   hxgrid_area = exchange_ds.createVariable('xgrid_area', 'd', ('ncells',))
            #   hxgrid_area.standard_name = 'exchange_grid_area'
            #   hxgrid_area.units = 'm2'
            #   hxgrid_area[:] = xgrid_area
            # xarray=0.19.0 requires unpacking of Dataset variables by using .data
            ds['xgrid_area'] = (('ncells'), xgrid_area.data)
            ds['xgrid_area'].attrs['standard_name'] = 'exchange_grid_area'
            ds['xgrid_area'].attrs['units'] = 'm2'

            #   htile1_dist = exchange_ds.createVariable('tile1_distance', 'd', ('ncells', 'two'))
            #   htile1_dist.standard_name = 'distance_from_parent1_cell_centroid'
            #   htile1_dist[:] = tile_dist
            ds['tile1_distance'] = (('ncells', 'two'), tile_dist)
            ds['tile1_distance'].attrs['standard_name'] = 'distance_from_parent1_cell_centroid'

            #   htile2_dist = exchange_ds.createVariable('tile2_distance', 'd', ('ncells', 'two'))
            #   htile2_dist.standard_name = 'distance_from_parent2_cell_centroid'
            #   htile2_dist[:] = tile_dist
            ds['tile2_distance'] = (('ncells', 'two'), tile_dist)
            ds['tile2_distance'].attrs['standard_name'] = 'distance_from_parent2_cell_centroid'

            # Global attributes
            self._add_global_attributes(ds)

            enc = grd.removeFillValueAttributes(data = ds, stringVars = {'contact': 255})
            # For the FMS coupler, *_cell variables must be i4/int32
            intType = 'int32'
            if 'intType' in kwargs.keys():
                intType = kwargs['intType']
            enc['tile1_cell'] = {'dtype': intType}
            enc['tile2_cell'] = {'dtype': intType}
            ds.to_netcdf(destinationFile, encoding = enc)

        return

    def write_MOM6_coupler_mosaic_file(self, grd, **kwargs):
        """Write the compler mosaic file, which references all the rest.
        Based on 'make_quick_mosaic' tool in version 5 of MOM (http://www.mom-ocean.org/).

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        def add_string_var_1d(ds, var_name, standard_name, value, strVarMap):
            """Creates and stores values for a 1-dimensional string variable."""
            #    id_var = fid.createVariable(var_name, 'c', ('string',))
            #    id_var.standard_name = standard_name
            #    id_var[0:len(value)] = value
            strVarMap[var_name] = 255
            ds[var_name] = value
            ds[var_name].attrs['standard_name'] = standard_name

        def add_string_var_2d(ds, var_name, dim_name, standard_name, value, strVarMap):
            """Creates and stores values for a 2-dimensional string variable, for
            which the first dimension has length 1."""
            #    id_var = fid.createVariable(var_name, 'c', (dim_name, 'string'))
            #    id_var.standard_name = standard_name
            #    id_var[0, 0:len(value)] = value
            strVarMap[var_name] = 255
            ds[var_name] = ((dim_name,), [value])
            ds[var_name].attrs['standard_name'] = standard_name

        # Accumulate string variables for encoding
        strVarMap = dict()

        # Initialize mom6_grid[] (if needed) - this happens if
        # this is called separately from the ROMS to MOM6 conversion tool.
        if not(self.initMOM6):
            self.setup_MOM6_grid(**kwargs)

        # Define target file
        destinationFile = os.path.join(kwargs['inputDirectory'], kwargs['couplerMosaicFilename'])
        if os.path.isfile(destinationFile) and not(kwargs['overwrite']):
            msg = ("WARNING: File (%s) exists, use overwrite=True to allow overwriting." % (destinationFile))
            grd.printMsg(msg, logging.WARNING)
            return

        ds = xr.Dataset()

        #with netCDF4.Dataset('mosaic.nc', 'w', format='NETCDF3_CLASSIC') as mosaic_ds:
        #    mosaic_ds.createDimension('string', mom6_grid['netcdf_info']['string_length'])
        #    mosaic_ds.createDimension('nfile_aXo', 1)
        #    mosaic_ds.createDimension('nfile_aXl', 1)
        #    mosaic_ds.createDimension('nfile_lXo', 1)

        # same mosaic file for all three -- just like when "make_solo_mosaic" is used

        add_string_var_1d(ds, 'atm_mosaic_dir',  'directory_storing_atmosphere_mosaic', self.mom6_grid['filenames']['directory'], strVarMap)
        add_string_var_1d(ds, 'atm_mosaic_file', 'atmosphere_mosaic_file_name',         self.mom6_grid['filenames']['mosaic']   , strVarMap)
        add_string_var_1d(ds, 'atm_mosaic',      'atmosphere_mosaic_name',              'atmos_mosaic'                     , strVarMap)

        add_string_var_1d(ds, 'lnd_mosaic_dir',  'directory_storing_land_mosaic',       self.mom6_grid['filenames']['directory'],  strVarMap)
        add_string_var_1d(ds, 'lnd_mosaic_file', 'land_mosaic_file_name',               self.mom6_grid['filenames']['mosaic']   ,  strVarMap)
        add_string_var_1d(ds, 'lnd_mosaic',      'land_mosaic_name',                    'land_mosaic'                      ,  strVarMap)

        add_string_var_1d(ds, 'ocn_mosaic_dir',  'directory_storing_ocean_mosaic',      self.mom6_grid['filenames']['directory'],  strVarMap)
        add_string_var_1d(ds, 'ocn_mosaic_file', 'ocean_mosaic_file_name',              self.mom6_grid['filenames']['mosaic']   ,  strVarMap)
        add_string_var_1d(ds, 'ocn_mosaic',      'ocean_mosaic_name',                   'ocean_mosaic'                     ,  strVarMap)

        add_string_var_1d(ds, 'ocn_topog_dir',   'directory_storing_ocean_topog',       self.mom6_grid['filenames']['directory'],  strVarMap)
        add_string_var_1d(ds, 'ocn_topog_file',  'ocean_topog_file_name',               self.mom6_grid['filenames']['topography'], strVarMap)

        add_string_var_2d(ds, 'aXo_file', 'nfile_aXo', 'atmXocn_exchange_grid_file', self.mom6_grid['filenames']['atmos_ocean_exchange'], strVarMap)
        add_string_var_2d(ds, 'aXl_file', 'nfile_aXl', 'atmXlnd_exchange_grid_file', self.mom6_grid['filenames']['atmos_land_exchange'] , strVarMap)
        add_string_var_2d(ds, 'lXo_file', 'nfile_lXo', 'lndXocn_exchange_grid_file', self.mom6_grid['filenames']['land_ocean_exchange'] , strVarMap)

        # Global attributes
        self._add_global_attributes(ds)

        ds.to_netcdf(destinationFile, encoding=grd.removeFillValueAttributes(data=ds,\
            stringVars=strVarMap))

        return

    # PRIVATE FUNCTIONS

    def _add_global_attributes(self, ds):
        '''Helper function to add common metadata to xarray datasets.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        '''
        ds.attrs['grid_version'] = self.mom6_grid['netcdf_info']['grid_version']
        ds.attrs['code_version'] = self.mom6_grid['netcdf_info']['code_version']
        ds.attrs['history']      = self.mom6_grid['netcdf_info']['history_entry']

        return

    def _calculate_MOM6_cell_grid_area(self):
        """Compute the area for the MOM6 cells (not the sub-cells of the
        supergrid).

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        # Combine areas of smaller supergrid cells into areas of cell-grid cells
        a00 = self.mom6_grid['supergrid']['area'][0::2,0::2]
        a01 = self.mom6_grid['supergrid']['area'][0::2,1::2]
        a10 = self.mom6_grid['supergrid']['area'][1::2,0::2]
        a11 = self.mom6_grid['supergrid']['area'][1::2,1::2]
        self.mom6_grid['cell_grid']['area'] = a00 + a01 + a10 + a11

    def _fill_in_MOM6_supergrid_metrics_spherical(self):
        """Fill in missing MOM6 supergrid metrics by computing best guess
        values based on latitude and longitude coordinates.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        lat = self.mom6_grid['supergrid']['lat']
        lon = self.mom6_grid['supergrid']['lon']

        # Approximate edge lengths as great arcs
        R = 6370.e3 # Radius of sphere
        self.mom6_grid['supergrid']['dx'][:,:] = R * spherical.angle_through_center( (lat[ :,1:],lon[ :,1:]), (lat[:  ,:-1],lon[:  ,:-1]) )
        self.mom6_grid['supergrid']['dy'][:,:] = R * spherical.angle_through_center( (lat[1:, :],lon[1:, :]), (lat[:-1,:  ],lon[:-1,:  ]) )

        # Approximate angles using centered differences in interior, and side differences on left/right edges
        # TODO: Why do something different at the edges when we have extra ROMS points available?
        # Because we're using a big enough footprint to need to.
        cos_lat = numpy.cos(numpy.radians(lat))
    #   self.mom6_grid['supergrid']['angle'][:,1:-1] = numpy.arctan( (lat[:,2:] - lat[:,:-2]) / ((lon[:,2:] - lon[:,:-2]) * cos_lat[:,1:-1]) )
    #   self.mom6_grid['supergrid']['angle'][:, 0  ] = numpy.arctan( (lat[:, 1] - lat[:, 0 ]) / ((lon[:, 1] - lon[:, 0 ]) * cos_lat[:, 0  ]) )
    #   self.mom6_grid['supergrid']['angle'][:,-1  ] = numpy.arctan( (lat[:,-1] - lat[:,-2 ]) / ((lon[:,-1] - lon[:,-2 ]) * cos_lat[:,-1  ]) )
        # Compute it twice to recover from dateline problems, if any
        angle = numpy.zeros(lat.shape)
        angle2 = numpy.zeros(lat.shape)
    #   angle[:,1:-1] = numpy.arctan2( (lat[:,2:] - lat[:,:-2]) , ((lon[:,2:] - lon[:,:-2]) * cos_lat[:,1:-1]) )
    #   angle[:, 0  ] = numpy.arctan2( (lat[:, 1] - lat[:, 0 ]) , ((lon[:, 1] - lon[:, 0 ]) * cos_lat[:, 0  ]) )
    #   angle[:,-1  ] = numpy.arctan2( (lat[:,-1] - lat[:,-2 ]) , ((lon[:,-1] - lon[:,-2 ]) * cos_lat[:,-1  ]) )
        lon = numpy.where(lon < 0., lon+360, lon)
        angle2[:,1:-1] = numpy.arctan2( (lat[:,2:] - lat[:,:-2]) , ((lon[:,2:] - lon[:,:-2]) * cos_lat[:,1:-1]) )
        angle2[:, 0  ] = numpy.arctan2( (lat[:, 1] - lat[:, 0 ]) , ((lon[:, 1] - lon[:, 0 ]) * cos_lat[:, 0  ]) )
        angle2[:,-1  ] = numpy.arctan2( (lat[:,-1] - lat[:,-2 ]) , ((lon[:,-1] - lon[:,-2 ]) * cos_lat[:,-1  ]) )
        self.mom6_grid['supergrid']['angle'][:,:] = numpy.maximum(angle, angle2)

        # Approximate cell areas as that of spherical polygon
        self.mom6_grid['supergrid']['area'][:,:] = R * R * spherical.quad_area(lat, lon)

        return

    def _fill_in_MOM6_supergrid_metrics_cartesian(self):
        """Fill in missing MOM6 supergrid metrics by computing best guess
        values based on x and y coordinates.

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        """

        x = self.mom6_grid['supergrid']['x']
        y = self.mom6_grid['supergrid']['y']

        # Compute edge lengths
        self.mom6_grid['supergrid']['dx'][:,:] = numpy.sqrt( (x[:,1:] - x[:,:-1])**2 + (y[:,1:] - y[:,:-1])**2 )
        self.mom6_grid['supergrid']['dy'][:,:] = numpy.sqrt( (x[1:,:] - x[:-1,:])**2 + (y[1:,:] - y[:-1,:])**2 )

        # Compute angles using centered differences in interior, and side differences on left/right edges
        # TODO: Why do something different at the edges when we have extra ROMS points available?
        self.mom6_grid['supergrid']['angle'][:,1:-1] = numpy.arctan2( (y[:,2:] - y[:,:-2]), (x[:,2:] - x[:,:-2]) )
        self.mom6_grid['supergrid']['angle'][:, 0  ] = numpy.arctan2( (y[:, 1] - y[:, 0 ]), (x[:, 1] - x[:, 0 ]) )
        self.mom6_grid['supergrid']['angle'][:,-1  ] = numpy.arctan2( (y[:,-1] - y[:,-2 ]), (x[:,-1] - x[:,-2 ]) )

        # Compute cell areas
        self.mom6_grid['supergrid']['area'][:,:] = self.mom6_grid['supergrid']['dx'][:-1, :] * self.mom6_grid['supergrid']['dy'][:, :-1]

        return

    def _generate_mask(self, maskType, grd, **kwargs):
        '''Generate a land or ocean mask based on parameters provided.

        :param maskType: mask type requested ('land' or 'ocean')
        :type maskType: string
        :param grd: object
        :type grd: GridUtils() object
        :return: land or ocean mask
        :rtype: xarray

        Keyword args must have a valid depth grid in *topographyGrid*.  MOM6
        parameters `MINIMUM_DEPTH`, `MASKING_DEPTH` and `MAXIMUM_DEPTH` may also
        be specified.

        **Keyword arguments**:

        * *epsilon* (``float``) -- Depth removed from masking depth to ensure new
          depth is deeper than the applied masking or minimum depth. Default: 1.0e-14
        * *MINIMUM_DEPTH* (``float``) --
          Minimum ocean depth. Default: 0.0
        * *MASKING_DEPTH* (``float``) --
          Ocean depths equal or shallower are set to land mask. Default: undefined
        * *MAXIMUM_DEPTH* (``float``) --
          Maximum depth of the ocean.  Defaults to maximum depth from data source
          if not specified or is negative. Default: undefined

        This function is based on code from :cite:p:`Ilicak_2020_ROMS_to_MOM6`.
        '''

        # Access depth field
        depthGrid = kwargs['topographyGrid']

        # Determine values from other possible arguments
        minimum_depth = 0.0
        masking_depth = -99999.0
        maximum_depth = -99999.0
        if 'MINIMUM_DEPTH' in kwargs.keys():
            minimum_depth = kwargs['MINIMUM_DEPTH']
        if 'MAXIMUM_DEPTH' in kwargs.keys():
            maximum_depth = kwargs['MAXIMUM_DEPTH']
        if 'MASKING_DEPTH' in kwargs.keys():
            masking_depth = kwargs['MASKING_DEPTH']

        # As is done in MOM6, if maximum is negative, it is defined by the maximum of
        # the 'depth' field passed.
        if maximum_depth < 0.0:
            maximum_depth = depthGrid.max().values.tolist()
            #msg = ("The (diagnosed) maximum depth of the ocean %f meters." % (maximum_depth))
            #grd.printMsg(msg, level=logging.INFO)

        # MINIMUM_DEPTH must be defined.  If MASKING_DEPTH is not defined, it is set to MINIMUM_DEPTH.
        if masking_depth < -99990.0:
            masking_depth = minimum_depth

        if maskType == 'land':
            return xr.where(depthGrid <= masking_depth, 1, 0)

        if maskType == 'ocean':
            return xr.where(depthGrid > masking_depth, 1, 0)

        msg = ("ERROR: Unknown mask type (%s) passed to mom6._generate_mask()")
        grd.printMsg(msg, logging.ERROR)


#!/usr/bin/env python3
# vi: set sw=4 et:
"""
How to take a ChimeraX.app on Linux and turn it into a debian package
for Ubuntu

References:
https://wiki.debian.org/FilesystemHierarchyStandard
https://www.debian.org/doc/debian-policy/
https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
#
Designed for Ubuntu, would mostly work on Debian except that ffmpeg
not available in Debian

Steps:
  1. build ChimeraX without Debian supplied prerequisites
  2. create Debian binary package layout
  3. build Deiban package
"""

import os
import subprocess
import sys
import tempfile
import textwrap

# app_author and app_name are the same as ChimeraX_main.py
app_author = "UCSF"
app_name = "ChimeraX"

CHIMERAX_INSTALL = f"{app_name}.app"
CHIMERAX_BIN = f"{CHIMERAX_INSTALL}/bin/{app_name}"

# lintian(1) complains about files /opt
# INST_DIR = "opt"
INST_DIR = "usr/lib"

UBUNTU_DEPENDENCIES = {
    "16.04": {
        "libasound2": "1.1.0",
        "libatk1.0-0": "2.18.0",
        "libbz2-1.0": "1.0.6",
        "libc6": "2.23",
        "libcairo-gobject2": "1.14.6",
        "libcairo2": "1.14.6",
        "libcups2": "2.1.3",
        "libdbus-1-3": "1.10.6",
        "libdrm2": "2.4.83",
        "libegl1-mesa": "17.2.8",
        "libexpat1": "2.1.0",
        "libfftw3-single3": "3.3.4",
        "libfontconfig1": "2.11.94",
        "libfreetype6": "2.6.1",
        "libgcc1": "6.0.1",
        "libgdk-pixbuf2.0-0": "2.32.2",
        "libgfortran3": "5.4.0",
        "libgl1-mesa-glx": "17.2.8",
        "libglib2.0-0": "2.48.2",
        "libglu1-mesa": "9.0.0",
        "libgstreamer-plugins-base1.0-0": "1.8.3",
        "libgstreamer1.0-0": "1.8.3",
        "libgtk-3-0": "3.18.9",
        "libllvm3.8": "3.8",
        "liblzma5": "5.1.1alpha+20120614",
        "libnspr4": "4.13.1",
        "libnss3": "3.28.4",
        "libpango-1.0-0": "1.38.1",
        "libpangocairo-1.0-0": "1.38.1",
        "libpulse-mainloop-glib0": "8.0",
        "libpulse0": "8.0",
        "libsqlite3-0": "3.11.0",
        "libssl1.0.0": "1.0.2g",
        "libstdc++6": "5.4.0",
        "libx11-6": "1.6.3",
        "libx11-xcb1": "1.6.3",
        "libxcb-glx0": "1.11.1",
        "libxcb-util1": "0.4.0",
        "libxcb-xfixes0": "1.11.1",
        "libxcb-xinerama0": "1.11.1",
        "libxcb1": "1.11.1",
        "libxcomposite1": "0.4.4",
        "libxcursor1": "1.1.14",
        "libxdamage1": "1.1.4",
        "libxext6": "1.3.3",
        "libxfixes3": "5.0.1",
        "libxi6": "1.7.6",
        "libxrandr2": "1.5.0",
        "libxrender1": "0.9.9",
        "libxtst6": "1.2.2",
        "zlib1g": "1.2.8.dfsg",
        "xdg-utils": "1.0",  # TODO: get right version
    },
    "18.04": {
        "libasound2": "1.1.3",
        "libatk1.0-0": "2.28.1",
        "libbz2-1.0": "1.0.6",
        "libc6": "2.27",
        "libcairo-gobject2": "1.15.10",
        "libcairo2": "1.15.10",
        "libcups2": "2.2.7",
        "libdbus-1-3": "1.12.2",
        "libdrm2": "2.4.91",
        "libegl1": "1.0.0",
        "libexpat1": "2.2.5",
        "libffi6": "3.2.1",
        "libfftw3-single3": "3.3.7",
        "libfontconfig1": "2.12.6",
        "libfreetype6": "2.8.1",
        "libgcc1": "8-20180414",
        "libgdk-pixbuf2.0-0": "2.36.11",
        "libgfortran4": "7.5.0",
        "libgl1": "1.0.0",
        "libglib2.0-0": "2.56.1",
        "libglu1-mesa": "9.0.0",
        "libgstreamer-plugins-base1.0-0": "1.14.1",
        "libgstreamer1.0-0": "1.14.1",
        "libgtk-3-0": "3.22.30",
        "liblzma5": "5.2.2",
        "libnspr4": "4.18",
        "libnss3": "3.35",
        "libosmesa6": "18.0.0",
        "libpango-1.0-0": "1.40.14",
        "libpangocairo-1.0-0": "1.40.14",
        "libpulse-mainloop-glib0": "11.1",
        "libpulse0": "11.1",
        "libsqlite3-0": "3.22.0",
        "libssl1.1": "1.1.0g",
        "libstdc++6": "8-20180414",
        "libx11-6": "1.6.4",
        "libx11-xcb1": "1.6.4",
        "libxcb-glx0": "1.13",
        "libxcb-util1": "0.4.0",
        "libxcb-xfixes0": "1.13",
        "libxcb-xinerama0": "1.13",
        "libxcb1": "1.13",
        "libxcomposite1": "0.4.4",
        "libxcursor1": "1.1.15",
        "libxdamage1": "1.1.4",
        "libxext6": "1.3.3",
        "libxfixes3": "5.0.3",
        "libxi6": "1.7.9",
        "libxrandr2": "1.5.1",
        "libxrender1": "0.9.10",
        "libxtst6": "1.2.3",
        "xdg-utils": "1.1.2",
        "zlib1g": "1.2.11.dfsg",
    },
    "20.04": {
        "libasound2": "1.2.2",
        "libatk1.0-0": "2.35.1",
        "libbz2-1.0": "1.0.8",
        "libc6": "2.31",
        "libcairo-gobject2": "1.16.0",
        "libcairo2": "1.16.0",
        "libcrypt1": "4.4.10",
        "libcups2": "2.3.1",
        "libdbus-1-3": "1.12.16",
        "libdrm2": "2.4.101",
        "libegl1": "1.3.1",
        "libexpat1": "2.2.9",
        "libffi7": "3.3",
        "libfftw3-single3": "3.3.8",
        "libfontconfig1": "2.13.1",
        "libfreetype6": "2.10.1",
        "libgcc-s1": "10-20200411",
        "libgdk-pixbuf2.0-0": "2.40.0+dfsg",
        "libgfortran5": "10-20200411",
        "libgl1": "1.3.1",
        "libglib2.0-0": "2.64.2",
        "libglu1-mesa": "9.0.1",
        "libgstreamer-plugins-base1.0-0": "1.16.2",
        "libgstreamer1.0-0": "1.16.2",
        "libgtk-3-0": "3.24.18",
        "liblzma5": "5.2.4",
        "libncursesw6": "6.2",
        "libnspr4": "4.25",
        "libnss3": "3.49.1",
        "libopenjp2-7": "2.3.1",
        "libosmesa6": "20.0.4",
        "libpango-1.0-0": "1.44.7",
        "libpangocairo-1.0-0": "1.44.7",
        "libpulse-mainloop-glib0": "13.99.1",
        "libpulse0": "13.99.1",
        "libsqlite3-0": "3.31.1",
        "libssl1.1": "1.1.1f",
        "libstdc++6": "10-20200411",
        "libtinfo6": "6.2",
        "libuuid1": "2.34",
        "libwayland-client0": "1.18.0",
        "libwayland-cursor0": "1.18.0",
        "libwayland-egl1": "1.18.0",
        "libwayland-server0": "1.18.0",
        "libx11-6": "1.6.9",
        "libx11-xcb1": "1.6.9",
        "libxcb-glx0": "1.14",
        "libxcb-icccm4": None,
        "libxcb-image0": None,
        "libxcb-keysyms1": None,
        "libxcb-render-util0": None,
        "libxcb-util1": "0.4.0",
        "libxcb-xinerama0": "1.14",
        "libxcb1": "1.14",
        "libxcomposite1": "0.4.5",
        "libxcursor1": "1.2.0",
        "libxdamage1": "1.1.5",
        "libxext6": "1.3.4",
        "libxfixes3": "5.0.3",
        "libxi6": "1.7.10",
        "libxkbcommon0": "0.10.0",
        "libxkbcommon-x11-0": None,
        "libxrender1": "0.9.10",
        "libxtst6": "1.2.3",
        "xdg-utils": "1.1.3",
        "zlib1g": "1.2.11.dfsg",
    },
    "22.04": {
        # TODO: update versions
        "libasound2": "1.2.6.1",
        "libatk1.0-0": "2.36.0",
        "libbz2-1.0": "1.0.8",
        "libc6": "2.35",
        "libcairo-gobject2": "1.16.0",
        "libcairo2": "1.16.0",
        "libcrypt1": "1:4.4.17",
        "libcups2": "2.3.1",
        "libdbus-1-3": "1.12.16",
        "libdrm2": "2.4.101",
        "libegl1": "1.3.1",
        "libexpat1": "2.2.9",
        "libffi7": "3.3",
        "libfftw3-single3": "3.3.8",
        "libfontconfig1": "2.13.1",
        "libfreetype6": "2.10.1",
        "libgcc-s1": "10-20200411",
        "libgdk-pixbuf2.0-0": "2.40.0+dfsg",
        "libgfortran5": "10-20200411",
        "libgl1": "1.3.1",
        "libglib2.0-0": "2.64.2",
        "libglu1-mesa": "9.0.1",
        "libgstreamer-plugins-base1.0-0": "1.16.2",
        "libgstreamer1.0-0": "1.16.2",
        "libgtk-3-0": "3.24.18",
        "liblzma5": "5.2.4",
        "libncursesw6": "6.2",
        "libnspr4": "4.25",
        "libnss3": "3.49.1",
        "libopenjp2-7": "2.3.1",
        "libosmesa6": "20.0.4",
        "libpango-1.0-0": "1.44.7",
        "libpangocairo-1.0-0": "1.44.7",
        "libpulse-mainloop-glib0": "13.99.1",
        "libpulse0": "13.99.1",
        "libsqlite3-0": "3.31.1",
        "libssl3": "3.0.2",
        "libstdc++6": "10-20200411",
        "libtinfo6": "6.2",
        "libuuid1": "2.34",
        "libwayland-client0": "1.18.0",
        "libwayland-cursor0": "1.18.0",
        "libwayland-egl1": "1.18.0",
        "libwayland-server0": "1.18.0",
        "libx11-6": "1.6.9",
        "libx11-xcb1": "1.6.9",
        "libxcb-glx0": "1.14",
        "libxcb-icccm4": None,
        "libxcb-image0": None,
        "libxcb-keysyms1": None,
        "libxcb-render-util0": None,
        "libxcb-util1": "0.4.0",
        "libxcb-xinerama0": "1.14",
        "libxcb1": "1.14",
        "libxcomposite1": "0.4.5",
        "libxcursor1": "1.2.0",
        "libxdamage1": "1.1.5",
        "libxext6": "1.3.4",
        "libxfixes3": "5.0.3",
        "libxi6": "1.7.10",
        "libxkbcommon0": "0.10.0",
        "libxkbcommon-x11-0": None,
        "libxrender1": "0.9.10",
        "libxtst6": "1.2.3",
        "xdg-utils": "1.1.3",
        "zlib1g": "1.2.11.dfsg",
    },
}

def main():
    """main program"""
    # assume ChimeraX.app already has appropriate binaries
    error = False
    os_version = None
    build = 'release'
    if len(sys.argv) >= 2:
        os_version = sys.argv[1]
    else:
        error = True
    if len(sys.argv) >= 3:
        build = sys.argv[2]
        if build not in ['release', 'candidate', 'daily', 'techpreview']:
            error = True
    if error or len(sys.argv) > 3 or os_version not in UBUNTU_DEPENDENCIES:
        print(f'Usage: {sys.argv[0]} ubuntu-version [build-type]', file=sys.stderr)
        print('  Supported Ubuntu versions are:', ', '.join(UBUNTU_DEPENDENCIES.keys()),
              file=sys.stderr)
        print('  Build-type is one of "release", "candidate", or "daily"', file=sys.stderr)
        raise SystemExit(2)
    dependencies = UBUNTU_DEPENDENCIES[os_version]
    output = subprocess.check_output([
        CHIMERAX_BIN, "--nocolor", "--version"], stderr=subprocess.DEVNULL).decode()
    full_version = [line for line in output.split('\n') if 'version:' in line]
    if not full_version:
        print("Not able to determine version number")
        raise SystemExit(1)
    full_version = full_version[0].split(':', maxsplit=1)[1].strip()
    version_number, version_date = full_version.split(maxsplit=1)
    from packaging.version import Version
    version = Version(version_number)
    version_date = version_date[1:-1].replace('-', '.')
    pkg_name = f"{app_author.lower()}-{app_name.lower()}"
    bin_name = app_name.lower()  # name of command in /usr/bin
    if build == 'daily':
        # daily build, version is date
        version = version_date
        pkg_name += "-daily"
        bin_name += "-daily"
    elif build == 'techpreview':
        # like daily build, version is date
        version = version_date
        pkg_name += "-techpreview"
        bin_name += "-techpreview"
    elif build == 'release':
        # release build
        version = version.base_version
    else:
        # candidate build
        version = f"{version.base_version}+rc{version_date}"
    version = f"{version}ubuntu{os_version}"
    deb_name = f"{pkg_name}-{version}"  # name of .deb file

    # print('full_version:', repr(full_version))
    # print('version_number:', version_number)
    # print('version_date:', version_date)
    # print('deb_name:', deb_name)
    # print('pkg_name:', pkg_name)
    # print('bin_name:', bin_name)

    os.umask(0o22)  # turn off group and other writability
    pkg_root = f"{deb_name}"
    os.mkdir(pkg_root)
    debian_dir = f"{pkg_root}/DEBIAN"
    os.mkdir(debian_dir)
    make_control_file(debian_dir, pkg_name, version, dependencies)
    make_postinst(debian_dir, pkg_name, bin_name)
    make_prerm(debian_dir, pkg_name, bin_name)
    doc_dir = f"{pkg_root}/usr/share/doc/{pkg_name}"
    os.makedirs(doc_dir)
    make_copyright_file(doc_dir)
    make_changelog_file(doc_dir)
    # add_readme(doc_dir)

    copy_app(pkg_root, pkg_name)
    make_bin(pkg_root, pkg_name, bin_name)
    src = f'{app_name}.app/share/man/man1/{app_name}.1'
    make_man_file(src, pkg_root, bin_name)

    subprocess.check_call(['/bin/chmod', '-R', 'a+rX,go-w', pkg_root])

    dst_dir = f'./ubuntu-{os_version}'
    os.makedirs(dst_dir, exist_ok=True)

    # fakeroot environment file
    frenv = tempfile.NamedTemporaryFile(prefix='frenv-', suffix='.db')
    subprocess.check_call([
        '/usr/bin/fakeroot',
        '-s', frenv.name,
        '--',
        '/bin/chown',
        '-hRP',
        '0.0',
        pkg_root
    ])
    subprocess.check_call([
        '/usr/bin/fakeroot',
        '-i', frenv.name,
        '--',
        '/usr/bin/dpkg-deb',
        '--build',
        pkg_root,
        dst_dir
    ])

def copy_app(pkg_root, pkg_name):
    """Copy application to correct place in package hierarchy"""
    import shutil
    src = f'{app_name}.app'
    dst_dir = f'{pkg_root}/{INST_DIR}'
    dst = f'{pkg_root}/{INST_DIR}/{pkg_name}'
    os.makedirs(dst_dir)
    shutil.copytree(src, dst, symlinks=True)

    # cleanup -- remove __pycache__ directories
    cache_dirs = subprocess.check_output([
        '/usr/bin/find',
        dst,
        '-name', '__pycache__'
        ]).strip().decode().split('\n')
    for d in cache_dirs:
        shutil.rmtree(d)

    # cleanup -- remove python shell scripts
    for fn in os.listdir(f'{dst}/bin'):
        filename = f'{dst}/bin/{fn}'
        if not os.path.isfile(filename):
            continue
        with open(filename, 'rb') as f:
            header = f.read(64)
        if header[0:2] != b'#!':
            continue
        program = header[2:].lstrip()
        if program.startswith(b'/bin/') or \
                program.startswith(b'/usr/bin/'):
            continue
        os.remove(filename)

def make_bin(pkg_root, pkg_name, bin_name):
    """Make symbolic link to executable"""
    bin_dir = f'{pkg_root}/usr/bin'
    os.makedirs(bin_dir, exist_ok=True)
    os.symlink(f'../../{INST_DIR}/{pkg_name}/bin/{app_name}', f'{bin_dir}/{bin_name}')

def make_control_file(debian_dir, pkg_name, version, dependencies):
    """Make control file"""
    # see "man 1 deb-control"
    if dependencies is not None:
        deps = list(dependencies)
    else:
        deps = []
    deps.insert(0, 'ffmpeg')
    depends = ', '.join(deps)
    with open(f"{debian_dir}/control", 'w') as f:
        print(textwrap.dedent(f"""\
            Package: {pkg_name}
            Version: {version}
            Architecture: amd64
            Depends: {depends}
            Maintainer: Chimera Staff <chimera-staff@cgl.ucsf.edu>
            Description: molecular visualization
             UCSF ChimeraX (or simply ChimeraX) is the next-generation
             molecular visualization program from the Resource for Biocomputing
             Visualization, and Informatics (RBVI), following UCSF Chimera.
             ChimeraX can be downloaded free of charge for academic, government
             nonprofit, and personal use. Commercial users, please see licensing.
            Homepage: https://www.rbvi.ucsf.edu/chimerax/
            Bugs: mailto:chimerax-bugs@cgl.ucsf.edu
            Section: contrib/science
            Priority: optional
            Suggests: opencl-icd
            Tag: science::visualisation, science::modelling, field::biology, field::chemistry,
             field::biology:structural, field::biology:bioinformatics,
             biology::nucleic-acids, biology::peptidic,
             use::viewing, use::analysing,
             scope::application, x11::application, role::program,
             interface::3d, interface::graphical, interface::commandline,
             implemented-in::c++, implemented-in::python, uitoolkit::qt,
             network::client
            """), file=f)

def make_postinst(debian_dir, pkg_name, bin_name):
    # see "man 1 deb-postinst"
    with open(f"{debian_dir}/postinst", 'w') as f:
        print(textwrap.dedent(f"""\
            #!/bin/sh
            set -e
            case "$1" in
                configure)
                    echo "Install desktop menu and associated mime types"
                    {bin_name} --exit --nogui --silent --cmd 'linux xdg-install system true'
                    echo "Precompiling Python packages"
                    ({bin_name} -m compileall /{INST_DIR}/{pkg_name} || exit 0)
                ;;
            esac
            """), file=f)
        os.fchmod(f.fileno(), 0o755)

def make_prerm(debian_dir, pkg_name, bin_name):
    # see "man 1 deb-prerm"
    with open(f"{debian_dir}/prerm", 'w') as f:
        print(textwrap.dedent(f"""\
            #!/bin/sh
            set -e
            echo "Deregister desktop menu and associated mime types"
            {bin_name} --exit --nogui --silent --cmd 'linux xdg-uninstall system true'
            echo "Remove Python cache files"
            find /{INST_DIR}/{pkg_name} -name __pycache__ -print0 | xargs -0 /bin/rm -rf
            """), file=f)
        os.fchmod(f.fileno(), 0o755)

def make_copyright_file(doc_dir):
    """Copy the copyright file"""
    # copyright file
    with open("copyright.txt") as f:
        our_copyright = f.read()
    with open(f"{doc_dir}/copyright", 'w') as f:
        print(textwrap.dedent("""\
            Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/

            Upstream-Name: ChimeraX
            Upstream-Contact: chimerax@rbvi.ucsf.edu
            Source: https://www.rbvi.ucsf.edu/chimerax/

            Disclaimer:  Package is maintained by the ChimeraX team
              Copyrights for embedded code are given in the documentation
              in INSTALLDIR/share/docs/embeded.html

            Comment:

              The computer code and documentation that comprises UCSF ChimeraX is protected
              by copyrights held by The Regents of the University of California ("The Regents")
              and is licensed for non-commercial use at no cost by The Regents.

              COMMERCIAL USE OF UCSF CHIMERAX IS COVERED BY A SEPARATE WRITTEN LICENSE AGREEMENT.
              Commercial licensing costs are tier-based, depending on the number of users.
              Please email chimerax@cgl.ucsf.edu
              if you are interested in using ChimeraX for commercial purposes.

            License: free for non-commercial use
            """), file=f)
        for line in our_copyright.split('\n'):
            if line.startswith("=== UCSF ChimeraX"):
                continue
            print(' ', line, file=f)

def gzip(filename):
    """Gzip without saving timestamp (nor name) so it is reproducible"""
    subprocess.check_call(['gzip', '-n9', filename])

def make_changelog_file(doc_dir):
    """Copy the change log file"""
    filename = f"{doc_dir}/changelog"
    with open(filename, 'wt') as f:
        print("See https://www.rbvi.ucsf.edu/trac/ChimeraX/wiki/ChangeLog", file=f)
    gzip(filename)

def make_man_file(src_file, pkg_root, bin_name):
    base, ext = os.path.splitext(src_file)
    if not ext or not os.path.exists(src_file):
        return
    section = ext[1:]
    man_dir = f'{pkg_root}/usr/share/man/man{section}'
    man_file = f'{man_dir}/{bin_name}.{section}'
    os.makedirs(man_dir, exist_ok=True)

    with open(src_file, 'rb') as f:
        src = f.read()
    with open(man_file, 'wb') as f:
        f.write(src)
    gzip(man_file)

if __name__ == '__main__':
    main()


from . import views
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('user/profile/', views.UserProfileView.as_view(), name="user-profile"),
    path('update-profile/', views.MyUserProfileUpdateView.as_view(), name='update-profile'),
    path('create-language/', views.LanguageCreateView.as_view(), name='create-language'),
    path('update-language/<int:id>', views.LanguageUpdateView.as_view(), name='update-language'),
    path('delete-language/<int:id>', views.LanguageDeleteView.as_view(), name='delete-language'),
    path('create-skill/', views.SkillCreateView.as_view(), name='create-skill'),
    path('update-skill/<int:id>', views.SkillUpdateView.as_view(), name='update-skill'),
    path('delete-skill/<int:id>', views.SkillDeleteView.as_view(), name='delete-skill'),
    path('create-education/', views.EducationCreateView.as_view(), name='create-education'),
    path('update-education/<int:id>', views.EducationUpdateView.as_view(), name='update-education'),
    path('delete-education/<int:id>', views.EducationDeleteView.as_view(), name='delete-education'),
    path('create-certificate/', views.CertificateCreateView.as_view(), name='create-certificate'),
    path('update-certificate/<int:id>', views.CertificateUpdateView.as_view(), name='update-certificate'),
    path('delete-certificate/<int:id>', views.CertificateDeleteView.as_view(), name='delete-certificate'),

              ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


import pandas as pd
import pyranges as pr
import numpy as np
import matplotlib.pyplot as plt
import collections
from collections import defaultdict
from os import path
import igraph as ig
from itertools import combinations

pd.set_option('display.max_columns', None)
desired_width = 1000
pd.set_option('display.width', desired_width)
pd.set_option('display.max_colwidth', 100)
np.set_printoptions(linewidth=desired_width)
pd.options.mode.chained_assignment = None

def algtöötlus(failinimi, salvesta, vaatleme_kromosoome, tüüp):
    """
    Failist andmete sisselugemine, veergude lisamine ja kustutamine ning kromosoomipõhiselt failidesse salvestamine.
    :param failinimi: vaadeldava faili nimetus
    :param salvesta: faililaiend salvestamiseks
    :param vaatleme_kromosoome: mitmenda kromosoomini vaadeldakse
    :param tüüp: vaadeldav protsess (geeniekspressioon, transkriptsioon, splaissimine, promootor, terminaator)
    """
    tabel = pd.read_csv(r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\eraldi\\" + failinimi, sep="	")  # loeb faili sisse
    tabel = tabel[['phenotype_id', 'chr', 'cs_id', 'pip', 'cs_size', 'variant_id']]  # mis veerud jätta alles
    tabel = tabel.rename(columns={'chr': 'Chromosome'})
    tabel = pd.concat([tabel, tabel["variant_id"].str.split("_", expand=True)[1].rename("Start")], axis=1)  # uus veerg Start
    tabel["Start"] = pd.to_numeric(tabel["Start"])
    tabel['End'] = tabel['Start'] + 1  # Pyranges puhul peab mingi algus ja lõppkoht olema
    tabel = tabel[['phenotype_id', 'variant_id', 'cs_id', 'Chromosome', 'Start', 'End', 'pip', 'cs_size']]  # mis veerud jätta alles

    # txrev faili puhul tuleb splaissimine, promootori ja terminaatori kasutuse andmed üksteisest eraldada
    if tüüp == 2:
        tabel = tabel[tabel['phenotype_id'].str.contains("contained")]

    elif tüüp == 3:
        tabel = tabel[tabel['phenotype_id'].str.contains("upstream")]

    elif tüüp == 4:
        tabel = tabel[tabel['phenotype_id'].str.contains("downstream")]

    tabel["cs_id"] = tabel["cs_id"] + "_" + failinimi.split(".purity_filtered.txt")[0]  # lisame cs_id veerule andmebaasi nime juurde

    # teeme igale kromosoomile oma tabeli
    tabelid = list(range(vaatleme_kromosoome))

    for i in range(vaatleme_kromosoome):  # kirjutame iga kromosoomi oma faili
        tabelid[i] = (tabel[(tabel['Chromosome'] == 1 + i)])
        if path.exists(
                r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\koos\\" + salvesta + str(i + 1) + ".csv"):
            tabelid[i].to_csv(
                r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\koos\\" + salvesta + str(i + 1) + ".csv",
                mode='a', index=False, header=False)
        else:
            tabelid[i].to_csv(
                r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\koos\\" + salvesta + str(i + 1) + ".csv",
                mode='a', index=False)  # esimene kord salvestame koos veerunimetustega

def ülekatete_leidmine(tabel, tüüp, laiend, cs_suurus, kromosoom, transkript_sõnastik):
    """
    Tabelist ülekatete leidmine kasutades Pyranges klasterdamist. Koostab ühendatud komponente, kus tippudeks on CSid ja servad on nende vahel siis kui need jagavad omavahel samu variante
    :param tabel: mis andeid analüüsitakse
    :param tüüp: vaadeldav protsess (geeniekspressioon, transkriptsioon, splaissimine, promootor, terminaator)
    :param laiend: mis laiendiga faili sisse lugeda
    :param cs_suurus: maksimaalne CS suurus
    :param kromosoom: mitmendat kromosoomi vaatab
    :param transkript_sõnastik: kui tüüp = transkript, siis sisaldab sõnastikku, et saada kätte geeni_id, muidu tühi
    :return: sõnastik, mitu geeni iga komponent sisaldab
    """
    tabel = tabel[tabel['cs_size'] <= cs_suurus]  # credible set maksimaalne suurus

    if tüüp == 2 or tüüp == 3 or tüüp == 4:
        tabel['phenotype_id'] = tabel['phenotype_id'].str.split(".", expand=True)  # jätab alles vaid geeni_id

    if tüüp == 5:
        tabel["gene_id"] = tabel.apply(lambda row: transkript_sõnastik[row.phenotype_id], axis=1)  # saame igale phenotype_id-le vastava geeni_id
    else:
        tabel["gene_id"] = tabel.phenotype_id  # muudel juhtudel on geeni_id teada
    tabel.drop(["phenotype_id"], axis=1)

    pr_tabel = pr.PyRanges(tabel)  # teeb PyRanges tabeli
    cluster = pr_tabel.cluster(by="Start")  # klasterdab asukohapõhiselt
    cluster_df = cluster.df  # teeb Pandas DataFrame'i

    cluster_df.to_csv(r"tulemused\\" + str(cs_suurus) + "\\" + laiend + "cluster_tulemus.csv", mode='a', index=False)  # kirjutab faili

    # annab iga variandi kohta teada, mitmes CSis esineb
    # merge = pr_tabel.merge(count=True, by="Start")
    # merge_df = merge.df
    # merge_df.to_csv(r"tulemused\\" + str(cs_suurus) + "\\" + laiend + "merge_tulemus.csv", mode='a', index=False) # kirjutab faili

    # saab kätte unikaalsed CSide nimetused
    cs_id = set((tabel.cs_id.values))

    g = ig.Graph()  # loob graafi

    # tippude loomine
    g.add_vertices(len(cs_id))
    eelmine_klaster = -1
    i = 0
    cs_id_indeks = {}  # tippude (CSide) indeksite salvestamine
    for cs in cs_id:
        cs_id_indeks[cs] = i
        i += 1

    eelmise_indeks = -1

    for i, rida in cluster_df.iterrows():  # vaatab klasterdamisest saadud tabeli reahaaval läbi
        klaster = rida.Cluster
        indeks = cs_id_indeks[rida.cs_id]

        try:  # kui selle tipu (CSi) kohta on juba tipp loodud
            g.vs[indeks]["positions"].append(rida.Start)  # lisab veel ühe variandi asukoha, mis kuulub sellesse CSi

        except:  # muidu lisatakse tipp ja selle info
            g.vs[indeks]['cs'] = rida.cs_id
            g.vs[indeks]["positions"] = [rida.Start]
            g.vs[indeks]["gene_id"] = rida.gene_id

        # servade lisamine
        # servad lisatakse ühes klastris vaid kahe järjestikuse CSi vahel (ei muuda lõpus tulemust ja muudab arvutuskäigu kiiremaks
        if klaster == eelmine_klaster:
            g.add_edge(indeks, eelmise_indeks)
        else:
            eelmine_klaster = klaster
        eelmise_indeks = indeks

        # kui ikkagi vaja saada servad kõikide CSide vahel kätte
    #     lisa_servad = [] # milliste CSide vahel teha servi
    #     if (klaster == eelmine_klaster): # kuulub eelnevaga samasse klastrisse
    #         lisa_servad.append(indeks)
    #     else:
    #         # vaadeldav on uus klaster, teeb eelmise klastri servad ära
    #         for servad in combinations(lisa_servad, 2):
    #             g.add_edge(servad[0], servad[1])
    #
    #         # muudab praeguse klastri informatsiooni
    #         lisa_servad = [indeks] # vaadeldava CSi indeks salvestatakse listi
    #         eelmine_klaster = klaster
    #
    # # joonistab servad viimase klastri kohta ära
    # for servad in combinations(lisa_servad, 2):
    #     g.add_edge(servad[0], servad[1])

    komponendid = g.clusters()  # leiab ühendatud komponendid (sidusad alamgraafid)
    komponentide_geenid = []  # millised geenid kuuluvad samasse ühendatud komponenti

    # leiab, mis geenid kuuluvad samadesse ühendatud komponentidesse
    for komponent in komponendid:
        geenid = set()
        # leiab kõik komponendi geenid
        for tipp in komponent:
            geenid.add(g.vs[tipp]["gene_id"])

        komponentide_geenid.append(geenid)  # mis geenid kuuluvad samasse komponenti

    # salvestab leitud ülekatted
    ülekatted = defaultdict(int)
    for komponent in komponentide_geenid:
        ülekatted[len(komponent)] += 1

    # andmete faili salvestamine
    with open(file=r"tulemused\\" + str(cs_suurus) + "\\" + laiend + "komponentidesse_kuuluvad_geenid.txt",
              mode='a') as f:
        f.write(str(kromosoom) + ": " + str(komponentide_geenid) + "\n")

    return ülekatted

def main(tüüp, failinimede_list):
    """
    Leiab, kui palju on vaadeldava protsessi puhul üht või mitut geeni sisaldavaid ühendatud komponente.
    Prindib huvipakkuva informatsiooni välja.
    :param tüüp: vaadeldav protsess (geeniekspressioon, transkriptsioon, splaissimine, promootor, terminaator)
    :param failinimede_list: mis failid vaadeldakse läbi
    :return: kui palju oli vaid üht geeni sisaldavad ühendatud komponente ja kui palju oli rohkem kui üht geeni sisaldavaid komponente
    """
    vaatleme_kromosoome = 22
    ülekatteid_kokku = defaultdict(int)  # kui suuri ülekatteid on leitud

    if tüüp == 1:  # geeniekspressioon
        laiend = "ge"
        salvesta = "ge_"
        nimetus = "Geeniekspressiooniga"
    elif tüüp == 2:  # RNA splaissimine
        laiend = "txrev"
        salvesta = "txrev_"
        nimetus = "RNA splaissimisega"
    elif tüüp == 3:  # promootor
        laiend = "txrev"
        salvesta = "upstream_"
        nimetus = "Promootori kasutusega "
    elif tüüp == 4:  # terminaator
        laiend = "txrev"
        salvesta = "downstream_"
        nimetus = "Terminaatori kasutusega"
    elif tüüp == 5:  # transkriptsioon
        laiend = "tx"
        salvesta = "tx_"
        nimetus = "Transkriptidega"
    else:
        laiend, salvesta, nimetus = "muu"

    # seda peab vaid 1 kord jooksutama, et salvestada kogu informatsioon kromosoomipõhiselt
    if not path.exists(
            r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\koos\\" + salvesta + "1.csv"):  # pole läbi jooksutatud
        for fail in failinimede_list:  # algtöötleme kõik andmed läbi ning salvestame failidesse
            algtöötlus(fail + laiend + ".purity_filtered.txt", salvesta, vaatleme_kromosoome, tüüp)

    if tüüp == 5:
        # vajalik transcriptide fenotüüpide teisendamiseks geenideks
        metadata = pd.read_csv(
            r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\eraldi\transcript_usage_Ensembl_96_phenotype_metadata.tsv",
            sep='\t', header=0)  # loeb metadata sisse
        metadata = metadata[["phenotype_id", "gene_id"]]
        transkript_sõnastik = metadata.set_index("phenotype_id").to_dict()["gene_id"]
    else:
        transkript_sõnastik = {}

    # analüüsib kromosoomipõhiselt kõik läbi
    for i in range(vaatleme_kromosoome):
        tabel = pd.read_csv(r"C:\Users\pr\Documents\kool\6 semester 2020-21\Baka\andmed\koos\\" + salvesta + str(i + 1) + ".csv")  # loeb tabeli sisse
        ülekatteid = ülekatete_leidmine(tabel, tüüp, salvesta, cs_suurus, i + 1, transkript_sõnastik)  # leiab ülekatted

        for ülekate in ülekatteid.keys():
            ülekatteid_kokku[ülekate] += ülekatteid[ülekate]  # salvestab, palju on ülekatteid kokku leitud

    mitu_geeni = 0  # palju on rohkem kui üht geeni sisaldavaid komponente
    for ülekate in ülekatteid_kokku.keys():
        if ülekate > 1:
            mitu_geeni += ülekatteid_kokku[ülekate]

    ülekatteid_kokku = dict(collections.OrderedDict(sorted(ülekatteid_kokku.items())))  # järjestab sõnastiku

    komponente_kokku = 0  # komponentide arv kokku
    paarid = [(k, ülekatteid_kokku[k]) for k in ülekatteid_kokku]  # paarid: geenide arv komponendis ja kui palju selliseid komponente leidus
    for paar in paarid:  # leiab komponentide arvu
        komponente_kokku += paar[0] * paar[1]

    print(nimetus + " leitud ülekatted:", dict(ülekatteid_kokku))
    print("Ühes ühendatud komponendis geene: üks -", ülekatteid_kokku[1], " mitu -", mitu_geeni)
    print("Vaid üht geeni sisaldavate ühendatud komponentide suhe kogu komponentide arvuga: ",
          (round((ülekatteid_kokku[1] / (ülekatteid_kokku[1] + mitu_geeni)) * 100, 3)), "%")
    print("Ühendatud komponente kokku ", komponente_kokku)
    ülekatteid_üldine = {1: ülekatteid_kokku[1], 2: mitu_geeni}

    return ülekatteid_üldine

# kõik töös kasutatud andmed
failinimede_list = ["Alasoo_2018.macrophage_IFNg+Salmonella_", "Alasoo_2018.macrophage_IFNg_",
                    "Alasoo_2018.macrophage_naive_", 'BLUEPRINT_PE.T-cell_', 'BLUEPRINT_SE.monocyte_',
                    'BLUEPRINT_SE.neutrophil_', 'BrainSeq.brain_', 'FUSION.adipose_naive_', 'FUSION.muscle_naive_',
                    'GENCORD.fibroblast_', 'GENCORD.LCL_', 'GENCORD.T-cell_', 'GEUVADIS.LCL_',
                    'GTEx.adipose_subcutaneous_', 'GTEx.adipose_visceral_', 'GTEx.adrenal_gland_', 'GTEx.artery_aorta_',
                    'GTEx.artery_coronary_', 'GTEx.artery_tibial_', 'GTEx.blood_', 'GTEx.brain_amygdala_',
                    'GTEx.brain_anterior_cingulate_cortex_', 'GTEx.brain_caudate_', 'GTEx.brain_cerebellar_hemisphere_',
                    'GTEx.brain_cerebellum_', 'GTEx.brain_cortex_', 'GTEx.brain_frontal_cortex_',
                    'GTEx.brain_hippocampus_', 'GTEx.brain_hypothalamus_', 'GTEx.brain_nucleus_accumbens_',
                    'GTEx.brain_putamen_', 'GTEx.brain_spinal_cord_', 'GTEx.brain_substantia_nigra_', 'GTEx.breast_',
                    'GTEx.colon_sigmoid_', 'GTEx.colon_transverse_', 'GTEx.esophagus_gej_', 'GTEx.esophagus_mucosa_',
                    'GTEx.esophagus_muscularis_', 'GTEx.fibroblast_', 'GTEx.heart_atrial_appendage_',
                    'GTEx.heart_left_ventricle_', 'GTEx.LCL_', 'GTEx.liver_', 'GTEx.lung_',
                    'GTEx.minor_salivary_gland_', 'GTEx.muscle_', 'GTEx.nerve_tibial_', 'GTEx.ovary_', 'GTEx.pancreas_',
                    'GTEx.pituitary_', 'GTEx.prostate_', 'GTEx.skin_not_sun_exposed_', 'GTEx.skin_sun_exposed_',
                    'GTEx.small_intestine_', 'GTEx.spleen_', 'GTEx.stomach_', 'GTEx.testis_', 'GTEx.thyroid_',
                    'GTEx.uterus_', 'GTEx.vagina_', 'HipSci.iPSC_', 'Lepik_2017.blood_',
                    'Nedelec_2016.macrophage_Listeria_', 'Nedelec_2016.macrophage_naive_',
                    'Nedelec_2016.macrophage_Salmonella_', 'Quach_2016.monocyte_IAV_', 'Quach_2016.monocyte_LPS_',
                    'Quach_2016.monocyte_naive_', 'Quach_2016.monocyte_Pam3CSK4_', 'Quach_2016.monocyte_R848_',
                    'ROSMAP.brain_naive_', 'Schmiedel_2018.B-cell_naive_', 'Schmiedel_2018.CD4_T-cell_anti-CD3-CD28_',
                    'Schmiedel_2018.CD4_T-cell_naive_', 'Schmiedel_2018.CD8_T-cell_anti-CD3-CD28_',
                    'Schmiedel_2018.CD8_T-cell_naive_', 'Schmiedel_2018.monocyte_CD16_naive_',
                    'Schmiedel_2018.monocyte_naive_', 'Schmiedel_2018.NK-cell_naive_', 'Schmiedel_2018.Tfh_memory_',
                    'Schmiedel_2018.Th1-17_memory_', 'Schmiedel_2018.Th17_memory_', 'Schmiedel_2018.Th1_memory_',
                    'Schmiedel_2018.Th2_memory_', 'Schmiedel_2018.Treg_memory_', 'Schmiedel_2018.Treg_naive_',
                    'Schwartzentruber_2018.sensory_neuron_', 'TwinsUK.blood_', 'TwinsUK.fat_', 'TwinsUK.LCL_',
                    'TwinsUK.skin_', 'van_de_Bunt_2015.pancreatic_islet_']
# failinimede_list = ["HipSci.iPSC_", "Alasoo_2018.macrophage_IFNg+Salmonella_"]  # katsetamiseks

ülekatted = []
for cs in range(10, 101, 10):  # mis CSi vahemikus koodi läbi jooksutada
    cs_suurus = cs
    print("\n\nCS suurus:", cs_suurus)

    # ge
    print("\nGeeniekspressioon:")
    ge_ülekatteid = main(1, failinimede_list)

    # txrevise contained
    print("\nRNA splaissimine:")
    txrev_ülekatteid = main(2, failinimede_list)

    # txrevise upstream
    print("\nPromootor:")
    upstream_ülekatteid = main(3, failinimede_list)

    # txrevise downstream
    print("\nTerminaator:")
    downstream_ülekatteid = main(4, failinimede_list)

    # tx
    print("\nTranskript:")
    tx_ülekatteid = main(5, failinimede_list)

def joonista(ge, txrev, upstream, downstream, transcript):
    fig, (ax0) = plt.subplots(figsize=(8, 8))

    # tulba laius
    bar_width = 0.18

    # tulba kõrgus
    bars1 = list(ge.values())
    bars2 = list(txrev.values())
    bars3 = list(upstream.values())
    bars4 = list(downstream.values())
    bars5 = list(transcript.values())

    # tulba asukoht X-teljel
    r1 = np.arange(len(bars1))
    r2 = [x + bar_width for x in r1]
    r3 = [x + bar_width for x in r2]
    r4 = [x + bar_width for x in r3]
    r5 = [x + bar_width for x in r4]

    # tulpade joonistamine
    ax0.bar(r1, bars1, color='c', width=bar_width, edgecolor='white', label='Geeniekspressioon')
    ax0.bar(r2, bars2, color='salmon', width=bar_width, edgecolor='white', label='RNA splaissimine')
    ax0.bar(r3, bars3, color='orchid', width=bar_width, edgecolor='white', label='Promootor')
    ax0.bar(r4, bars4, color='lightgreen', width=bar_width, edgecolor='white', label='Terminaator')
    ax0.bar(r5, bars5, color='gold', width=bar_width, edgecolor='white', label='Transkript')

    ax0.set_title('Geenide arv ühendatud komponendis vastavalt uuritud protsessile, cs <=' + str(cs_suurus))

    # telgede pealkirjadA
    ax0.set_xlabel('Geenide arv klikis')
    ax0.set_ylabel('Klikkide arv')

    # grupeeritud tulpade nimetused
    ax0.set_xticks([r + (bar_width / 5) for r in range(len(bars1))])
    ax0.set_xticklabels(["üks", "mitu"])

    ax0.legend()
    plt.show()

joonista(ge_ülekatteid, txrev_ülekatteid, upstream_ülekatteid, downstream_ülekatteid, tx_ülekatteid)


from typing import List

import pandas as pd

from orange_cb_recsys.content_analyzer.field_content_production_techniques.embedding_technique.combining_technique import \
    CombiningTechnique, Centroid
from orange_cb_recsys.recsys.content_based_algorithm.content_based_algorithm import ContentBasedAlgorithm
from orange_cb_recsys.recsys.content_based_algorithm.classifier.classifiers import Classifier
from orange_cb_recsys.recsys.content_based_algorithm.exceptions import NoRatedItems, OnlyPositiveItems, \
    OnlyNegativeItems, NotPredictionAlg, EmptyUserRatings
from orange_cb_recsys.utils.load_content import get_rated_items, get_unrated_items, \
    get_chosen_items
from orange_cb_recsys.utils.const import recsys_logger

class ClassifierRecommender(ContentBasedAlgorithm):
    """
    Class that implements recommendation through a specified Classifier.
    It's a ranking algorithm so it can't do score prediction.

    USAGE:
        > # Interested in only a field representation, DecisionTree classifier from sklearn,
        > # threshold = 3 (Every item with rating >= 3 will be considered as positive)
        > alg = ClassifierRecommender({"Plot": 0}, SkDecisionTree(), 3)

        > # Interested in only a field representation, KNN classifier with custom parameters from sklearn,
        > # threshold = 3 (Every item with rating >= 3 will be considered as positive)
        > alg = ClassifierRecommender({"Plot": 0}, SkKNN(n_neighbors=3), 0)

        > # Interested in multiple field representations of the items, KNN classifier with custom parameters from
        > # sklearn, threshold = 3 (Every item with rating >= 3 will be considered as positive)
        > alg = ClassifierRecommender(
        >                             item_field={"Plot": [0, "tfidf"],
        >                                         "Genre": [0, 1],
        >                                         "Director": "doc2vec"},
        >                             classifier=SkKNN(n_neighbors=3),
        >                             threshold=3)

        > # After instantiating the ClassifierRecommender algorithm, pass it in the initialization of
        > # a CBRS and the use its method to calculate ranking for single user or multiple users:
        > cbrs = ContentBasedRS(algorithm=alg, ...)
        > cbrs.fit_rank(...)
        > ...
        > # Check the corresponding method documentation for more

       Args:
            item_field (dict): dict where the key is the name of the field
                that contains the content to use, value is the representation(s) id(s) that will be
                used for the said item. The value of a field can be a string or a list,
                use a list if you want to use multiple representations for a particular field.
            classifier (Classifier): classifier that will be used.
               Can be one object of the Classifier class.
            threshold (float): Threshold for the ratings. If the rating is greater than the threshold, it will be considered
                as positive
       """

    def __init__(self, item_field: dict, classifier: Classifier, threshold: float = None,
                 embedding_combiner: CombiningTechnique = Centroid()):
        super().__init__(item_field, threshold)
        self.__classifier = classifier
        self.__embedding_combiner = embedding_combiner
        self.__labels: list = None
        self.__rated_dict: dict = None

    def process_rated(self, user_ratings: pd.DataFrame, items_directory: str):
        """
        Function that extracts features from rated item and labels them.
        The extracted features will be later used to fit the classifier.

        Features and labels will be stored in private attributes of the class.

        IF there are no rated_items available locally or if there are only positive/negative
        items, an exception is thrown.

        Args:
            user_ratings (pd.DataFrame): DataFrame containing ratings of a single user
            items_directory (str): path of the directory where the items are stored
        Raises:
            NoRatedItems: exception raised when there isn't any item available locally
                rated by the user
            OnlyPositiveItems: exception raised when there are only positive items available locally
                for the user (Items that the user liked)
            OnlyNegativeitems: exception raised when there are only negative items available locally
                for the user (Items that the user disliked)
        """
        # Load rated items from the path
        rated_items = get_rated_items(items_directory, user_ratings)

        threshold = self.threshold
        if threshold is None:
            threshold = self._calc_mean_user_threshold(user_ratings)

        # Assign label and extract features from the rated items
        labels = []
        rated_dict = {}

        recsys_logger.info("Processing rated items")
        for item in rated_items:
            if item is not None:
                rated_dict[item] = self.extract_features_item(item)

                # This conversion raises Exception when there are multiple same to_id for the user
                score_assigned = float(user_ratings[user_ratings['to_id'] == item.content_id].score)
                if score_assigned >= threshold:
                    labels.append(1)
                else:
                    labels.append(0)

        if user_ratings.empty:
            raise EmptyUserRatings("The user selected doesn't have any ratings!")

        user_id = user_ratings.from_id.iloc[0]
        if len(rated_dict) == 0:
            raise NoRatedItems("User {} - No rated item available locally!".format(user_id))
        if 0 not in labels:
            raise OnlyPositiveItems("User {} - There are only positive items available locally!".format(user_id))
        elif 1 not in labels:
            raise OnlyNegativeItems("User {} - There are only negative items available locally!".format(user_id))

        self.__labels = labels
        self.__rated_dict = rated_dict

    def fit(self):
        """
        Fit the classifier specified in the constructor with the features and labels
        extracted with the process_rated() method.

        It uses private attributes to fit the classifier, so process_rated() must be called
        before this method.
        """
        recsys_logger.info("Fitting {} classifier".format(self.__classifier))
        self._set_transformer()

        rated_features = list(self.__rated_dict.values())

        # Fuse the input if there are dicts, multiple representation, etc.
        fused_features = self.fuse_representations(rated_features, self.__embedding_combiner)

        self.__classifier.fit(fused_features, self.__labels)

    def predict(self, user_ratings: pd.DataFrame, items_directory: str,
                filter_list: List[str] = None) -> pd.DataFrame:
        """
        ClassifierRecommender is not a score prediction algorithm, calling this method will raise
        the 'NotPredictionAlg' exception!
        Raises:
            NotPredictionAlg
        """
        raise NotPredictionAlg("ClassifierRecommender is not a Score Prediction Algorithm!")

    def rank(self, user_ratings: pd.DataFrame, items_directory: str, recs_number: int = None,
             filter_list: List[str] = None) -> pd.DataFrame:
        """
        Rank the top-n recommended items for the user. If the recs_number parameter isn't specified,
        All unrated items will be ranked (or only items in the filter list, if specified).

        One can specify which items must be ranked with the filter_list parameter,
        in this case ONLY items in the filter_list parameter will be ranked.
        One can also pass items already seen by the user with the filter_list parameter.
        Otherwise, ALL unrated items will be ranked.

        Args:
            user_ratings (pd.DataFrame): DataFrame containing ratings of a single user
            items_directory (str): path of the directory where the items are stored
            recs_number (int): number of the top items that will be present in the ranking, if None
                all unrated items will be ranked
            filter_list (list): list of the items to rank, if None all unrated items will be ranked
        Returns:
            pd.DataFrame: DataFrame containing one column with the items name,
                one column with the rating predicted, sorted in descending order by the 'rating' column
        """
        # Load items to predict
        if filter_list is None:
            items_to_predict = get_unrated_items(items_directory, user_ratings)
        else:
            items_to_predict = get_chosen_items(items_directory, filter_list)

        # Extract features of the items to predict
        id_items_to_predict = []
        features_items_to_predict = []
        for item in items_to_predict:
            if item is not None:
                id_items_to_predict.append(item.content_id)
                features_items_to_predict.append(self.extract_features_item(item))

        recsys_logger.info("Calculating rank")
        if len(id_items_to_predict) > 0:
            # Fuse the input if there are dicts, multiple representation, etc.
            fused_features_items_to_pred = self.fuse_representations(features_items_to_predict, self.__embedding_combiner)

            score_labels = self.__classifier.predict_proba(fused_features_items_to_pred)
        else:
            score_labels = []

        result = {'to_id': [], 'score': []}

        for item_id, score in zip(id_items_to_predict, score_labels):
            result['to_id'].append(item_id)
            result['score'].append(score[1])

        result = pd.DataFrame(result, columns=['to_id', 'score'])

        result.sort_values(by=['score'], ascending=False, inplace=True)

        rank = result[:recs_number]

        return rank


from utils import List, test

# @lc code=start

class Solution:
    def dominantIndex(self, nums: List[int]) -> int:
        mx, pm, ix = 0, 0, -1
        for i, el in enumerate(nums):
            if el > mx:
                pm, mx, ix = mx, el, i
            elif el > pm:
                pm = el
        return ix if pm * 2 <= mx else -1

# @lc code=end
f = Solution().dominantIndex

test(-1, f, [0, 0, 3, 2])
test(-1, f, [])
test(1, f, [3, 6, 1, 0])
test(-1, f, [1, 2, 3, 4])
test(0, f, [1])