# -*- coding: utf-8 -*-
"""
/***************************************************************************
                                 A QGIS plugin
 A plugin for using Google drive sheets as QGIS layer shared between concurrent users
 portions of code are from https://github.com/g-sherman/pseudo_csv_provider
                              -------------------
        begin                : 2015-03-13
        git sha              : $Format:%H$
        copyright            : (C)2017 Enrico Ferreguti (C)2015 by GeoApt LLC gsherman@geoapt.com
        email                : enricofer@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from __future__ import print_function
from __future__ import absolute_import

from future import standard_library
standard_library.install_aliases()
from builtins import zip
from builtins import str
from builtins import range
from builtins import object

__author__ = 'enricofer@gmail.com'
__date__ = '2017-03-24'
__copyright__ = 'Copyright 2017, Enrico Ferreguti'


import csv
import shutil
import os
import io
import sys
import io
import json
import collections
import base64
import zlib
import _thread
import traceback
import inspect
from time import sleep

from tempfile import NamedTemporaryFile
from qgis.PyQt import  QtGui
from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtWidgets import QProgressBar, QAction, QWidget, QApplication
from qgis.PyQt.QtGui import QIcon, QPixmap, QImage, QColor, QPainter, QRegion, QBrush, QBitmap
from qgis.PyQt.QtCore import QObject, pyqtSignal, QThread, QVariant, QSize, Qt

from qgis.core import (QgsVectorLayer, QgsFeature, QgsGeometry, QgsExpression, QgsField, QgsMapLayer, QgsMapRendererParallelJob,
                       QgsFeatureRequest, QgsMessageLog,QgsCoordinateReferenceSystem, Qgis, QgsReadWriteContext, QgsProject,
                       QgsCoordinateTransform, QgsCoordinateReferenceSystem, QgsPointXY, QgsRectangle, QgsMapSettingsUtils)

from qgis.gui import QgsMessageBar, QgsMapCanvas

import qgis.core

logger = lambda msg: QgsMessageLog.logMessage("(%s.%s)  %s" % (inspect.stack()[1][0].f_locals['self'].__class__.__name__, inspect.stack()[1][3], msg), 'Google Drive Provider', 0)

from .services import pack, unpack, google_authorization, service_drive, service_spreadsheet

#from mapboxgl.mapboxgl import toMapboxgl

from .extlibs.bridgestyle.qgis import layerStyleAsMapbox

from .utils import slugify


class progressBar(object):
    def __init__(self, parent, msg = ''):
        '''
        progressBar class instatiation method. It creates a QgsMessageBar with provided msg and a working QProgressBar
        :param parent:
        :param msg: string
        '''
        self.iface = parent.iface
        widget = self.iface.messageBar().createMessage("GooGIS plugin:",msg)
        progressBar = QProgressBar()
        progressBar.setRange(0,0) #(1,steps)
        progressBar.setValue(0)
        progressBar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        widget.layout().addWidget(progressBar)
        QApplication.processEvents()
        self.iface.messageBar().pushWidget(widget, Qgis.Info, 50)
        QApplication.processEvents()

    def stop(self, msg = ''):
        '''
        the progressbar is stopped with a succes message
        :param msg: string
        :return:
        '''
        self.iface.messageBar().clearWidgets()
        message = self.iface.messageBar().createMessage("GooGIS plugin:",msg)
        self.iface.messageBar().pushWidget(message, Qgis.Success, 3)

class GoogleDriveLayer(QObject):
    """ Pretend we are a data provider """

    invalidEdit = pyqtSignal()
    deferredEdit = pyqtSignal()
    dirty = False
    restyled = False
    doing_attr_update = False
    geom_types = ("Point", "LineString", "Polygon","Unknown","NoGeometry")

    def __init__(self, parent, authorization, layer_name, spreadsheet_id = None, loading_layer = None, importing_layer = None, crs_def = None, geom_type = None, test = None, precision=17):
        '''
        Initialize the layer by reading the Google drive sheet, creating a memory
        layer, and adding records to it, optionally used fo layer export to google drive
        :param parent:
        :param authorization: google authorization object
        :param layer_name: the layer name
        :param spreadsheet_id: the spreadsheetId of the table to download and load as qgis layer; default to None
        :param loading_layer: the layer loading from project file; default to None
        :param importing_layer: the layer that is being imported; default to None
        :param test: used for testing
        '''

        super(GoogleDriveLayer, self).__init__()
        # Save the path to the file soe we can update it in response to edits
        self.test = test
        self.parent = parent
        self.iface = parent.iface
        self.precision = None
        bar = progressBar(self, 'loading google drive layer')
        self.service_drive = service_drive(authorization)
        self.client_id = authorization.client_id
        self.authorization = authorization
        if spreadsheet_id:
            self.spreadsheet_id = spreadsheet_id
            self.service_sheet = service_spreadsheet(authorization, self.spreadsheet_id)
            self.precision = self.service_sheet.precision()
        elif importing_layer:
            self.precision = precision
            layer_as_list = self.qgis_layer_to_list(importing_layer)
            logger("1")
            self.service_sheet = service_spreadsheet(authorization, new_sheet_name=importing_layer.name(),new_sheet_data=True)
            self.spreadsheet_id = self.service_sheet.spreadsheetId
            logger("2")
            self.service_sheet.set_crs(importing_layer.crs().authid())
            self.service_sheet.set_geom_type(self.geom_types[importing_layer.geometryType()])
            self.service_sheet.set_style_qgis(self.layer_style_to_xml(importing_layer))
            self.service_sheet.set_style_sld(self.SLD_to_xml(importing_layer))
            self.service_sheet.set_style_mapbox(self.layer_style_to_json(importing_layer))
            self.service_sheet.set_precision(precision)
            self.dirty = True
            self.saveFieldTypes(importing_layer.fields())  
            logger("5")
            self.service_sheet.upload_rows(layer_as_list)
            logger("6")

        self.service_sheet.lockedEntry.connect(self.rollbackRow)

        self.reader = self.service_sheet.get_sheet_values()
        self.header = self.reader[0]
        self.service_sheet.update_header()

        self.crs_def = self.service_sheet.crs()
        self.geom_type = self.service_sheet.geom_type()
        logger("LOADED GOOGLE SHEET LAYER: %s CRS_ID:%s GEOM_type:%s" % (self.service_sheet.name,self.crs_def, self.geom_type))
        # Build up the URI needed to create memory layer
        if loading_layer:
            self.lyr = loading_layer
            attrIds = [i for i in range (0, self.lyr.fields().count())]
            self.lyr.dataProvider().deleteAttributes(attrIds)
            self.lyr.updateFields()
        else:
            self.uri = self.uri = "Multi%s?crs=%s&index=yes" % (self.geom_type, self.crs_def)
            #logger(self.uri)
            self.lyr = QgsVectorLayer(self.uri, layer_name, 'memory')

        if importing_layer:
            logger("3")
            self.update_summary_sheet() 
            logger("4")
            self.saveMetadataState(importing_layer)

        '''
        #check if public layer
        meta = self.service_drive.getFileMetadata(self.spreadsheet_id)
        permissions = meta["metadata"]["permissions"]
        self.is_public_layer = False
        for permission in permissions:
            if permission["id"] == "anyoneWithLink":
                self.is_public_layer = True
                logger("is public layer")
        '''

        fields_types = self.service_sheet.get_line("ROWS", 1, sheet="settings")
        attributes = []
        for i in range(2,len(self.header)):
            if self.header[i][:8] != 'DELETED_':
                type_pack = fields_types[i].split("|")
                attributes.append(QgsField(name=self.header[i],type=int(type_pack[0]), len=int(type_pack[1]), prec=int(type_pack[2])))
                #self.uri += u'&field={}:{}'.format(fld.decode('utf8'), field_name_types[fld])
        self.lyr.dataProvider().addAttributes(attributes)
        self.lyr.updateFields()

        self.xml_to_layer_style(self.lyr,self.service_sheet.style())
        #disable memory layers save checking when closing project
        self.lyr.setCustomProperty("googleDriveId", self.spreadsheet_id)
        self.lyr.setCustomProperty("skipMemoryLayersCheck", 1)

        self.add_records()

        # Make connections
        self.makeConnections(self.lyr)

        # Add the layer the map
        if not loading_layer:
            QgsProject.instance().addMapLayer(self.lyr)
        
        self.lyr.setAbstract(self.service_sheet.abstract())
        self.lyr.gdrive_control = self

        bar.stop("Layer %s succesfully loaded" % layer_name)

    def makeConnections(self,lyr):
        '''
        The method handle default signal connections to the connected qgis memory layer
        :param lyr: qgis layer
        :return:
        '''
        self.deferredEdit.connect(self.apply_locks)
        lyr.editingStarted.connect(self.editing_started)
        lyr.editingStopped.connect(self.editing_stopped)
        lyr.committedAttributesDeleted.connect(self.attributes_deleted)
        lyr.committedAttributesAdded .connect(self.attributes_added)
        lyr.committedFeaturesAdded.connect(self.features_added)
        lyr.committedGeometriesChanges.connect(self.geometry_changed)
        lyr.committedAttributeValuesChanges.connect(self.attributes_changed)
        lyr.destroyed.connect(self.unsubscribe)
        lyr.beforeCommitChanges.connect(self.inspect_changes)
        lyr.styleChanged.connect(self.style_changed)
        #add contextual menu
        self.sync_with_google_drive_action = QAction(QIcon(os.path.join(self.parent.plugin_dir,'sync.png')), "Sync with Google drive", self.iface )
        self.iface.addCustomActionForLayerType(self.sync_with_google_drive_action, "", QgsMapLayer.VectorLayer, allLayers=False)
        self.iface.addCustomActionForLayer(self.sync_with_google_drive_action, lyr)
        self.sync_with_google_drive_action.triggered.connect(self.sync_with_google_drive)

        lyr.gDriveInterface = self

    def add_records(self):
        '''
        Add records to the memory layer by reading the Google Sheet
        '''
        self.lyr.startEditing()

        for i, row in enumerate(self.reader[1:]):
            flds = collections.OrderedDict(list(zip(self.header, row)))
            status = flds.pop('STATUS')

            if status != 'D': #non caricare i deleted
                wkt_geom = unpack(flds.pop('WKTGEOMETRY'))
                #fid = int(flds.pop('FEATUREID'))
                feature = QgsFeature()
                geometry = QgsGeometry.fromWkt(wkt_geom)
                feature.setGeometry(geometry)
                cleared_row = [] #[fid]
                for field, attribute in flds.items():
                    if field[:8] != 'DELETED_': #skip deleted fields
                        if attribute == '()':
                            cleared_row.append(qgis.core.NULL)
                        else:
                            cleared_row.append(attribute)
                    else:
                        logger( "DELETED " + field)
                feature.setAttributes(cleared_row)
                self.lyr.addFeature(feature)
        self.lyr.commitChanges()

    def saveMetadataState(self,lyr=None,metadata=None):
        logger ("metadata changed")
        if not metadata:
            metadata = self.get_layer_metadata(lyr)
        self.service_sheet.update_metadata(self.spreadsheet_id, metadata)

    def style_changed(self):
        '''
        landing method for rendererChanged signal. It stores xml qgis style definition to the setting sheet
        '''
        logger( "style changed")
        self.dirty = True
        self.restyled = True

    def renew_connection(self):
        '''
        when connection stay alive too long we have to rebuild service
        '''
        self.service_drive.renew_connection()

    def sync_with_google_drive(self):
        self.renew_connection()
        self.update_from_subscription()
        self.update_summary_sheet()

    def update_from_subscription(self):
        '''
        The method updates qgis memory layer with changes made by other users and sincronize the local qgis layer with google sheet spreadsheet
        '''
        self.renew_connection()
        bar = progressBar(self, 'updating local layer from remote')
        # fix_print_with_import
        if self.service_sheet.canEdit:
            updates = self.service_sheet.get_line('COLUMNS','A', sheet=self.client_id)
            if updates:
                self.service_sheet.erase_cells(self.client_id)
        else:
            new_changes_log_rows = self.service_sheet.get_line("COLUMNS",'A',sheet="changes_log")
            if len(new_changes_log_rows) > len(self.service_sheet.changes_log_rows):
                updates = new_changes_log_rows[-len(new_changes_log_rows)+len(self.service_sheet.changes_log_rows):]
                self.service_sheet.changes_log_rows = new_changes_log_rows
            else:
                updates = []
        # fix_print_with_import
        for update in updates:
            decode_update = update.split("|")
            if decode_update[0] in ('new_feature', 'delete_feature', 'update_geometry', 'update_attributes'):
                sheet_feature = self.service_sheet.get_line('ROWS',decode_update[1])
                if decode_update[0] == 'new_feature':
                    feat = QgsFeature()
                    geom = QgsGeometry().fromWkt(unpack(sheet_feature[0]))
                    feat.setGeometry(geom)
                    feat.setAttributes(sheet_feature[2:])
                    logger(( "updating from subscription, new_feature: " + str(self.lyr.dataProvider().addFeatures([feat]))))
                else:
                    sheet_feature_id = decode_update[1]
                    feat = next(self.lyr.getFeatures(QgsFeatureRequest(QgsExpression(' "FEATUREID" = %s' % sheet_feature_id))))
                    if   decode_update[0] == 'delete_feature':
                        # fix_print_with_import
                        logger("updating from subscription, delete_feature: " + str(self.lyr.dataProvider().deleteFeatures([feat.id()])))
                    elif decode_update[0] == 'update_geometry':
                        update_set = {feat.id(): QgsGeometry().fromWkt(unpack(sheet_feature[0]))}
                        # fix_print_with_import
                        logger("updating from subscription, update_geometry: " + str(self.lyr.dataProvider().changeGeometryValues(update_set)))
                    elif decode_update[0] == 'update_attributes':
                        new_attributes = sheet_feature_id[2:]
                        attributes_map = {}
                        for i in range(0, len(new_attributes)):
                            attributes_map[i] = new_attributes[i]
                        update_map = {feat.id(): attributes_map,}
                        # fix_print_with_import
                        logger("updating from subscription, update_attributes: " +(self.lyr.dataProvider().changeAttributeValues(update_map)))
            elif decode_update[0] == 'add_field':
                field_a1_notation = self.service_sheet.header_map[decode_update[1]]
                type_def = self.service_sheet.sheet_cell('settings!%s1' % field_a1_notation)
                type_def_decoded = type_def.split("|")
                new_field = QgsField(name=decode_update[1],type=int(type_def_decoded[0]), len=int(type_def_decoded[1]), prec=int(type_def_decoded[2]))
                # fix_print_with_import
                logger("updating from subscription, add_field: ", + (self.lyr.dataProvider().addAttributes([new_field])))
                self.lyr.updateFields()
            elif decode_update[0] == 'delete_field':
                # fix_print_with_import
                logger("updating from subscription, delete_field: " + str(self.lyr.dataProvider().deleteAttributes([self.lyr.dataProvider().fields().fieldNameIndex(decode_update[1])])))
                self.lyr.updateFields()
        self.lyr.triggerRepaint()
        bar.stop("local layer updated")

    def editing_started(self):
        '''
        Connect to the edit buffer so we can capture geometry and attribute
        changes
        '''
        # fix_print_with_import
        logger("editing")
        self.update_from_subscription()
        self.bar = None
        if self.service_sheet.canEdit:
            self.activeThreads = 0
            self.editing = True
            self.lyr.geometryChanged.connect(self.buffer_geometry_changed)
            self.lyr.attributeValueChanged.connect(self.buffer_attributes_changed)
            self.lyr.beforeCommitChanges.connect(self.catch_deleted)
            self.lyr.beforeRollBack.connect(self.rollBack)
            self.invalidEdit.connect(self.rollBack)
            self.changes_log=[]
            self.locking_queue = []
            self.timer = 0
        else: #refuse editing if file is read only
            self.lyr.rollBack()

    def buffer_geometry_changed(self,fid,geom):
        '''
        Landing method for geometryChanged signal.
        When a geometry is modified, the row related to the modified feature is marked as modified by local user.
        Further edits to the modified feature are denied to other concurrent users
        :param fid:
        :param geom:
        '''
        if self.editing:
            self.lock_feature(fid)

    def buffer_attributes_changed(self,fid,attr_id,value):
        '''
        Landing method for attributeValueChanged signal.
        When an attribute is modified, the row related to the modified feature is marked as modified by local user.
        Further edits to the modified feature are denied to other concurrent users
        :param fid:
        :param attr_id:
        :param value:
        '''
        if self.editing:
            self.lock_feature(fid)


    def lock_feature(self, fid):
        """
        The row in google sheet linked to feature that has been modified is locked
        Filling the the STATUS column with the client_id.
        Further edits to the modified feature are denied to other concurrent users
        """
        if fid >= 0: # fid <0 means that the change relates to newly created features not yet present in the sheet
            self.locks_applied = None
            feature_locking = next(self.lyr.getFeatures(QgsFeatureRequest(fid)))
            locking_row_id = feature_locking[0]
            self.locking_queue.append(locking_row_id)
            _thread.start_new_thread(self.deferred_apply_locks, ())


    def deferred_apply_locks(self):
        if self.timer > 0:
            self.timer = 0
            return
        else:
            while self.timer < 100:
                self.timer += 1
                sleep(0.01)
            #APPLY_LOCKS
            self.deferredEdit.emit()
            #self.apply_locks()

    def apply_locks(self):
        if self.locks_applied:
            return
        self.locks_applied = True
        status_range = []
        for row_id in self.locking_queue:
            status_range.append(['STATUS', row_id])
        status_control = self.service_sheet.multicell(status_range)
        if "valueRanges" in status_control:
            mods = []
            for valueRange in status_control["valueRanges"]:
                if valueRange["values"][0][0] in ('()', None):
                    mods.append([valueRange["range"],0,self.client_id])
                    row_id = valueRange["range"].split('B')[-1]
            if mods:
                self.service_sheet.set_multicell(mods, A1notation=True)
        self.locking_queue = []
        self.timer = 0

    def rollbackRow(self,row):
        logger(str(row))
        rollback_row = self.service_sheet.get_line('ROWS',row)
        #print ("Original row", rollback_row)
        wkt_geom = unpack(rollback_row[0])
        attrs = {}
        for attr in range(2,len(rollback_row)):
            attrs[attr-2] = rollback_row[attr]

        try:
            #identify feature to rollback
            feat = next(self.lyr.getFeatures(QgsFeatureRequest(QgsExpression(' "FEATUREID" = %s' % rollback_row[2]))))
            geom_update = {feat.id(): QgsGeometry.fromWkt(wkt_geom)}
            #rollback geometry
            self.lyr.dataProvider().changeGeometryValues(geom_update)
            #rollback attributes
            attrs_update = {feat.id():attrs}
            #print ("UPDATES", geom_update, attrs_update)
            self.lyr.dataProvider().changeFeatures(attrs_update, geom_update)
            
        except:
            logger("RESTORING DELETED FEATURE")
            #rollback deleted feat
            restore_feat = QgsFeature(self.lyr.fields())
            restore_feat.setGeometry(QgsGeometry.fromWkt(wkt_geom))
            for key,attr in attrs.items():
                logger(str(key)+str(attr))
                restore_feat.setAttribute(key, attr)
            self.lyr.dataProvider().addFeatures([restore_feat])
            
        self.lyr.triggerRepaint()
        message = self.iface.messageBar().createMessage("GooGIS plugin:","Feature %d is locked by %s: pending edits not applied" % (rollback_row[2], rollback_row[1]))
        self.iface.messageBar().pushWidget(message, Qgis.Warning, 5)


    def rollBack(self):
        """
        before rollback changes status field is cleared and the edits from concurrent user are allowed
        """
        # fix_print_with_import
        logger("ROLLBACK")
        try:
            self.lyr.geometryChanged.disconnect(self.buffer_geometry_changed)
        except:
            pass
        try:
            self.lyr.attributeValueChanged.disconnect(self.buffer_attributes_changed)
        except:
            pass
        self.renew_connection()
        self.clean_status_row()
        try:
            self.lyr.beforeRollBack.disconnect(self.rollBack)
        except:
            pass

        #self.lyr.geometryChanged.disconnect(self.buffer_geometry_changed)
        #self.lyr.attributeValueChanged.disconnect(self.buffer_attributes_changed)
        self.editing = False

    def editing_stopped(self):
        """
        Update the remote sheet if changes were committed
        """
        # fix_print_with_import
        logger("EDITING_STOPPED")
        self.renew_connection()
        self.clean_status_row()
        if self.service_sheet.canEdit:
            self.service_sheet.advertise(self.changes_log)
        self.editing = False
        #if self.dirty:
        #    self.update_summary_sheet()
        #    self.dirty = None
        if self.bar:
            self.bar.stop("update to remote finished")

    def inspect_changes(self):
        '''
        here we can inspect changes before commit them
        self.deleted_list = []
        for deleted in self.lyr.editBuffer().deletedAttributeIds():
            self.deleted_list.append(self.lyr.fields().at(deleted).name())
        print self.deleted_list

        logger("attributes_added")
        for field in self.lyr.editBuffer().addedAttributes():
            print "ADDED FIELD", field.name()
            self.service_sheet.add_column([field.name()], fill_with_null = True)
        '''
        # fix_print_with_import
        logger("INSPECT_CHANGES")
        pass

    def attributes_added(self, layer, added):
        """
        Landing method for attributeAdded.
        Fields (attribute) changed
        New colums are appended to the google drive spreadsheets creating remote colums syncronized with the local layer fields.
        Edits are advertized to other concurrent users for subsequent syncronization with remote table
        """
        logger("attributes_added")
        for field in added:
            logger( "ADDED FIELD %s" % field.name())
            self.service_sheet.add_column([field.name()], fill_with_null = True)
            self.service_sheet.add_column(["%d|%d|%d" % (field.type(), field.length(), field.precision())],child_sheet="settings", fill_with_null = None)
            self.changes_log.append('%s|%s' % ('add_field', field.name()))
        self.dirty = True

    def attributes_deleted(self, layer, deleted_ids):
        """
        Landing method for attributeDeleted.
        Fields (attribute) are deleted
        New colums are marked as deleted in the google drive spreadsheets.
        Edits are advertized to other concurrent users for subsequent syncronization with remote table
        """
        logger("attributes_deleted")
        for deleted in deleted_ids:
            deleted_name = self.service_sheet.mark_field_as_deleted(deleted)
            self.changes_log.append('%s|%s' % ('delete_field', deleted_name))
        self.dirty = True


    def features_added(self, layer, features):
        """
        Landing method for featureAdded.
        The new features are written adding rows to the google drive spreadsheets .
        Edits are advertized to other concurrent users for subsequent syncronization with remote table
        """
        logger("features added")

        for count,feature in enumerate(features):
            new_fid = self.service_sheet.new_fid()
            self.lyr.dataProvider().changeAttributeValues({feature.id() : {0: new_fid}})
            feature.setAttribute(0, new_fid+count)
            new_row_dict = {}.fromkeys(self.service_sheet.header,'()')
            new_row_dict['WKTGEOMETRY'] = pack(feature.geometry().asWkt(precision=self.precision))
            new_row_dict['STATUS'] = '()'
            for i,item in enumerate(feature.attributes()):
                fieldName = self.lyr.fields().at(i).name()
                try:
                    new_row_dict[fieldName] = item.toString(format = Qt.ISODate)
                except:
                    if not item or item == qgis.core.NULL:
                        new_row_dict[fieldName] = '()'
                    else:
                        new_row_dict[fieldName] = item
            new_row_dict['FEATUREID'] = '=ROW()' #assure correspondance between feature and sheet row
            result = self.service_sheet.add_row(new_row_dict)
            sheet_new_row = int(result['updates']['updatedRange'].split('!A')[1].split(':')[0])
            self.changes_log.append('%s|%s' % ('new_feature', str(new_fid)))
        self.dirty = True

    def catch_deleted(self):
        """
        Landing method for beforeCommitChanges signal.
        The method intercepts edits before they were written to the layer so from deleted features
        can be extracted the feature id of the google drive spreadsheet related rows.
        The affected rows are marked as deleted and hidden away from the layer syncronization
        """
        self.bar = progressBar(self, 'updating local edits to remote')
        """ Features removed; but before commit """
        deleted_ids = self.lyr.editBuffer().deletedFeatureIds()
        if deleted_ids:
            deleted_mods = []
            for fid in deleted_ids:
                removed_feat = next(self.lyr.dataProvider().getFeatures(QgsFeatureRequest(fid)))
                removed_row = removed_feat[0]
                logger ("Deleting FEATUREID %s" % removed_row)
                deleted_mods.append(("STATUS",removed_row,'D'))
                self.changes_log.append('%s|%s' % ('delete_feature', str(removed_row)))
            if deleted_mods:
                self.service_sheet.set_protected_multicell(deleted_mods)
            self.dirty = True

    def geometry_changed(self, layer, geom_map):
        """
        Landing method for geometryChange signal.
        Features geometries changed
        The edited geometry, not locked by other users, are written to the google drive spreadsheets modifying the related rows.
        the WKT geometry definition is zipped and then base64 encoded for a compact storage
        (sigle cells string contents can't be larger the 50000 bytes)
        Edits are advertized to other concurrent users for subsequent syncronization with remote table
        """
        geometry_mod = []
        for fid,geom in geom_map.items():
            feature_changing = next(self.lyr.getFeatures(QgsFeatureRequest(fid)))
            row_id = feature_changing[0]
            wkt = geom.asWkt(precision=self.precision)
            geometry_mod.append(('WKTGEOMETRY',row_id, pack(wkt) ))
            logger ("Updated FEATUREID %s geometry" % row_id)
            self.changes_log.append('%s|%s' % ('update_geometry', str(row_id)))

        value_mods_result = self.service_sheet.set_protected_multicell(geometry_mod, lockBy=self.client_id)
        self.dirty = True

    def attributes_changed(self, layer, changes):
        """
        Landing method for attributeChange.
        Attribute values changed
        Edited feature, not locked by other users, are written to the google drive spreadsheets modifying the related rows.
        Edits are advertized to other concurrent users for subsequent syncronization with remote table
        """
        if not self.doing_attr_update:
            #print "changes",changes
            attribute_mods = []
            for fid,attrib_change in changes.items():
                feature_changing = next(self.lyr.getFeatures(QgsFeatureRequest(fid)))
                row_id = feature_changing[0]
                logger ( "Attribute changing FEATUREID: %s" % row_id)
                for attrib_idx, new_value in attrib_change.items():
                    fieldName = QgsProject.instance().mapLayer(layer).fields().field(attrib_idx).name()
                    if fieldName == 'FEATUREID':
                        logger("can't modify FEATUREID")
                        continue
                    try:
                        cleaned_value = new_value.toString(format = Qt.ISODate)
                    except:
                        if not new_value or new_value == qgis.core.NULL:
                            cleaned_value = '()'
                        else:
                            cleaned_value = new_value
                    attribute_mods.append((fieldName,row_id, cleaned_value))
                self.changes_log.append('%s|%s' % ('update_attributes', str(row_id)))

            if attribute_mods:
                attribute_mods_result = self.service_sheet.set_protected_multicell(attribute_mods, lockBy=self.client_id)
                print (attribute_mods)
                print (attribute_mods_result)
            self.dirty = True

    def clean_status_row(self):
        status_line = self.service_sheet.get_line("COLUMNS","B")
        clean_status_mods = []
        for row_line, row_value in enumerate(status_line):
            if row_value == self.client_id:
                clean_status_mods.append(("STATUS",row_line+1,'()'))
        value_mods_result = self.service_sheet.set_multicell(clean_status_mods)
        return value_mods_result

    def unsubscribe(self):
        '''
        When a read/write layer is removed from the legend the remote subscription sheet is removed and update summary sheet if dirty
        '''
        self.renew_connection()
        self.service_sheet.unsubscribe()

    def qgis_layer_to_csv(self,qgis_layer):
        '''
        method to transform the specified qgis layer in a csv object for uploading
        :param qgis_layer:
        :return: csv object
        '''
        stream = io.BytesIO()
        writer = csv.writer(stream, delimiter=',', quotechar='"', lineterminator='\n')
        row = ["WKTGEOMETRY","FEATUREID","STATUS"]
        for feat in qgis_layer.getFeatures():
            for field in feat.fields().toList():
                row.append(field.name().encode("utf-8"))
            break
        writer.writerow(row)
        for feat in qgis_layer.getFeatures():
            row = [pack(feat.geometry().asWkt(precision=self.precision)),feat.id(),"()"]
            for field in feat.fields().toList():
                if feat[field.name()] == qgis.core.NULL:
                    content = "()"
                else:
                    if type(feat[field.name()]) == str:
                        content = feat[field.name()].encode("utf-8")
                    else:
                        content = feat[field.name()]
                row.append(content)
            writer.writerow(row)
        stream.seek(0)
        #csv.reader(stream, delimiter=',', quotechar='"', lineterminator='\n')
        return stream

    def qgis_layer_to_list(self,qgis_layer):
        '''
        method to transform the specified qgis layer in list of rows (field/value) dicts for uploading
        :param qgis_layer:
        :return: row list object
        '''
        row = ["WKTGEOMETRY","STATUS","FEATUREID"]
        for feat in qgis_layer.getFeatures():
            for field in feat.fields().toList():
                row.append(field.name())
                #row.append(str(field.name()).encode("utf-8"))# slugify(field.name())
            break
        rows = [row]
        for feat in qgis_layer.getFeatures():
            row = [pack(feat.geometry().asWkt(precision=self.precision)),"()","=ROW()"] # =ROW() perfect row/featureid correspondance
            if len(row[0]) > 50000: # ignore features with geometry > 50000 bytes zipped
                continue
            for field in feat.fields().toList():
                if feat[field.name()] == qgis.core.NULL:
                    content = "()"
                else:
                    if type(feat[field.name()]) == str:
                        content = feat[field.name()] #feat[field.name()].encode("utf-8")
                    elif field.typeName() in ('Date', 'Time'):
                        content = feat[field.name()].toString(format = Qt.ISODate)
                    else:
                        content = feat[field.name()]
                row.append(content)
            rows.append(row)
        #csv.reader(stream, delimiter=',', quotechar='"', lineterminator='\n')
        return rows

    def saveFieldTypes(self,fields):
        '''
        writes the layer field types to the setting sheet
        :param fields:
        :return:
        '''
        types_array = ["s1","s2","4|4|0"] #default featureId type to longint
        for field in fields.toList():
            types_array.append("%d|%d|%d" % (field.type(), field.length(), field.precision()))
        # fix_print_with_import
        self.service_sheet.update_cells('settings!A1',types_array)

    def layer_style_to_xml(self,qgis_layer):
        '''
        saves qgis style to the setting sheet
        :param qgis_layer:
        :return:
        '''
        XMLDocument = QDomDocument("qgis_style")
        XMLStyleNode = XMLDocument.createElement("style")
        XMLDocument.appendChild(XMLStyleNode)
        error = None
        rw_context = QgsReadWriteContext()
        rw_context.setPathResolver( QgsProject.instance().pathResolver() )
        qgis_layer.writeSymbology(XMLStyleNode, XMLDocument, error,rw_context)
        xmldoc = XMLDocument.toString(1)
        return xmldoc

    def SLD_to_xml(self,qgis_layer):
        '''
        saves SLD style to the setting sheet. Not used, keeped here for further extensions.
        :param qgis_layer:
        :return:
        '''
        XMLDocument = QDomDocument("sld_style")
        error = None
        qgis_layer.exportSldStyle(XMLDocument, error)
        xmldoc = XMLDocument.toString(1)
        return xmldoc

    def xml_to_layer_style(self,qgis_layer,xml):
        '''
        retrieve qgis style from the setting sheet
        :param qgis_layer:
        :return:
        '''
        XMLDocument = QDomDocument()
        error = None
        XMLDocument.setContent(xml)
        XMLStyleNode = XMLDocument.namedItem("style") 
        rw_context = QgsReadWriteContext()
        rw_context.setPathResolver( QgsProject.instance().pathResolver() )
        qgis_layer.readSymbology(XMLStyleNode, error, rw_context)

    def layer_style_to_json(self, qgis_layer):

        #old_mapbox_style = toMapboxgl([qgis_layer])
        mapbox_style,icons,warnings = layerStyleAsMapbox(qgis_layer)
        if warnings:
            logger("Warning exporting to mapboxgl style: " + json.dumps(warnings))
        else:
            logger("mapboxgl style ok")
        return mapbox_style

    def get_gdrive_id(self):
        '''
        returns spreadsheet_id associated with layer
        :return: spreadsheet_id associated with layer
        '''
        return self.spreadsheet_id

    def get_service_drive(self):
        '''
        returns the google drive wrapper object associated with layer
        :return: google drive wrapper object
        '''
        return self.service_drive

    def get_service_sheet(self):
        '''
        returns the google spreadsheet wrapper object associated with layer
        :return: google spreadsheet wrapper object
        '''
        return self.service_sheet

    def wgs84_extent(self,extent):
        llp = self.transformToWGS84(QgsPointXY(extent.xMinimum(),extent.yMinimum()))
        rtp = self.transformToWGS84(QgsPointXY(extent.xMaximum(),extent.yMaximum()))
        return QgsRectangle(llp,rtp)

    def transformToWGS84(self, pPoint):
        crsDest = QgsCoordinateReferenceSystem(4326)  # WGS 84
        xform = QgsCoordinateTransform(self.lyr.crs(), crsDest, QgsProject.instance())
        return xform.transform(pPoint) # forward transformation: src -> dest

    def get_layer_metadata(self,lyr=None):
        '''
        builds a metadata dict of the current layer to be stored in summary sheet
        '''

        if not lyr:
            lyr = self.lyr

        #fields = collections.OrderedDict()
        fields = ""
        for field in lyr.fields().toList():
            fields += field.name()+'_'+QVariant.typeToName(field.type())+'|'+str(field.length())+'|'+str(field.precision())+' '
        #metadata = collections.OrderedDict()
        metadata = [
            ['layer_name', lyr.name(),],
            ['gdrive_id', self.service_sheet.spreadsheetId,],
            ['geometry_type', self.geom_types[lyr.geometryType()],],
            ['features', "'%s" % str(lyr.featureCount()),],
            ['extent', self.wgs84_extent(lyr.extent()).asWktCoordinates(),],
            #['fields', fields,],
            ['abstract', lyr.abstract(),],
            ['srid', lyr.crs().authid(),],
            ['proj4_def', "'%s" % lyr.crs().toProj4(),],
        ]

        return metadata

    def update_summary_sheet(self,lyr=None, force=None):
        '''
        Creates a summary sheet with thumbnail, layer metadata and online view link
        '''
        #create a layer snapshot and upload it to google drive

        if not lyr:
            lyr = self.lyr

        mapbox_style = self.service_sheet.sheet_cell('settings!A5')
        if not mapbox_style:
            logger("migrating mapbox style")
            self.service_sheet.set_style_mapbox(self.layer_style_to_json(self.lyr))
        
        if not force and not self.dirty and not self.restyled:
            return

        if self.restyled:
            self.service_sheet.set_style_qgis(self.layer_style_to_xml(self.lyr))
            self.service_sheet.set_style_sld(self.SLD_to_xml(self.lyr))
            self.service_sheet.set_style_mapbox(self.layer_style_to_json(self.lyr))
            self.saveMetadataState()
        
        canvas = QgsMapCanvas()
        canvas.resize(QSize(600,600))
        canvas.setCanvasColor(Qt.white)
        canvas.setExtent(lyr.extent())
        canvas.setLayers([lyr])
        canvas.refresh()
        canvas.update()
        settings = canvas.mapSettings()
        settings.setLayers([lyr])
        job = QgsMapRendererParallelJob(settings)
        job.start()
        job.waitForFinished()
        image = job.renderedImage()

        transparent_image = QImage(image.width(), image.height(), QImage.Format_ARGB32)
        transparent_image.fill(Qt.transparent)
        p = QPainter(transparent_image)
        mask = image.createMaskFromColor(QColor(255, 255, 255).rgb(), Qt.MaskInColor)
        p.setClipRegion(QRegion(QBitmap(QPixmap.fromImage(mask))))
        p.drawPixmap(0, 0, QPixmap.fromImage(image))
        p.end()

        tmp_path = os.path.join(self.parent.plugin_dir,self.service_sheet.name+".png")
        transparent_image.save(tmp_path,"PNG")
        image_istances = self.service_drive.list_files(mimeTypeFilter='image/png',filename=self.service_sheet.name+".png")
        for imagename, image_props in image_istances.items():
            self.service_drive.delete_file(image_props['id'])
        result = self.service_drive.upload_image(tmp_path)
        self.service_drive.add_permission(result['id'],'anyone','reader')
        webLink = result['webContentLink'] #'https://drive.google.com/uc?export=view&id='+result['id']
        logger("webLink:" + webLink)
        canvas.setDestinationCrs(QgsCoordinateReferenceSystem(4326))
        worldfile = QgsMapSettingsUtils.worldFileContent(settings)
        lonlat_min = self.transformToWGS84(QgsPointXY(canvas.extent().xMinimum(), canvas.extent().yMinimum()))
        lonlat_max = self.transformToWGS84(QgsPointXY(canvas.extent().xMaximum(), canvas.extent().yMaximum()))
        keymap_extent = [lonlat_min.x(),lonlat_min.y(),lonlat_max.x(),lonlat_max.y()]
        
        os.remove(tmp_path)
        #update layer metadata
        summary_id = self.service_sheet.add_sheet('summary', no_grid=True)
        appPropsUpdate = [
            ["keymap",webLink],
            ["worldfile",pack(worldfile)],
            ["keymap_extent", json.dumps(keymap_extent)]
        ]
        res = self.service_sheet.update_appProperties(self.spreadsheet_id,appPropsUpdate)
        
        self.saveMetadataState(metadata=self.get_layer_metadata())
        self.parent.public_db.setKey(self.spreadsheet_id, dict(self.get_layer_metadata()+appPropsUpdate),only_update=True)
        #merge cells to visualize snapshot and aaply image snapshot
        request_body = {
            'requests': [{
                'mergeCells': {
                    "range": {
                        "sheetId": summary_id,
                        "startRowIndex": 9,
                        "endRowIndex": 32,
                        "startColumnIndex": 0,
                        "endColumnIndex": 9,
                    },
                "mergeType": 'MERGE_ALL'
                }
            }]
        }
        self.service_sheet.service.spreadsheets().batchUpdate(spreadsheetId=self.spreadsheet_id, body=request_body).execute()
        self.service_sheet.set_sheet_cell('summary!A10','=IMAGE("%s",3)' % webLink)

        permissions = self.service_drive.file_property(self.spreadsheet_id,'permissions')
        for permission in permissions:
            if permission['type'] == 'anyone':
                public = True
                break
            else:
                public = False
        if public:
            update_range = 'summary!A9:B9'
            update_body = {
                "range": update_range,
                "values": [['public link', "https://enricofer.github.io/gdrive_provider/weblink/converter.html?spreadsheet_id="+self.spreadsheet_id]]
            }
            self.service_sheet.service.spreadsheets().values().update(spreadsheetId=self.spreadsheet_id,range=update_range, body=update_body, valueInputOption='USER_ENTERED').execute()

        #hide worksheets except summary
        sheets = self.service_sheet.get_sheets()
        #self.service_sheet.toggle_sheet('summary', sheets['summary'], hidden=None)
        for sheet_name,sheet_id in sheets.items():
            if not sheet_name == 'summary':
                self.service_sheet.toggle_sheet(sheet_name, sheet_id, hidden=True)







        

