/*
 * Project:  NextGIS Mobile
 * Purpose:  Mobile GIS for Android.
 * Author:   Dmitry Baryshnikov (aka Bishop), [email protected]
 * Author:   NikitaFeodonit, [email protected]
 * Author:   Stanislav Petriakov, [email protected]
 * *****************************************************************************
 * Copyright (c) 2012-2020 NextGIS, [email protected]
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nextgis.maplib.map;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.text.TextUtils;
import android.util.JsonReader;
import android.util.Log;
import android.util.Pair;

import com.nextgis.maplib.R;
import com.nextgis.maplib.api.INGWLayer;
import com.nextgis.maplib.api.IProgressor;
import com.nextgis.maplib.datasource.Feature;
import com.nextgis.maplib.datasource.Field;
import com.nextgis.maplib.datasource.GeoGeometry;
import com.nextgis.maplib.datasource.GeoGeometryFactory;
import com.nextgis.maplib.datasource.ngw.Connection;
import com.nextgis.maplib.datasource.ngw.SyncAdapter;
import com.nextgis.maplib.util.AccountUtil;
import com.nextgis.maplib.util.AttachItem;
import com.nextgis.maplib.util.Constants;
import com.nextgis.maplib.util.DatabaseContext;
import com.nextgis.maplib.util.FeatureChanges;
import com.nextgis.maplib.util.GeoConstants;
import com.nextgis.maplib.util.HttpResponse;
import com.nextgis.maplib.util.NGException;
import com.nextgis.maplib.util.NGWUtil;
import com.nextgis.maplib.util.NetworkUtil;
import com.nextgis.maplib.util.ProgressBufferedInputStream;
import com.nextgis.maplib.util.SettingsConstants;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import static com.nextgis.maplib.datasource.ngw.Connection.NGWResourceTypePostgisLayer;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_ATTACH;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_TEMP;
import static com.nextgis.maplib.util.Constants.FIELD_ATTACH_ID;
import static com.nextgis.maplib.util.Constants.FIELD_ATTACH_OPERATION;
import static com.nextgis.maplib.util.Constants.FIELD_FEATURE_ID;
import static com.nextgis.maplib.util.Constants.FIELD_ID;
import static com.nextgis.maplib.util.Constants.FIELD_OPERATION;
import static com.nextgis.maplib.util.Constants.MIN_LOCAL_FEATURE_ID;
import static com.nextgis.maplib.util.Constants.TAG;
import static com.nextgis.maplib.util.Constants.URI_ATTACH;
import static com.nextgis.maplib.util.Constants.URI_CHANGES;


public class NGWVectorLayer
        extends VectorLayer
        implements INGWLayer
{
    protected static final String JSON_ACCOUNT_KEY           = "account";
    protected static final String JSON_NGW_VERSION_MAJOR_KEY = "ngw_version_major";
    protected static final String JSON_NGW_VERSION_MINOR_KEY = "ngw_version_minor";
    protected static final String JSON_SYNC_TYPE_KEY         = "sync_type";
    protected static final String JSON_NGWLAYER_TYPE_KEY     = "ngw_layer_type";
    protected static final String JSON_SERVERWHERE_KEY       = "server_where";
    protected static final String JSON_TRACKED_KEY           = "tracked";
    protected static final String JSON_SYNC_DIRECTION_KEY    = "sync_direction";

    protected static final int TYPE_CHANGES_TABLE     = 125;
    protected static final int TYPE_CHANGES_FEATURE   = 126;
    protected static final int TYPE_CHANGES_ATTACH    = 127;
    protected static final int TYPE_CHANGES_ATTACH_ID = 128;

    protected static final int DIRECTION_TO = 1;
    protected static final int DIRECTION_FROM = 2;
    protected static final int DIRECTION_BOTH = 3;

    protected static boolean mIsAddedToUriMatcher = false;

    protected NetworkUtil mNet;

    protected int mNgwVersionMajor = Constants.NOT_FOUND;
    protected int mNgwVersionMinor = Constants.NOT_FOUND;

    protected String mAccountName;
    protected long   mRemoteId;
    protected int    mSyncType;
    protected int    mNGWLayerType;
    protected int    mCRS = GeoConstants.CRS_WEB_MERCATOR;
    protected String mServerWhere;
    protected boolean mTracked;
    protected int mSyncDirection = DIRECTION_BOTH; //1 - to server only, 2 - from server only, 3 - both directions
    //check where to sync on GSM/WI-FI for data/attachments


    public NGWVectorLayer(
            Context context,
            File path)
    {
        super(context, path);

        if (null == mNet) {
            mNet = new NetworkUtil(context);
        }

        mSyncType = Constants.SYNC_NONE;
        mLayerType = Constants.LAYERTYPE_NGW_VECTOR;
        mNGWLayerType = Connection.NGWResourceTypeNone;

        if (!mIsAddedToUriMatcher) {
            // get changes for all rows
            mUriMatcher.addURI(mAuthority, "*/" + URI_CHANGES, TYPE_CHANGES_TABLE);

            // get changes for single row
            mUriMatcher.addURI(mAuthority, "*/" + URI_CHANGES + "/#", TYPE_CHANGES_FEATURE);

            //get changes for all attaches of row
            mUriMatcher.addURI(
                    mAuthority, "*/" + URI_CHANGES + "/#/" + URI_ATTACH, TYPE_CHANGES_ATTACH);

            //get changes for single attach by id
            mUriMatcher.addURI(mAuthority, "*/" + URI_CHANGES + "/#/" + URI_ATTACH + "/#",
                    TYPE_CHANGES_ATTACH_ID);

            mIsAddedToUriMatcher = true;
        }
    }


    @Override
    public String getAccountName()
    {
        return mAccountName;
    }


    @Override
    public void setAccountName(String accountName)
    {
        mAccountName = accountName;
        setAccountCacheData();
    }


    @Override
    public long getRemoteId()
    {
        return mRemoteId;
    }


    public String getRemoteUrl()
    {
        try {
            AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);
            return NGWUtil.getResourceUrl(accountData.url, mRemoteId);
        } catch (IllegalStateException e) {
            return null;
        }
    }


    @Override
    public void setRemoteId(long remoteId)
    {
        mRemoteId = remoteId;
    }


    public String getServerWhere()
    {
        return mServerWhere;
    }


    public void setServerWhere(String serverWhere)
    {
        mServerWhere = serverWhere;
    }


    public String getChangeTableName()
    {
        return mPath.getName() + Constants.CHANGES_NAME_POSTFIX;
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject rootConfig = super.toJSON();
        rootConfig.put(JSON_NGW_VERSION_MAJOR_KEY, mNgwVersionMajor);
        rootConfig.put(JSON_NGW_VERSION_MINOR_KEY, mNgwVersionMinor);
        rootConfig.put(JSON_ACCOUNT_KEY, mAccountName);
        rootConfig.put(Constants.JSON_ID_KEY, mRemoteId);
        rootConfig.put(JSON_SYNC_TYPE_KEY, mSyncType);
        rootConfig.put(JSON_NGWLAYER_TYPE_KEY, mNGWLayerType);
        rootConfig.put(JSON_SERVERWHERE_KEY, mServerWhere);
        rootConfig.put(JSON_TRACKED_KEY, mTracked);
        rootConfig.put(GeoConstants.GEOJSON_CRS, mCRS);
        rootConfig.put(JSON_SYNC_DIRECTION_KEY, mSyncDirection);

        return rootConfig;
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException, SQLiteException
    {
        super.fromJSON(jsonObject);

        mTracked = jsonObject.optBoolean(JSON_TRACKED_KEY);
        mCRS = jsonObject.optInt(GeoConstants.GEOJSON_CRS, GeoConstants.CRS_WEB_MERCATOR);
        if (jsonObject.has(JSON_NGW_VERSION_MAJOR_KEY)) {
            mNgwVersionMajor = jsonObject.getInt(JSON_NGW_VERSION_MAJOR_KEY);
        }
        if (jsonObject.has(JSON_NGW_VERSION_MINOR_KEY)) {
            mNgwVersionMinor = jsonObject.getInt(JSON_NGW_VERSION_MINOR_KEY);
        }

        setAccountName(jsonObject.optString(JSON_ACCOUNT_KEY));

        mRemoteId = jsonObject.optLong(Constants.JSON_ID_KEY);
        mSyncType = jsonObject.optInt(JSON_SYNC_TYPE_KEY, Constants.SYNC_NONE);
        mNGWLayerType = jsonObject.optInt(JSON_NGWLAYER_TYPE_KEY, Constants.LAYERTYPE_NGW_VECTOR);
        mServerWhere = jsonObject.optString(JSON_SERVERWHERE_KEY);
        mSyncDirection = jsonObject.optInt(JSON_SYNC_DIRECTION_KEY, DIRECTION_BOTH);
    }


    @Override
    public void setAccountCacheData()
    {
        // do nothing
    }


    @Override
    protected long insertInternal(ContentValues contentValues)
    {
        if (!contentValues.containsKey(Constants.FIELD_ID)) {
            long id = getUniqId();
            if (MIN_LOCAL_FEATURE_ID > id) {
                id = MIN_LOCAL_FEATURE_ID;
            }
            contentValues.put(FIELD_ID, id);
        }

        return super.insertInternal(contentValues);
    }


    @Override
    protected boolean checkGeometryType(Feature feature)
    {
        return mNgwVersionMajor < Constants.NGW_v3 || super.checkGeometryType(feature);
    }


    // for overriding in the subclasses
    protected String getFeaturesUrl(AccountUtil.AccountData accountData)
    {
        if (mTracked)
            return NGWUtil.getTrackedFeaturesUrl(accountData.url, mRemoteId, getPreferences().getLong(SettingsConstants.KEY_PREF_LAST_SYNC_TIMESTAMP, 0));
        else
            return NGWUtil.getFeaturesUrl(accountData.url, mRemoteId, mServerWhere);
    }


    // for overriding in the subclasses
    protected String getResourceMetaUrl(AccountUtil.AccountData accountData)
    {
        return NGWUtil.getResourceUrl(accountData.url, mRemoteId);
    }


    // for overriding in the subclasses
    protected String getRequiredCls()
    {
        return "vector_layer";
    }


    /**
     * download and create new NGW layer from GeoJSON data
     */
    public void createFromNGW(IProgressor progressor)
            throws NGException, IOException, JSONException, SQLiteException
    {
        if (!mNet.isNetworkAvailable()) { //return tile from cache
            throw new NGException(getContext().getString(R.string.error_network_unavailable));
        }

        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "download layer " + getName());
        }

        // get account
        AccountUtil.AccountData accountData;
        try {
            accountData = AccountUtil.getAccountData(mContext, mAccountName);
        } catch (IllegalStateException e) {
            throw new NGException(getContext().getString(R.string.error_auth));
        }

        if (null == accountData.url) {
            throw new NGException(getContext().getString(R.string.error_404));
        }

        // get NGW version
        Pair<Integer, Integer> ver = null;
        try {
            ver = NGWUtil.getNgwVersion(accountData.url, accountData.login, accountData.password);
        } catch (IOException | JSONException | NumberFormatException ignored) { }

        if (null != ver) {
            mNgwVersionMajor = ver.first;
            mNgwVersionMinor = ver.second;
        }

        // get layer description
        JSONObject geoJSONObject;
        HttpResponse response = NetworkUtil.get(getResourceMetaUrl(accountData), accountData.login,
                accountData.password, false);
        if (!response.isOk()) {
            throw new NGException(NetworkUtil.getError(mContext, response.getResponseCode()));
        }
        geoJSONObject = new JSONObject(response.getResponseBody());

        //fill field list
        JSONObject featureLayerJSONObject = geoJSONObject.getJSONObject("feature_layer");
        JSONArray fieldsJSONArray = featureLayerJSONObject.getJSONArray(NGWUtil.NGWKEY_FIELDS);
        List<Field> fields = NGWUtil.getFieldsFromJson(fieldsJSONArray);

        //fill SRS
        JSONObject vectorLayerJSONObject = null;
        if (geoJSONObject.has(getRequiredCls())) {
            vectorLayerJSONObject = geoJSONObject.getJSONObject(getRequiredCls());
            mNGWLayerType = Connection.NGWResourceTypeVectorLayer;
        } else if (mNgwVersionMajor >= Constants.NGW_v3 && geoJSONObject.has("postgis_layer")) {
            vectorLayerJSONObject = geoJSONObject.getJSONObject("postgis_layer");
            mNGWLayerType = NGWResourceTypePostgisLayer;
        }
        if (null == vectorLayerJSONObject) {
            throw new NGException(getContext().getString(R.string.error_download_data));
        }

        String geomTypeString = vectorLayerJSONObject.getString(JSON_GEOMETRY_TYPE_KEY);
        int geomType = GeoGeometryFactory.typeFromString(geomTypeString);
        JSONObject srs = vectorLayerJSONObject.getJSONObject(NGWUtil.NGWKEY_SRS);
        mCRS = srs.getInt("id");
        if (mCRS != GeoConstants.CRS_WEB_MERCATOR && mCRS != GeoConstants.CRS_WGS84) {
            throw new NGException(getContext().getString(R.string.error_crs_unsupported));
        }

        create(geomType, fields);

        String sURL = getFeaturesUrl(accountData);
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "download features from: " + sURL);
        }

        // get features and fill them
        HttpURLConnection urlConnection = NetworkUtil.getHttpConnection("GET", sURL, accountData.login, accountData.password);
        if (null == urlConnection) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + sURL);

            if (null != progressor)
                progressor.setMessage(getContext().getString(R.string.error_connect_failed));

            return;
        }

        InputStream in = new ProgressBufferedInputStream(urlConnection.getInputStream(), urlConnection.getContentLength());
        JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
        reader.beginArray();

        SQLiteDatabase db = DatabaseContext.getDbForLayer(this);

        int streamSize = in.available();
        if (null != progressor) {
            progressor.setIndeterminate(false);
            if (streamSize > 0)
                progressor.setMax(streamSize);
            progressor.setMessage(getContext().getString(R.string.start_fill_layer) + " " + getName());
        }

        int featureCount = 0;
        while (reader.hasNext()) {
            try {
                final Feature feature = NGWUtil.readNGWFeature(reader, fields, mCRS);
                if (feature.getGeometry() == null || !feature.getGeometry().isValid())
                    continue;

                createFeatureBatch(feature, db);
            } catch (OutOfMemoryError | IllegalStateException | IOException | NumberFormatException e) {
                e.printStackTrace();
                if (null != progressor)
                    throw new NGException(getContext().getString(R.string.error_download_data));

                save();
                return;
            }

            if (null != progressor) {
                if (progressor.isCanceled()) {
                    save();
                    return;
                }
                progressor.setValue(streamSize - in.available());
                progressor.setMessage(getContext().getString(R.string.process_features) + ": " + featureCount);
            }

            ++featureCount;
        }
        reader.endArray();
        reader.close();
        //db.close();

        urlConnection.disconnect();
        mTracked = vectorLayerJSONObject.optBoolean(JSON_TRACKED_KEY);

        save();

        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "feature count: " + featureCount);
        }
    }


    @Override
    public void create(
            int geometryType,
            List<Field> fields)
            throws SQLiteException
    {
        if (mNgwVersionMajor < Constants.NGW_v3 && geometryType < 4
                && mNGWLayerType == Connection.NGWResourceTypeVectorLayer) {
            // to multi
            geometryType += 3;
        }

        super.create(geometryType, fields);
        FeatureChanges.initialize(getChangeTableName());
    }


    @Override
    public void addChange(
            long featureId,
            int operation)
    {
        if (0 == (mSyncType & Constants.SYNC_DATA)) {
            return;
        }

        String changeTableName = getChangeTableName();
        boolean canAddChanges = true;

        // for delete operation
        if (operation == Constants.CHANGE_OPERATION_DELETE) {

            // if featureId == NOT_FOUND remove all changes for all features
            if (featureId == Constants.NOT_FOUND) {
                FeatureChanges.removeAllChanges(changeTableName);

                // if feature has changes then remove them for the feature
            } else if (FeatureChanges.isChanges(changeTableName, featureId)) {
                // if feature was new then just remove its changes
                canAddChanges = !FeatureChanges.isChanges(changeTableName, featureId,
                        Constants.CHANGE_OPERATION_NEW);
                FeatureChanges.removeChanges(changeTableName, featureId);
            }
        }

        // we are trying to re-create feature - warning
        if (operation == Constants.CHANGE_OPERATION_NEW && FeatureChanges.isChanges(
                changeTableName, featureId)) {
            Log.w(Constants.TAG, "Something wrong. Should nether get here");
            canAddChanges = false;
        }

        // if can then add change
        if (canAddChanges) {
            FeatureChanges.add(changeTableName, featureId, operation);
        }
    }


    @Override
    public void addChange(
            long featureId,
            long attachId,
            int attachOperation)
    {
        if (0 == (mSyncType & Constants.SYNC_ATTACH)) {
            return;
        }

        String changeTableName = getChangeTableName();
        boolean canAddChanges = true;

        // for delete operation
        if (attachOperation == Constants.CHANGE_OPERATION_DELETE) {

            // if attachId == NOT_FOUND remove all attach changes for the feature
            if (attachId == Constants.NOT_FOUND) {
                FeatureChanges.removeAllAttachChanges(changeTableName, featureId);

                // if attachment has changes then remove them for the attachment
            } else if (FeatureChanges.isAttachChanges(changeTableName, featureId, attachId)) {
                // if attachment was new then just remove its changes
                canAddChanges =
                        !FeatureChanges.isAttachChanges(changeTableName, featureId, attachId,
                                Constants.CHANGE_OPERATION_NEW);
                FeatureChanges.removeAttachChanges(changeTableName, featureId, attachId);
            }
        }

        // we are trying to re-create the attach - warning
        // TODO: replace to attachOperation == CHANGE_OPERATION_NEW ???
        if (0 != (attachOperation & Constants.CHANGE_OPERATION_NEW)
                && FeatureChanges.isAttachChanges(changeTableName, featureId, attachId)) {
            Log.w(Constants.TAG, "Something wrong. Should nether get here");
            canAddChanges = false;
        }

        if (canAddChanges) {
            FeatureChanges.add(changeTableName, featureId, attachId, attachOperation);
        }
    }


    /**
     * Synchronize changes with NGW. Should be run from non UI thread.
     *
     * @param authority
     *         - a content resolver authority (i.e. com.nextgis.mobile.provider)
     * @param syncResult
     *         - report some errors via this parameter
     */
    @Override
    public void sync(
            String authority,
            Pair<Integer, Integer> ver,
            SyncResult syncResult)
    {
        syncResult.clear();
        if (0 != (mSyncType & Constants.SYNC_NONE) || mFields == null) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG,
                        "Layer " + getName() + " is not checked to sync or not inited");
            }
            return;
        }

        // 1. check NGW version
        if (null != ver) {
            int majorVer = ver.first;
            int minorVer = ver.second;

            if (mNgwVersionMajor != majorVer) {
                return;
            }
        }

        // 2. get remote changes
        if (isRemoteGetAllowed())
            if (!getChangesFromServer(authority, syncResult)) {
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "Get remote changes failed");
                }
            }

        if (isRemoteReadOnly()) {
            return;
        }

        // 3. send current changes
        if (isRemoteSendAllowed())
            if (!sendLocalChanges(syncResult)) {
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "Set local changes failed");
                }
            }
    }

    private boolean isRemoteGetAllowed() {
        return (mSyncDirection & DIRECTION_FROM) != 0;
    }

    private boolean isRemoteSendAllowed() {
        return (mSyncDirection & DIRECTION_TO) != 0;
    }

    public int getSyncDirection() {
        return mSyncDirection;
    }

    public void setSyncDirection(int direction) {
        mSyncDirection = direction;
    }

    public boolean sendLocalChanges(SyncResult syncResult)
    {
        String changeTableName = getChangeTableName();
        long changesCount = FeatureChanges.getChangeCount(changeTableName);
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "sendLocalChanges: " + changesCount);
        }

        if (0 == changesCount) {
            return true;
        }

        boolean isError = false;

        try {
            // get column's IDs, there is at least one entry
            Cursor changeCursor = FeatureChanges.getFirstChangeFromRecordId(changeTableName, 0);
            changeCursor.moveToFirst();

            int recordIdColumn = changeCursor.getColumnIndex(Constants.FIELD_ID);
            int featureIdColumn = changeCursor.getColumnIndex(Constants.FIELD_FEATURE_ID);
            int operationColumn = changeCursor.getColumnIndex(Constants.FIELD_OPERATION);
            int attachIdColumn = changeCursor.getColumnIndex(Constants.FIELD_ATTACH_ID);
            int attachOperationColumn =
                    changeCursor.getColumnIndex(Constants.FIELD_ATTACH_OPERATION);

            long nextChangeRecordId = changeCursor.getLong(recordIdColumn);

            changeCursor.close();

            while (true) {

                changeCursor = FeatureChanges.getFirstChangeFromRecordId(changeTableName,
                        nextChangeRecordId);

                if (null == changeCursor) {
                    break;
                }

                if (!changeCursor.moveToFirst()) {
                    // no more change records
                    changeCursor.close();
                    break;
                }

                long changeRecordId = changeCursor.getLong(recordIdColumn);
                nextChangeRecordId = changeRecordId + 1;

                long changeFeatureId = changeCursor.getLong(featureIdColumn);
                int changeOperation = changeCursor.getInt(operationColumn);
                long changeAttachId = changeCursor.getLong(attachIdColumn);
                int changeAttachOperation = changeCursor.getInt(attachOperationColumn);

                changeCursor.close();

                long lastChangeRecordId = FeatureChanges.getLastChangeRecordId(changeTableName);

                if (0 == (changeOperation & Constants.CHANGE_OPERATION_ATTACH)) {

                    if (0 != (changeOperation & Constants.CHANGE_OPERATION_DELETE)) {
                        if (deleteFeatureOnServer(changeFeatureId, syncResult)) {
                            FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed deleteFeatureOnServer() failed");
                            }
                        }

                    } else if (0 != (changeOperation & Constants.CHANGE_OPERATION_NEW)) {
                        if (addFeatureOnServer(changeFeatureId, syncResult)) {
                            FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                            FeatureChanges.removeChangesToLast(changeTableName, changeFeatureId,
                                    Constants.CHANGE_OPERATION_CHANGED, lastChangeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed addFeatureOnServer() failed");
                            }
                        }

                    } else if (0 != (changeOperation & Constants.CHANGE_OPERATION_CHANGED)) {
                        if (changeFeatureOnServer(changeFeatureId, syncResult)) {
                            FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                            FeatureChanges.removeChangesToLast(changeTableName, changeFeatureId,
                                    Constants.CHANGE_OPERATION_CHANGED, lastChangeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed changeFeatureOnServer() failed");
                            }
                        }
                    }
                }

                //process attachments
                else { // 0 != (changeOperation & CHANGE_OPERATION_ATTACH)

                    if (changeAttachOperation == Constants.CHANGE_OPERATION_DELETE) {
                        if (deleteAttachOnServer(changeFeatureId, changeAttachId, syncResult)) {
                            FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed deleteAttachOnServer() failed");
                            }
                        }

                    } else if (changeAttachOperation == Constants.CHANGE_OPERATION_NEW) {
                        if (sendAttachOnServer(changeFeatureId, changeAttachId, syncResult)) {
                            FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                            FeatureChanges.removeAttachChangesToLast(changeTableName,
                                    changeFeatureId, changeAttachId,
                                    Constants.CHANGE_OPERATION_CHANGED, lastChangeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed sendAttachOnServer() failed");
                            }
                        }

                    } else if (changeAttachOperation == Constants.CHANGE_OPERATION_CHANGED) {
                        if (changeAttachOnServer(changeFeatureId, changeAttachId, syncResult)) {
                            FeatureChanges.removeAttachChangesToLast(changeTableName,
                                    changeFeatureId, changeAttachId,
                                    Constants.CHANGE_OPERATION_CHANGED, lastChangeRecordId);
                        } else {
                            isError = true;
                            if (Constants.DEBUG_MODE) {
                                Log.d(Constants.TAG, "proceed changeAttachOnServer() failed");
                            }
                        }
                    }
                }
            }

            // check records count changing
            if (changesCount != FeatureChanges.getChangeCount(changeTableName)) {
//                mCache.save(new File(mPath, RTREE));  // useless due to save in notifyUpdate
//                if (DEBUG_MODE)
//                    Log.d(Constants.TAG, "mCache: saving sendLocalChanges");
                //notify to reload changes
                getContext().sendBroadcast(new Intent(SyncAdapter.SYNC_CHANGES));
            }

        } catch (SQLiteException e) {
            isError = true;
            syncResult.stats.numConflictDetectedExceptions++;
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "proceed sendLocalChanges() failed");
            }
            e.printStackTrace();
        }

        return !isError;
    }


    private boolean changeAttachOnServer(
            long featureId,
            long attachId,
            SyncResult syncResult)
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }

        AttachItem attach = getAttach("" + featureId, "" + attachId);
        if (null == attach) {   // just remove buggy item
            return true;
        }

        try {
            JSONObject putData = new JSONObject();
            //putData.put(JSON_ID_KEY, attach.getAttachId());
            putData.put(Constants.JSON_NAME_KEY, attach.getDisplayName());
            //putData.put("mime_type", attach.getMimetype());
            putData.put("description", attach.getDescription());

            HttpResponse response = changeAttachOnServer(featureId, attachId, putData.toString());

            if (!response.isOk()) {
                log(syncResult, response.getResponseCode() + "");
                return false;
            }

            return true;
        } catch (JSONException e) {
            log(e, "changeAttachOnServer JSONException");
            syncResult.stats.numParseExceptions++;
            return false;
        } catch (IOException e) {
            log(e, "changeAttachOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numUpdates++;
            return false;
        } catch (IllegalStateException e) {
            log(e, "changeAttachOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        }
    }


    protected HttpResponse changeAttachOnServer(long featureId, long attachId, String putData) throws IOException {
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);
        String url = NGWUtil.getFeatureAttachmentUrl(accountData.url, mRemoteId, featureId) + attachId;
        return NetworkUtil.put(url, putData, accountData.login,
                                                accountData.password, false);
    }


    private boolean deleteAttachOnServer(
            long featureId,
            long attachId,
            SyncResult syncResult)
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }

        try {
            HttpResponse response = deleteAttachOnServer(featureId, attachId);

            if (!response.isOk()) {
                syncResult.stats.numIoExceptions++;
                syncResult.stats.numEntries++;
                return false;
            }

            return true;
        } catch (IOException e) {
            log(e, "deleteAttachOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numDeletes++;
            return false;
        } catch (IllegalStateException e) {
            log(e, "deleteAttachOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        }
    }


    protected HttpResponse deleteAttachOnServer(long featureId, long attachId) throws IOException {
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        return NetworkUtil.delete(NGWUtil.getFeatureAttachmentUrl(accountData.url, mRemoteId, featureId)
                        + attachId, accountData.login, accountData.password, false);
    }


    protected boolean sendAttachOnServer(
            long featureId,
            long attachId,
            SyncResult syncResult)
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }

        AttachItem attach = getAttach("" + featureId, "" + attachId);
        if (null == attach) {   //just remove buggy item
            return true;
        }

        try {
            HttpResponse response = sendAttachOnServer(featureId, attach);

            if (!response.isOk()) {
                log(syncResult, response.getResponseCode() + "");
                return false;
            }

            JSONObject result = new JSONObject(response.getResponseBody());
            if (!proceedAttach(result, syncResult)) {
                return false;
            }

            response = sendFeatureAttachOnServer(result, featureId, attach);
            if (!response.isOk()) {
                log(syncResult, response.getResponseCode() + "");
                return false;
            }

            // set new local id for attach
            result = new JSONObject(response.getResponseBody());
            if (!result.has(Constants.JSON_ID_KEY)) {
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "Problem sendAttachOnServer(), result has not ID key, result: " + result.toString());
                }
                syncResult.stats.numParseExceptions++;
                return false;
            }

            long newAttachId = result.getLong(Constants.JSON_ID_KEY);
            setNewAttachId("" + featureId, attach, "" + newAttachId);

            return true;
        } catch (IOException e) {
            log(e, "sendAttachOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numInserts++;
            return false;
        }  catch (JSONException e) {
            log(e, "sendAttachOnServer JSONException");
            syncResult.stats.numParseExceptions++;
            return false;
        } catch (IllegalStateException e) {
            log(e, "sendAttachOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        }
    }


    protected boolean proceedAttach(JSONObject result, SyncResult syncResult) throws JSONException {
        // get attach info
        if (!result.has("upload_meta")) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "Problem sendAttachOnServer(), result has not upload_meta, result: " + result.toString());
            }
            syncResult.stats.numParseExceptions++;
            return false;
        }

        JSONArray uploadMetaArray = result.getJSONArray("upload_meta");
        if (uploadMetaArray.length() == 0) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "Problem sendAttachOnServer(), result upload_meta length() == 0");
            }
            syncResult.stats.numParseExceptions++;
            return false;
        }

        return true;
    }

    protected HttpResponse sendFeatureAttachOnServer(JSONObject result, long featureId, AttachItem attach) throws JSONException, IOException {
        // add attachment to row
        JSONObject postJsonData = new JSONObject();
        JSONArray uploadMetaArray = result.getJSONArray("upload_meta");
        postJsonData.put("file_upload", uploadMetaArray.get(0));
        postJsonData.put("description", attach.getDescription());
        String postload = postJsonData.toString();
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "postload: " + postload);
        }

        // get account data
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        // upload file
        String url = NGWUtil.getFeatureAttachmentUrl(accountData.url, mRemoteId, featureId);

        // update record in NGW
        return NetworkUtil.post(url, postload, accountData.login, accountData.password, false);
    }

    protected HttpResponse sendAttachOnServer(long featureId, AttachItem attach) throws IOException {
        // fill attach info
        String fileName = attach.getDisplayName();
        File filePath = new File(mPath, featureId + File.separator + attach.getAttachId());
        String fileMime = attach.getMimetype();

        // get account data
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        // upload file
        String url = NGWUtil.getFileUploadUrl(accountData.url);
        return NetworkUtil.postFile(url, fileName, filePath, fileMime, accountData.login, accountData.password, false);
    }

    protected void log(SyncResult syncResult, String code) {
        int responseCode = Integer.parseInt(code);
        switch (responseCode) {
            case HttpURLConnection.HTTP_UNAUTHORIZED:
            case HttpURLConnection.HTTP_FORBIDDEN:
                syncResult.stats.numAuthExceptions++;
                break;
            case 1:
                syncResult.stats.numParseExceptions++;
                break;
            case 0:
            default:
            case HttpURLConnection.HTTP_NOT_FOUND:
            case HttpURLConnection.HTTP_INTERNAL_ERROR:
                syncResult.stats.numIoExceptions++;
                syncResult.stats.numEntries++;
                break;
        }
    }


    protected void log(Exception e, String tag) {
        e.printStackTrace();
        if (Constants.DEBUG_MODE) {
            String error = e.getLocalizedMessage() == null ? tag + ": Exception" : e.getLocalizedMessage();
            Log.d(Constants.TAG, error);
        }
    }


    protected void changeFeatureId(
            long oldFeatureId,
            long newFeatureId)
    {
        if (oldFeatureId == newFeatureId) {
            return;
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }
        //update id in DB
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "old id: " + oldFeatureId + " new id: " + newFeatureId);
        }
        SQLiteDatabase db = map.getDatabase(false);
        ContentValues values = new ContentValues();
        values.put(Constants.FIELD_ID, newFeatureId);
        if (db.update(mPath.getName(), values, Constants.FIELD_ID + " = " + oldFeatureId, null)
                != 1) {
            Log.w(Constants.TAG, "failed to set new id");
        }

        //update id in cache
        Intent notify = new Intent(Constants.NOTIFY_UPDATE);
        notify.putExtra(Constants.FIELD_OLD_ID, oldFeatureId);
        notify.putExtra(Constants.FIELD_ID, newFeatureId);
        notify.putExtra(Constants.ATTRIBUTES_ONLY, true);
        notify.putExtra(Constants.NOTIFY_LAYER_NAME, mPath.getName());
        getContext().sendBroadcast(notify);

        //rename photo id folder if exist
        File photoFolder = new File(mPath, "" + oldFeatureId);
        if (photoFolder.exists()) {
            if (photoFolder.renameTo(new File(mPath, "" + newFeatureId))) {

                int chRes = FeatureChanges.changeFeatureIdForAttaches(getChangeTableName(),
                        oldFeatureId, newFeatureId);
                if (chRes <= 0) {
                    if (Constants.DEBUG_MODE) {
                        Log.d(Constants.TAG,
                                "Feature ID for attaches not changed, oldFeatureId: " + oldFeatureId
                                        + ", newFeatureId: " + newFeatureId);
                    }
                }

            } else {
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "rename photo folder " + oldFeatureId + "failed");
                }
            }
        }
    }


    public boolean getChangesFromServer(
            String authority,
            SyncResult syncResult)
    {
        if (!mNet.isNetworkAvailable()) {
            return false;
        }

        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "The network is available. Get changes from server");
        }

        List<Feature> features, added = null, deleted = null, changed = null;
        List<Long> deleteItems = new ArrayList<>();
        HashMap<Integer, List<Feature>> tracked = getFeatures(syncResult, mTracked);

        if (tracked == null)
            return false;

        if (mTracked) {
            added = tracked.get(0);
            changed = tracked.get(1);
            deleted = tracked.get(2);
            features = new ArrayList<>();
            if(null != added)
                features.addAll(added);
            if(null != changed)
                features.addAll(changed);
            if(null != deleted)
                features.addAll(deleted);

            if (Constants.DEBUG_MODE) {
                Log.d(TAG, "Layer " + mName + " is tracked for history");
                Log.d(Constants.TAG, "added: " + added.size() + " | changed: " + changed.size() + " | deleted: " + deleted.size());
            }
        } else
            features = tracked.get(0);

        if (features == null)
            return false;

        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "Got " + features.size() + " feature(s) from server");
        }

        try {
            if (!mCacheLoaded) {
                reloadCache();
            }

            String changeTableName = getChangeTableName();
            if (mTracked) {
                proceedAddedFeatures(added, authority, changeTableName);
                proceedChangedFeatures(changed, authority, changeTableName);
                proceedDeletedFeatures(deleted, changeTableName);
            } else {
                // analyse feature
                for (Feature remoteFeature : features) {
                    Cursor cursor = query(null, Constants.FIELD_ID + " = " + remoteFeature.getId(), null, null, null);

                    try {
                        //no local feature
                        if (null == cursor || cursor.getCount() == 0) {
                            //if we have changes (delete) not create new feature
                            boolean createNewFeature =
                                    !FeatureChanges.isChanges(changeTableName, remoteFeature.getId());

                            //create new feature with remoteId
                            if (createNewFeature)
                                createNewFeature(remoteFeature, authority);
                        } else {
                            compareFeature(cursor, authority, remoteFeature, changeTableName);
                        }
                    } catch (Exception e) {
                        //Log.d(TAG, e.getLocalizedMessage());
                    } finally {
                        if (null != cursor) {
                            cursor.close();
                        }
                    }
                }

                // remove features not exist on server from local layer
                // if no operation is in changes array or change operation for local feature present
                for (Long featureId : query(null)) {
                    boolean bDeleteFeature = true;
                    for (Feature remoteFeature : features) {
                        if (remoteFeature.getId() == featureId) {
                            bDeleteFeature = false;
                            break;
                        }
                    }

                    // if local item is in update list and state ADD_NEW skip delete
                    bDeleteFeature =
                            bDeleteFeature && !FeatureChanges.isChanges(changeTableName, featureId,
                                    Constants.CHANGE_OPERATION_NEW) &&
                                    !FeatureChanges.hasFeatureFlags(changeTableName, featureId);

                    if (bDeleteFeature) {
                        deleteItems.add(featureId);
                    }
                }

                deleteFeatures(deleteItems);
            }

            if (!mTracked) {
                Cursor changeCursor = FeatureChanges.getChanges(changeTableName);
                // remove changes already applied on server (delete already deleted id or add already added)
                if (null != changeCursor) {
                    try {
                        if (changeCursor.moveToFirst()) {
                            int recordIdColumn = changeCursor.getColumnIndex(Constants.FIELD_ID);
                            int featureIdColumn =
                                    changeCursor.getColumnIndex(Constants.FIELD_FEATURE_ID);
                            int operationColumn =
                                    changeCursor.getColumnIndex(Constants.FIELD_OPERATION);
                            int attachOperationColumn =
                                    changeCursor.getColumnIndex(Constants.FIELD_ATTACH_OPERATION);

                            do {
                                long changeRecordId = changeCursor.getLong(recordIdColumn);
                                long changeFeatureId = changeCursor.getLong(featureIdColumn);
                                int changeOperation = changeCursor.getInt(operationColumn);
                                int attachChangeOperation = changeCursor.getInt(attachOperationColumn);

                                boolean bDeleteChange = true; // if feature not exist on server
                                for (Feature remoteFeature : features) {
                                    if (remoteFeature.getId() == changeFeatureId) {
                                        if (0 != (changeOperation & Constants.CHANGE_OPERATION_NEW)) {
                                            // if feature already exist, just change it
                                            FeatureChanges.setOperation(changeTableName, changeRecordId,
                                                    Constants.CHANGE_OPERATION_CHANGED);
                                        }
                                        bDeleteChange = false; // in other cases just apply
                                        break;
                                    }
                                }

                                if ((0 != (changeOperation & Constants.CHANGE_OPERATION_NEW) || 0 != (
                                        attachChangeOperation & Constants.CHANGE_OPERATION_NEW))
                                        && bDeleteChange) {

                                    bDeleteChange = false;
                                }

                                if (bDeleteChange) {
                                    if (Constants.DEBUG_MODE) {
                                        Log.d(Constants.TAG,
                                                "Delete change for feature #" + changeFeatureId +
                                                        ", changeOperation " + changeOperation +
                                                        ", attachChangeOperation " +
                                                        attachChangeOperation);
                                    }
                                    // TODO: analise for operation, remove all equal
                                    FeatureChanges.removeChangeRecord(changeTableName, changeRecordId);
                                }

                            } while (changeCursor.moveToNext());
                        }
                    } catch (Exception e) {
                        //Log.d(TAG, e.getLocalizedMessage());
                    } finally {
                        changeCursor.close();
                    }
                }
            }
        } catch (SQLiteException | ConcurrentModificationException e) {
            syncResult.stats.numConflictDetectedExceptions++;
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "proceed getChangesFromServer() failed");
            }
            e.printStackTrace();
            return false;
        }

        getPreferences().edit().putLong(SettingsConstants.KEY_PREF_LAST_SYNC_TIMESTAMP, System.currentTimeMillis()).apply();
        return true;
    }


    protected void proceedAddedFeatures(List<Feature> added, String authority, String changeTableName) {
        if (added != null) {
            for (Feature remoteFeature : added) {
                Cursor cursor = query(null, Constants.FIELD_ID + " = " + remoteFeature.getId(), null, null, null);
                boolean hasFeature = false;
                if (cursor != null) {
                    if (cursor.moveToFirst()) {
                        compareFeature(cursor, authority, remoteFeature, changeTableName);
                        hasFeature = true;
                    }
                    cursor.close();
                }

                if (!hasFeature)
                    createNewFeature(remoteFeature, authority);
            }
        }
    }


    protected void proceedChangedFeatures(List<Feature> changed, String authority, String changeTableName) {
        if (changed != null) {
            for (Feature remoteFeature : changed) {
                Cursor cursor = query(null, Constants.FIELD_ID + " = " + remoteFeature.getId(), null, null, null);
                if (cursor != null) {
                    if (cursor.moveToFirst()) {
                        compareFeature(cursor, authority, remoteFeature, changeTableName);
                    }
                    cursor.close();
                }
            }
        }
    }


    protected void proceedDeletedFeatures(List<Feature> deleted, String changeTableName) {
        List<Long> deleteItems = new ArrayList<>();
        if (deleted != null) {
            for (Feature remoteFeature : deleted)
                deleteItems.add(remoteFeature.getId());

            deleteFeatures(deleteItems);
        }
    }


    protected void createNewFeature(Feature remoteFeature, String authority) {
        ContentValues values = remoteFeature.getContentValues(true);
        Uri uri = Uri.parse("content://" + authority + "/" + getPath().getName());
        //prevent add changes and events
        uri = uri.buildUpon().fragment(NO_SYNC).build();
        Uri newFeatureUri = insert(uri, values);
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "Add new feature from server - " + newFeatureUri.toString());
        }
    }


    protected void deleteFeatures(List<Long> deleteItems) {
        for (long itemId : deleteItems) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "Delete feature #" + itemId + " not exist on server");
            }
            delete(itemId, Constants.FIELD_ID + " = " + itemId, null);
        }
    }

    protected void compareFeature(Cursor cursor, String authority, Feature remoteFeature, String changeTableName) {
        cursor.moveToFirst();
        // with the given ID (remoteFeature.getId()) must be only one feature
        Feature currentFeature = cursorToFeature(cursor);

        //compare features
        boolean eqData = remoteFeature.equalsData(currentFeature);
        boolean eqAttach = remoteFeature.equalsAttachments(currentFeature);

        //process data
        if (eqData) {
            //remove from changes
            if (FeatureChanges.isChanges(changeTableName, remoteFeature.getId())) {
                if (eqAttach && !FeatureChanges.isAttachesForDelete(
                        changeTableName, remoteFeature.getId())
                        || !FeatureChanges.isAttachChanges(
                        changeTableName, remoteFeature.getId())) {

                    FeatureChanges.removeChanges(
                            changeTableName, remoteFeature.getId());
                }
            }
        } else {
            // we have local changes ready for sent to server
            boolean isChangedLocal = FeatureChanges.isChanges(changeTableName,
                    remoteFeature.getId());

            //no local changes - update local feature
            if (!isChangedLocal) {
                ContentValues values = remoteFeature.getContentValues(false);

                Uri uri = Uri.parse(
                        "content://" + authority + "/" + getPath().getName());
                Uri updateUri =
                        ContentUris.withAppendedId(uri, remoteFeature.getId());
                updateUri = updateUri.buildUpon().fragment(NO_SYNC).build();
                //prevent add changes
                int count = update(updateUri, values, null, null);
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG,
                            "Update feature (" + count + ") from server - " +
                                    remoteFeature.getId());
                }
            }
        }

        //process attachments
        if (eqAttach) {
            if (FeatureChanges.isChanges(changeTableName, remoteFeature.getId())
                    && (eqData || FeatureChanges.isAttachChanges(
                    changeTableName, remoteFeature.getId()))) {

                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "The feature " + remoteFeature.getId() +
                            " already changed on server. Remove changes for it");
                }

                FeatureChanges.removeChanges(
                        changeTableName, remoteFeature.getId());
            }

        } else {
            boolean isChangedLocal = FeatureChanges.isAttachChanges(changeTableName,
                    remoteFeature.getId());

            if (!isChangedLocal) {
                Iterator<String> iterator =
                        currentFeature.getAttachments().keySet().iterator();

                while (iterator.hasNext()) {
                    String attachId = iterator.next();

                    //delete attachment which not exist on server
                    if (!remoteFeature.getAttachments().containsKey(attachId)) {
                        iterator.remove();
                        saveAttach("" + currentFeature.getId(),
                                currentFeature.getAttachments());

                    } else { //or change attachment properties
                        AttachItem currentItem =
                                currentFeature.getAttachments().get(attachId);
                        AttachItem remoteItem =
                                remoteFeature.getAttachments().get(attachId);

                        if (null != currentItem && !currentItem.equals(
                                remoteItem)) {
                            long attachIdL =
                                    Long.parseLong(remoteItem.getAttachId());
                            boolean changeOnServer =
                                    !FeatureChanges.isAttachChanges(changeTableName,
                                            remoteFeature.getId(), attachIdL);

                            if (changeOnServer) {
                                currentItem.setDescription(
                                        remoteItem.getDescription());
                                currentItem.setMimetype(remoteItem.getMimetype());
                                currentItem.setDisplayName(
                                        remoteItem.getDisplayName());
                                saveAttach("" + currentFeature.getId(),
                                        currentFeature.getAttachments());
                            }
                        }
                    }
                }
            }
        }
    }


    protected HttpURLConnection getHttpConnection(AccountUtil.AccountData accountData) throws IOException {
        URL url = new URL(getFeaturesUrl(accountData));
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        final String basicAuth = NetworkUtil.getHTTPBaseAuth(accountData.login, accountData.password);
        if (null != basicAuth) {
            connection.setRequestProperty("Authorization", basicAuth);
        }

        return connection;
    }


    // read layer contents as string
    protected HashMap<Integer, List<Feature>> getFeatures(SyncResult syncResult, boolean tracked) {
        AccountUtil.AccountData accountData;
        try {
            accountData = AccountUtil.getAccountData(mContext, mAccountName);
        } catch (IllegalStateException e) {
            log(e, "getFeatures(): account is null");
            syncResult.stats.numAuthExceptions++;
            return null;
        }

        HashMap<Integer, List<Feature>> results = new HashMap<>();

        try {
            HttpURLConnection urlConnection = getHttpConnection(accountData);
            Log.d(TAG, "url: " + urlConnection.getURL().toString());

            InputStream in = new ProgressBufferedInputStream(urlConnection.getInputStream(), urlConnection.getContentLength());
            JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));

            if (tracked) {
                List<Feature> added = new LinkedList<>(), changed = new LinkedList<>(), deleted = new LinkedList<>();
                reader.beginObject();
                while (reader.hasNext()) {
                    String name = reader.nextName();
                    switch (name) {
                        case "deleted":
                            reader.beginArray();
                            while (reader.hasNext())
                                deleted.add(new Feature(reader.nextLong(), getFields()));
                            reader.endArray();
                            break;
                        case "added":
                            readFeatures(reader, added);
                            break;
                        case "changed":
                            readFeatures(reader, changed);
                            break;
                    }
                }
                reader.endObject();

                results.put(0, added);
                results.put(1, changed);
                results.put(2, deleted);
            } else {
                List<Feature> features = new LinkedList<>();
                readFeatures(reader, features);
                results.put(0, features);
            }
            reader.close();

            urlConnection.disconnect();
        } catch (MalformedURLException e) {
            log(e, "getFeatures(): MalformedURLException");
            syncResult.stats.numIoExceptions++;
            return null;
        } catch (FileNotFoundException e) {
            log(e, "getFeatures(): FileNotFoundException");
            syncResult.stats.numIoExceptions++;
            return null;
        } catch (IOException e) {
            log(e, "getFeatures(): IOException");
            syncResult.stats.numParseExceptions++;
            return null;
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numSkippedEntries++;
            return null;
        } catch (IllegalStateException | NumberFormatException e) {
            log(e, "getFeatures(): IllegalStateException | NumberFormatException");
            syncResult.stats.numParseExceptions++;
            return null;
        }

        return results;
    }


    protected void readFeatures(JsonReader reader, List<Feature> features) throws IOException, IllegalStateException, NumberFormatException, OutOfMemoryError {
        reader.beginArray();
        while (reader.hasNext()) {
            final Feature feature = NGWUtil.readNGWFeature(reader, getFields(), mCRS);
            if (feature.getGeometry() == null || !feature.getGeometry().isValid())
                continue;
            features.add(feature);
        }
        reader.endArray();
    }


    protected boolean addFeatureOnServer(
            long featureId,
            SyncResult syncResult)
            throws SQLiteException
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }
        Uri uri = ContentUris.withAppendedId(getContentUri(), featureId);
        uri = uri.buildUpon().fragment(NO_SYNC).build();

        Cursor cursor = query(uri, null, null, null, null, null);
        if (null == cursor) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "addFeatureOnServer: Get cursor failed");
            }
            return true; //just remove buggy data
        }

        try {
            if (cursor.moveToFirst()) {
                // feature to string
                String payload = cursorToJson(cursor);
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "payload: " + payload);
                }

                // post to NGW
                HttpResponse response = addFeatureOnServer(payload);

                if (!response.isOk()) {
                    log(syncResult, response.getResponseCode() + "");
                    return false;
                }

                //set new id from server // like: {"id": 24}
                JSONObject result = new JSONObject(response.getResponseBody());
                if (result.has(Constants.JSON_ID_KEY)) {
                    long id = result.getLong(Constants.JSON_ID_KEY);
                    changeFeatureId(featureId, id);
                }

                return true;
            } else {
                Log.d(Constants.TAG, "addFeatureOnServer: Get cursor failed");
                return true; //just remove buggy data
            }

        } catch (JSONException e) {
            log(e, "addFeatureOnServer JSONException");
            syncResult.stats.numParseExceptions++;
            return false;
        } catch (IOException e) {
            log(e, "addFeatureOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numInserts++;
            return false;
        } catch (SQLiteConstraintException e) {
            log(e, "addFeatureOnServer SQLiteConstraintException");
            syncResult.stats.numConflictDetectedExceptions++;
            return false;
        } catch (IllegalStateException e) {
            log(e, "addFeatureOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        } finally {
            cursor.close();
        }
    }


    protected HttpResponse addFeatureOnServer(String payload) throws IOException {
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        return NetworkUtil.post(NGWUtil.getFeaturesUrl(accountData.url, mRemoteId),
                         payload, accountData.login, accountData.password, false);
    }


    protected boolean deleteFeatureOnServer(
            long featureId,
            SyncResult syncResult)
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }

        try {
            HttpResponse response = deleteFeatureOnServer(featureId);

            if (!response.isOk()) {
                syncResult.stats.numIoExceptions++;
                syncResult.stats.numEntries++;
                return false;
            }

            return true;
        } catch (IOException e) {
            log(e, "deleteFeatureOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numDeletes++;
            return false;
        } catch (IllegalStateException e) {
            log(e, "deleteFeatureOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        }
    }


    protected HttpResponse deleteFeatureOnServer(long featureId) throws IOException {
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        return NetworkUtil.delete(NGWUtil.getFeatureUrl(accountData.url, mRemoteId, featureId),
                                   accountData.login, accountData.password, false);
    }


    protected boolean changeFeatureOnServer(
            long featureId,
            SyncResult syncResult)
            throws SQLiteException
    {
        if (!mNet.isNetworkAvailable()) {
            syncResult.stats.numIoExceptions++;
            return false;
        }

        // get uri for feature
        Uri uri = ContentUris.withAppendedId(getContentUri(), featureId);
        uri = uri.buildUpon().fragment(NO_SYNC).build();

        // get it's cursor
        Cursor cursor = query(uri, null, null, null, null, null);
        if (null == cursor) {
            if (Constants.DEBUG_MODE) {
                Log.d(Constants.TAG, "empty cursor for uri: " + uri);
            }
            return true; //just remove buggy data
        }

        try {
            if (cursor.moveToFirst()) {
                // get payload from cursor
                String payload = cursorToJson(cursor);
                if (Constants.DEBUG_MODE) {
                    Log.d(Constants.TAG, "payload: " + payload);
                }

                // change on server
                HttpResponse response = changeFeatureOnServer(featureId, payload);

                if (!response.isOk()) {
                    log(syncResult, response.getResponseCode() + "");
                    return false;
                }

                return true;
            } else {
                Log.d(Constants.TAG, "changeFeatureOnServer(), empty cursor for uri: " + uri);
                return true; //just remove buggy data
            }
        } catch (IllegalStateException e) {
            log(e, "changeFeatureOnServer IllegalStateException");
            syncResult.stats.numAuthExceptions++;
            return false;
        } catch (IOException e) {
            log(e, "changeFeatureOnServer IOException");
            syncResult.stats.numIoExceptions++;
            syncResult.stats.numUpdates++;
            return false;
        } catch (JSONException e) {
            log(e, "changeFeatureOnServer JSONException");
            syncResult.stats.numParseExceptions++;
            return false;
        } finally {
            cursor.close();
        }
    }


    protected HttpResponse changeFeatureOnServer(long featureId, String payload) throws IOException {
        AccountUtil.AccountData accountData = AccountUtil.getAccountData(mContext, mAccountName);

        // change on server
        String url = NGWUtil.getFeatureUrl(accountData.url, mRemoteId, featureId);
        return NetworkUtil.put(url, payload, accountData.login, accountData.password, false);
    }


    protected String cursorToJson(Cursor cursor)
            throws JSONException, IOException
    {
        JSONObject rootObject = new JSONObject();
        if (0 != (mSyncType & Constants.SYNC_ATTRIBUTES)) {
            JSONObject valueObject = new JSONObject();
            for (int i = 0; i < cursor.getColumnCount(); i++) {
                String name = cursor.getColumnName(i);
                if (name.equals(Constants.FIELD_ID) || name.equals(Constants.FIELD_GEOM)) {
                    continue;
                }

                Field field = mFields.get(cursor.getColumnName(i));
                if (null == field) {
                    continue;
                }

                switch (field.getType()) {
                    case GeoConstants.FTReal:
                        valueObject.put(name, cursor.getFloat(i));
                        break;
                    case GeoConstants.FTInteger:
                        valueObject.put(name, cursor.getInt(i));
                        break;
                    case GeoConstants.FTString:
                        String stringVal = cursor.getString(i);
                        if (null != stringVal && !stringVal.equals("null")) {
                            valueObject.put(name, stringVal);
                        }
                        break;
                    case GeoConstants.FTDateTime:
                        TimeZone timeZoneDT = TimeZone.getDefault();
                        timeZoneDT.setRawOffset(0); // set to UTC
                        Calendar calendarDT = Calendar.getInstance(timeZoneDT);
                        calendarDT.setTimeInMillis(cursor.getLong(i));
                        JSONObject jsonDateTime = new JSONObject();
                        jsonDateTime.put("year", calendarDT.get(Calendar.YEAR));
                        jsonDateTime.put("month", calendarDT.get(Calendar.MONTH) + 1);
                        jsonDateTime.put("day", calendarDT.get(Calendar.DAY_OF_MONTH));
                        jsonDateTime.put("hour", calendarDT.get(Calendar.HOUR_OF_DAY));
                        jsonDateTime.put("minute", calendarDT.get(Calendar.MINUTE));
                        jsonDateTime.put("second", calendarDT.get(Calendar.SECOND));
                        valueObject.put(name, jsonDateTime);
                        break;
                    case GeoConstants.FTDate:
                        TimeZone timeZoneD = TimeZone.getDefault();
                        timeZoneD.setRawOffset(0); // set to UTC
                        Calendar calendarD = Calendar.getInstance(timeZoneD);
                        calendarD.setTimeInMillis(cursor.getLong(i));
                        JSONObject jsonDate = new JSONObject();
                        jsonDate.put("year", calendarD.get(Calendar.YEAR));
                        jsonDate.put("month", calendarD.get(Calendar.MONTH) + 1);
                        jsonDate.put("day", calendarD.get(Calendar.DAY_OF_MONTH));
                        valueObject.put(name, jsonDate);
                        break;
                    case GeoConstants.FTTime:
                        TimeZone timeZoneT = TimeZone.getDefault();
                        timeZoneT.setRawOffset(0); // set to UTC
                        Calendar calendarT = Calendar.getInstance(timeZoneT);
                        calendarT.setTimeInMillis(cursor.getLong(i));
                        JSONObject jsonTime = new JSONObject();
                        jsonTime.put("hour", calendarT.get(Calendar.HOUR_OF_DAY));
                        jsonTime.put("minute", calendarT.get(Calendar.MINUTE));
                        jsonTime.put("second", calendarT.get(Calendar.SECOND));
                        valueObject.put(name, jsonTime);
                        break;
                    default:
                        break;
                }
            }
            rootObject.put(NGWUtil.NGWKEY_FIELDS, valueObject);
        }

        if (0 != (mSyncType & Constants.SYNC_GEOMETRY)) {
            //may be found geometry in cache by id is faster
            GeoGeometry geometry = GeoGeometryFactory.fromBlob(
                    cursor.getBlob(cursor.getColumnIndex(Constants.FIELD_GEOM)));

            geometry.setCRS(GeoConstants.CRS_WEB_MERCATOR);
            if (mCRS != GeoConstants.CRS_WEB_MERCATOR)
                geometry.project(mCRS);

            rootObject.put(NGWUtil.NGWKEY_GEOM, geometry.toWKT(true));
            //rootObject.put("id", cursor.getLong(cursor.getColumnIndex(FIELD_ID)));
        }

        return rootObject.toString();
    }


    /**
     * get synchronization type
     *
     * @return the synchronization type - the OR of this values: SYNC_NONE - no synchronization
     * SYNC_DATA - synchronize only data SYNC_ATTACH - synchronize only attachments SYNC_ALL -
     * synchronize everything
     */
    @Override
    public int getSyncType()
    {
        return mSyncType;
    }


    protected synchronized void applySync(int syncType)
    {
        if (syncType == Constants.SYNC_NONE) {
            FeatureChanges.removeAllChanges(getChangeTableName());
        } else {
            if (mTracked)
                return;

            for (Long featureId : query(null)) {
                addChange(featureId, Constants.CHANGE_OPERATION_NEW);
                //add attach
                File attacheFolder = new File(mPath, "" + featureId);
                if (attacheFolder.isDirectory()) {
                    for (File attach : attacheFolder.listFiles()) {
                        String attachId = attach.getName();
                        if (attachId.equals(META)) {
                            continue;
                        }
                        Long attachIdL = Long.parseLong(attachId);
                        if (attachIdL >= Constants.MIN_LOCAL_FEATURE_ID) {
                            addChange(featureId, attachIdL, Constants.CHANGE_OPERATION_NEW);
                        }
                    }
                }
            }
        }
    }


    @Override
    public void setSyncType(int syncType)
    {
        if (!isSyncable()) {
            return;
        }

        if (mSyncType == syncType) {
            return;
        }

        if (syncType == Constants.SYNC_NONE) {
            mSyncType = syncType;

            new Thread(new Runnable()
            {
                public void run()
                {
                    android.os.Process.setThreadPriority(
                            android.os.Process.THREAD_PRIORITY_BACKGROUND);
                    applySync(Constants.SYNC_NONE);
                }
            }).start();

        } else if (mSyncType == Constants.SYNC_NONE && 0 != (syncType & Constants.SYNC_DATA)) {
            mSyncType = syncType;

            new Thread(new Runnable()
            {
                public void run()
                {
                    android.os.Process.setThreadPriority(
                            android.os.Process.THREAD_PRIORITY_BACKGROUND);
                    applySync(Constants.SYNC_ALL);
                }
            }).start();


        } else {
            mSyncType = syncType;
        }
    }


    @Override
    public boolean delete()
            throws SQLiteException
    {
        FeatureChanges.delete(getChangeTableName());

        return super.delete();
    }


    /**
     * Indicate if layer can sync data with remote server
     *
     * @return true if layer can sync or false
     */
    public boolean isSyncable()
    {
        return true;
    }


    /**
     * Indicate if layer can send changes to remote server
     *
     * @return true if layer can send changes to remote server or false
     */
    public boolean isRemoteReadOnly()
    {
        return !(mNGWLayerType == Connection.NGWResourceTypeVectorLayer || mNGWLayerType == NGWResourceTypePostgisLayer);
    }


    @Override
    protected Cursor queryInternal(
            Uri uri,
            int uriType,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit)
    {
        String featureId;
        String attachId;
        List<String> pathSegments;

        String changeTableName = getChangeTableName();

        switch (uriType) {
            case TYPE_CHANGES_TABLE: {
                return FeatureChanges.query(
                        changeTableName, projection, selection, selectionArgs, sortOrder, limit);
            }

            case TYPE_CHANGES_FEATURE: {
                featureId = uri.getLastPathSegment();

                String changeSel = FIELD_FEATURE_ID + " = " + featureId;

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.query(
                        changeTableName, projection, selection, selectionArgs, sortOrder, limit);
            }

            case TYPE_CHANGES_ATTACH: {
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 2);

                String changeSel = FIELD_FEATURE_ID + " = " + featureId + " AND " + "( 0 != ( "
                        + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )";

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.query(
                        changeTableName, projection, selection, selectionArgs, sortOrder, limit);
            }

            case TYPE_CHANGES_ATTACH_ID: {
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 3);
                attachId = uri.getLastPathSegment();

                String changeSel = FIELD_FEATURE_ID + " = " + featureId + " AND " + "( 0 != ( "
                        + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) ) AND "
                        + FIELD_ATTACH_ID + " = " + attachId;

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.query(
                        changeTableName, projection, selection, selectionArgs, sortOrder, limit);
            }

            default: {
                return super.queryInternal(
                        uri, uriType, projection, selection, selectionArgs, sortOrder, limit);
            }
        }
    }


    @Override
    protected int deleteInternal(
            Uri uri,
            int uriType,
            String selection,
            String[] selectionArgs)
    {
        String featureId;
        String attachId;
        List<String> pathSegments;

        String changeTableName = getChangeTableName();

        switch (uriType) {

            case TYPE_CHANGES_TABLE: {
                return FeatureChanges.delete(changeTableName, selection, selectionArgs);
            }

            case TYPE_CHANGES_FEATURE: {
                featureId = uri.getLastPathSegment();

                String changeSel = FIELD_FEATURE_ID + " = " + featureId;

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.delete(changeTableName, selection, selectionArgs);
            }

            case TYPE_CHANGES_ATTACH: {
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 2);

                String changeSel = FIELD_FEATURE_ID + " = " + featureId + " AND " + "( 0 != ( "
                        + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )";

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.delete(changeTableName, selection, selectionArgs);
            }

            case TYPE_CHANGES_ATTACH_ID: {
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 3);
                attachId = uri.getLastPathSegment();

                String changeSel = FIELD_FEATURE_ID + " = " + featureId + " AND " + "( 0 != ( "
                        + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) ) AND "
                        + FIELD_ATTACH_ID + " = " + attachId;

                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection += " AND " + changeSel;
                }

                return FeatureChanges.delete(changeTableName, selection, selectionArgs);
            }

            default: {
                return super.deleteInternal(uri, uriType, selection, selectionArgs);
            }
        }
    }


    @Override
    public boolean isChanges()
    {
        return FeatureChanges.isChanges(getChangeTableName());
    }


    @Override
    protected boolean haveFeaturesNotSyncFlag()
    {
        return FeatureChanges.haveFeaturesNotSyncFlag(getChangeTableName());
    }


    @Override
    protected boolean hasFeatureChanges(long featureId)
    {
        return FeatureChanges.isChanges(getChangeTableName(), featureId);
    }


    @Override
    protected boolean hasAttachChanges(
            long featureId,
            long attachId)
    {
        return FeatureChanges.isAttachChanges(getChangeTableName(), featureId, attachId);
    }


    @Override
    public Cursor queryFirstTempFeatureFlags()
    {
        // TODO: move work with temp features into VectorLayer
        String selection = "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        Cursor cursor =
                FeatureChanges.query(getChangeTableName(), selection, FIELD_ID + " ASC", "1");

        if (null != cursor) {

            if (cursor.moveToFirst()) {
                return cursor;
            }

            cursor.close();
        }

        return null;
    }


    @Override
    public Cursor queryFirstTempAttachFlags()
    {
        // TODO: move work with temp features into VectorLayer
        String selection = "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        Cursor cursor =
                FeatureChanges.query(getChangeTableName(), selection, FIELD_ID + " ASC", "1");

        if (null != cursor) {

            if (cursor.moveToFirst()) {
                return cursor;
            }

            cursor.close();
        }

        return null;
    }


    @Override
    public boolean hasFeatureTempFlag(long featureId)
    {
        // TODO: move work with temp features into VectorLayer
        return FeatureChanges.hasFeatureTempFlag(getChangeTableName(), featureId);
    }


    @Override
    public boolean hasFeatureNotSyncFlag(long featureId)
    {
        return FeatureChanges.hasFeatureNotSyncFlag(getChangeTableName(), featureId);
    }


    @Override
    public boolean hasAttachTempFlag(
            long featureId,
            long attachId)
    {
        // TODO: move work with temp features into VectorLayer
        return FeatureChanges.hasAttachTempFlag(getChangeTableName(), featureId, attachId);
    }


    @Override
    public boolean hasAttachNotSyncFlag(
            long featureId,
            long attachId)
    {
        return FeatureChanges.hasAttachNotSyncFlag(getChangeTableName(), featureId, attachId);
    }


    @Override
    public long setFeatureTempFlag(
            long featureId,
            boolean flag)
    {
        // TODO: move work with temp features into VectorLayer
        if (flag) {
            return FeatureChanges.setFeatureTempFlag(getChangeTableName(), featureId);
        } else {
            return FeatureChanges.deleteFeatureTempFlag(getChangeTableName(), featureId);
        }
    }


    @Override
    public long setFeatureNotSyncFlag(
            long featureId,
            boolean flag)
    {
        if (flag) {
            return FeatureChanges.setFeatureNotSyncFlag(getChangeTableName(), featureId);
        } else {
            return FeatureChanges.deleteFeatureNotSyncFlag(getChangeTableName(), featureId);
        }
    }


    @Override
    public long setAttachTempFlag(
            long featureId,
            long attachId,
            boolean flag)
    {
        // TODO: move work with temp features into VectorLayer
        if (flag) {
            return FeatureChanges.setAttachTempFlag(getChangeTableName(), featureId, attachId);
        } else {
            return FeatureChanges.deleteAttachTempFlag(getChangeTableName(), featureId, attachId);
        }
    }


    @Override
    public long setAttachNotSyncFlag(
            long featureId,
            long attachId,
            boolean flag)
    {
        if (flag) {
            return FeatureChanges.setAttachNotSyncFlag(getChangeTableName(), featureId, attachId);
        } else {
            return FeatureChanges.deleteAttachNotSyncFlag(
                    getChangeTableName(), featureId, attachId);
        }
    }


    @Override
    public int deleteAllTempFeaturesFlags()
    {
        // TODO: move work with temp features into VectorLayer
        String selection = "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        return FeatureChanges.delete(getChangeTableName(), selection);
    }


    @Override
    public int deleteAllTempAttachesFlags()
    {
        // TODO: move work with temp features into VectorLayer
        String selection = "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        return FeatureChanges.delete(getChangeTableName(), selection);
    }
}