# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import tarfile
from binascii import crc32
from os.path import getmtime, getsize, isdir, isfile, join

try:
    from twisted.python import constants  # pylint: disable=import-error
except ImportError:
    # https://docs.twisted.org/en/twisted-16.5.0/core/howto/constants.html
    import constantly as constants  # pylint: disable=import-error

from platformio.compat import hashlib_encode_data


class PROJECT_SYNC_STAGE(constants.Flags):
    INIT = constants.FlagConstant()
    DBINDEX = constants.FlagConstant()
    DELETE = constants.FlagConstant()
    UPLOAD = constants.FlagConstant()
    EXTRACTED = constants.FlagConstant()
    COMPLETED = constants.FlagConstant()


class ProjectSync:
    def __init__(self, path):
        self.path = path
        if not isdir(self.path):
            os.makedirs(self.path)
        self.items = []
        self._db = {}

    def add_item(self, path, relpath, cb_filter=None):
        self.items.append((path, relpath, cb_filter))

    def get_items(self):
        return self.items

    def rebuild_dbindex(self):
        self._db = {}
        for path, relpath, cb_filter in self.items:
            if cb_filter and not cb_filter(path):
                continue
            self._insert_to_db(path, relpath)
            if not isdir(path):
                continue
            for root, _, files in os.walk(path, followlinks=True):
                for name in files:
                    self._insert_to_db(
                        join(root, name), join(relpath, root[len(path) + 1 :], name)
                    )

    def _insert_to_db(self, path, relpath):
        if not isfile(path):
            return
        index_hash = "%s-%s-%s" % (relpath, getmtime(path), getsize(path))
        index = crc32(hashlib_encode_data(index_hash))
        self._db[index] = (path, relpath)

    def get_dbindex(self):
        return list(self._db.keys())

    def delete_dbindex(self, dbindex):
        for index in dbindex:
            if index not in self._db:
                continue
            path = self._db[index][0]
            if isfile(path):
                os.remove(path)
            del self._db[index]
        self.delete_empty_folders()
        return True

    def delete_empty_folders(self):
        deleted = False
        for item in self.items:
            if not isdir(item[0]):
                continue
            for root, dirs, files in os.walk(item[0]):
                if not dirs and not files and root != item[0]:
                    deleted = True
                    os.rmdir(root)
        if deleted:
            return self.delete_empty_folders()

        return True

    def compress_items(self, fileobj, dbindex, max_size):
        compressed = []
        total_size = 0
        tar_opts = dict(fileobj=fileobj, mode="w:gz", bufsize=0, dereference=True)
        with tarfile.open(**tar_opts) as tgz:
            for index in dbindex:
                compressed.append(index)
                if index not in self._db:
                    continue
                path, relpath = self._db[index]
                tgz.add(path, relpath)
                total_size += getsize(path)
                if total_size > max_size:
                    break
        return compressed

    def decompress_items(self, fileobj):
        fileobj.seek(0)
        with tarfile.open(fileobj=fileobj, mode="r:gz") as tgz:
            tgz.extractall(self.path)
        return True
