# vim: fileencoding=utf-8
#
import os
from .base_format import Format, FormatError
from pyparsing import Literal, Regex, SkipTo, LineEnd, lineno, LineStart
from calculate.utils.package import PackageAtomParser, Package, PackageNotFound
from calculate.utils.files import join_paths
from glob import iglob
from fnmatch import fnmatch


ADD, REMOVE, MOVE = range(0, 3)


class ContentsFormat(Format):
    FORMAT = 'contents'
    EXECUTABLE = True

    _initialized = False

    def __new__(cls, *args, **kwargs):
        if not cls._initialized:
            cls._initialize_parser()
            return super().__new__(cls)

    def __init__(self, template_text: str,
                 template_path: str,
                 ignore_comments=None):
        self._command_methods = {ADD: self._add_command,
                                 REMOVE: self._remove_command,
                                 MOVE: self._move_command}
        self._parse_errors = []

        self._commands = self._parse_template(template_text)
        if self._parse_errors:
            raise FormatError('errors while parsing template:\n{}'.
                              format('\n'.join(self._parse_errors)))
        self._template_path = template_path
        self._packages = dict()
        self._atom_parser = None

    def execute_format(self, target_path, chroot_path='/'):
        '''Метод для запуска работы формата.'''
        self._package = dict()
        self._atom_parser = PackageAtomParser(chroot_path=chroot_path)
        for command in self._commands:
            target_package = self._get_package(command['target'], chroot_path)

            if 'source' not in command:
                source_package = None
            else:
                source_package = self._get_package(command['source'],
                                                   chroot_path)

            self._command_methods[command['action']](target_package,
                                                     command['path'],
                                                     command['lineno'],
                                                     source=source_package)
        self._save_changes()
        return {}

    def _add_command(self, target, glob_path, lineno, source=None):
        # Если файл уже есть в пакете -- ничего не делаем.
        glob_path = join_paths(target.chroot_path, glob_path)
        for file_path in iglob(glob_path):
            # Если файл уже в пакете -- ничего не делаем.
            if file_path in target:
                continue

            # Проверяем существование файла.
            if not os.path.exists(file_path):
                raise FormatError(f"Format processing error:{lineno}: file "
                                  f"'{file_path}' does not exist.")

            # Проверяем, не принадлежит ли файл какому-нибудь другому пакету.
            try:
                file_package = self._atom_parser.get_file_package(file_path)
            except PackageNotFound:
                file_package = None
            if file_package is not None:
                raise FormatError(f"Format processing error:{lineno}: can not"
                                  f" add file '{file_path}' belonging to the"
                                  f" package {file_package} into the package"
                                  f" '{target}'.")

            target.add_file(file_path)

    def _remove_command(self, target, glob_path, lineno, source=None):
        paths_to_remove = []
        if glob_path.endswith('/'):
            glob_path = glob_path[0:-1]

        for file_path in target.contents_dictionary.keys():
            if fnmatch(file_path, glob_path):
                paths_to_remove.append(file_path)

        for file_path in paths_to_remove:
            target.remove_file(file_path)

    def _move_command(self, target, glob_path, lineno, source=None):
        if source is None:
            return

        paths_to_move = []
        if glob_path.endswith('/'):
            glob_path = glob_path[0:-1]

        for file_path in source.contents_dictionary.keys():
            if fnmatch(file_path, glob_path):
                paths_to_move.append(file_path)

        for file_path in paths_to_move:
            removed = source.remove_file(file_path)

            for file_path, file_info in removed.items():
                if file_info['type'] == 'dir':
                    target.add_dir(file_path)
                elif file_info['type'] == 'obj':
                    target.add_obj(file_path,
                                   file_md5=file_info['md5'],
                                   mtime=file_info['mtime'])
                elif file_info['type'] == 'sym':
                    target.add_sym(file_path,
                                   target_path=file_info['target'],
                                   mtime=file_info['mtime'])
        target.sort_contents_dictionary()

    def _save_changes(self):
        for atom_name, package in self._packages.items():
            package.write_contents_file()
        self._packages = {}

    def _get_package(self, atom_name: str, chroot_path: str) -> Package:
        atom = self._atom_parser.parse_package_parameter(atom_name)
        if atom not in self._packages:
            package = Package(atom, chroot_path=chroot_path)
            self._packages.update({atom: package})
        else:
            package = self._packages[atom]

        return package

    def _parse_template(self, template_text: str) -> list:
        for tokens, start, end in self.template_parser.scanString(
                                                                template_text):
            parse_result = tokens[0]
            if parse_result is None:
                continue
            elif 'error' in parse_result:
                error = (f'Parse Error:{parse_result["lineno"]}:'
                         f' {parse_result["error"]}')
                self._parse_errors.append(error)
                continue
            yield parse_result

    @classmethod
    def _initialize_parser(cls):
        atom = Regex(PackageAtomParser.atom_regex)
        remove_symbol = Literal('!')
        path = Regex(r'\S+')

        add_parser = atom('target') + path('path') + LineEnd().suppress()
        add_parser.setParseAction(cls._parse_add)

        move_parser = (atom('source') + Literal(',').suppress()
                       + atom('target') + path('path') + LineEnd().suppress())
        move_parser.setParseAction(cls._parse_move)

        remove_parser = (remove_symbol.suppress() + atom('target')
                         + path('path') + LineEnd().suppress())
        remove_parser.setParseAction(cls._parse_remove)

        empty_line = LineStart().suppress() + LineEnd().suppress()
        empty_line.setParseAction(cls._parse_skip)

        comment = Literal('#') + SkipTo(LineEnd(), include=True)
        comment.setParseAction(cls._parse_skip)

        unexpected = SkipTo(LineEnd(), include=True)
        unexpected.setParseAction(cls._parse_unexpected)

        cls.template_parser = (move_parser | remove_parser | add_parser
                               | empty_line | comment | unexpected)

    @classmethod
    def _parse_add(cls, string, location, parse_result):
        target = parse_result.asDict()['target']
        path = parse_result.asDict()['path']
        return {'action': ADD, 'target': target, 'path': path,
                'lineno': lineno(location, string)}

    @classmethod
    def _parse_remove(cls, string, location, parse_result):
        target = parse_result.asDict()['target']
        path = parse_result.asDict()['path']
        return {'action': REMOVE, 'target': target, 'path': path,
                'lineno': lineno(location, string)}

    @classmethod
    def _parse_move(cls, string, location, parse_result):
        source = parse_result.asDict()['source']
        target = parse_result.asDict()['target']
        path = parse_result.asDict()['path']
        return {'action': MOVE, 'source': source,
                'target': target, 'path': path,
                'lineno': lineno(location, string)}

    @classmethod
    def _parse_skip(cls, parse_result):
        return [None]

    @classmethod
    def _parse_unexpected(cls, string, location, parse_result):
        result = parse_result[0]
        if not result:
            return [None]
        return {'error': result, 'lineno': lineno(location, string)}
