# -*- coding: utf-8 -*-

"""Installable PyCharm application descriptions."""

__all__ = ['PyCharmProApp', 'PyCharmCEApp']

import contextlib
import json
import os
import re
import typing
import webbrowser
from anaconda_navigator import config as navigator_config
from anaconda_navigator.static import images
from . import base
from . import detectors

if typing.TYPE_CHECKING:
    import typing_extensions
    from anaconda_navigator.api import process
    from anaconda_navigator.config import user as user_config


MAC_APPS: typing.Optional[str] = detectors.join(
    detectors.Mac.home, 'Library', 'Application Support', 'JetBrains', 'Toolbox', 'apps',
)
LINUX_APPS: typing.Optional[str] = detectors.join(
    detectors.Linux.home, '.local', 'share', 'JetBrains', 'Toolbox', 'apps',
)
WIN_APPS: typing.Optional[str] = detectors.join(
    detectors.Win.local_app_data, 'JetBrains', 'Toolbox', 'apps',
)


class CheckVersion:  # pylint: disable=too-few-public-methods
    """
    Detect version of the PyCharm application.

    :param product_code: acceptable product code(s)

                         Known options:

                         - PY: PyCharm Pro
                         - PC: PyCharm Community
                         - PE: PyCharm Edu
    :param product_info_path: Path to the `product-info.json` file relative to the root of the application.
    :param build_path: Path to the `build.txt` file relative to the root of the application.
    """

    __slots__ = ('__build_path', '__product_code', '__product_info_path')

    build_pattern: 'typing_extensions.Final[typing.Pattern[str]]' = re.compile(
        r'(?P<product>[a-zA-Z0-9]+)-(?P<build>[0-9.]+)',
    )
    known_builds: 'typing_extensions.Final[typing.Mapping[str, str]]' = {
        '101.15': '1.1.1',
        '105.58': '1.2.1',
        '107.756': '1.5.4',
        '111.291': '2.0.2',
        '117.663': '2.5.2',
        '121.378': '2.6.3',
        '125.57': '2.7',
        '125.92': '2.7.1',
        '129.314': '2.7.2',
        '129.782': '2.7.3',
        '129.1566': '2.7.4',
        '131.19': '3.0',
        '131.339': '3.0.1',
        '131.618': '3.0.2',
        '131.849': '3.0.3',
        '133.804': '3.1',
        '133.881': '3.1.1',
        '133.1229': '3.1.2',
        '133.1347': '3.1.3',
        '133.1884': '3.1.4',
        '135.973': '3.4',
        '135.1057': '3.4.1',
        '135.1317': '3.4.2',
        '135.1318': '3.4.3',
        '135.1357': '3.4.4',
        '139.487': '4.0',
        '139.574': '4.0.1',
        '139.711': '4.0.2',
        '139.781': '4.0.3',
        '139.1001': '4.0.4',
        '139.1547': '4.0.5',
        '139.1659': '4.0.6',
        '139.1803': '4.0.7',
        '141.1116': '4.5',
        '141.1245': '4.5.1',
        '141.158': '4.5.2',
        '141.1899': '4.5.3',
        '141.2569': '4.5.4',
        '141.3058': '4.5.5',
        '143.589': '5.0',
        '143.595': '5.0.1',
        '143.1184': '5.0.2',
        '143.1559': '5.0.3',
        '143.1919': '5.0.4',
        '143.2370': '5.0.5',
        '143.2371': '5.0.6',
        '145.26': '2016.1',
        '145.598': '2016.1.1',
        '145.844': '2016.1.2',
        '145.971': '2016.1.3',
        '145.1504': '2016.1.4',
        '145.2073.10': '2016.1.5',
        '162.1237.1': '2016.2',
        '162.1628.8': '2016.2.1',
        '162.1812.1': '2016.2.2',
        '162.1967.10': '2016.2.3',
        '163.8233.8': '2016.3',
        '163.9735.8': '2016.3.1',
        '163.10154.50': '2016.3.2',
        '163.15188.4': '2016.3.3',
        '163.15529.21': '2016.3.4',
        '163.15529.24': '2016.3.5',
        '163.15529.25': '2016.3.6',
        '171.3780.115': '2017.1',
        '171.4249.47': '2017.1.2',
        '171.4424.42': '2017.1.3',
        '171.4694.38': '2017.1.4',
        '171.4694.67': '2017.1.5',
        '171.4694.79': '2017.1.6',
        '171.4694.87': '2017.1.7',
        '171.4694.94': '2017.1.8',
        '172.3317.103': '2017.2',
        '172.3544.46': '2017.2.1',
        '172.3757.67': '2017.2.2',
        '172.3968.37': '2017.2.3',
        '172.4343.24': '2017.2.4',
        '172.4574.27': '2017.2.5',
        '172.4574.33': '2017.2.6',
        '172.4574.37': '2017.2.7',
        '173.3727.137': '2017.3',
        '173.3942.36': '2017.3.1',
        '173.4127.16': '2017.3.2',
        '173.4301.16': '2017.3.3',
        '173.4674.37': '2017.3.4',
        '173.4674.54': '2017.3.5',
        '173.4674.57': '2017.3.6',
        '173.4674.62': '2017.3.7',
        '181.4203.547': '2018.1',
        '181.4445.76': '2018.1.1',
        '181.4668.75': '2018.1.2',
        '181.4892.64': '2018.1.3',
        '181.5087.37': '2018.1.4',
        '181.5540.17': '2018.1.5',
        '181.5540.34': '2018.1.6',
        '182.3684.100': '2018.2',
        '182.3911.33': '2018.2.1',
        '182.4129.34': '2018.2.2',
        '182.4323.49': '2018.2.3',
        '182.4505.26': '2018.2.4',
        '182.5107.22': '2018.2.5',
        '182.5107.44': '2018.2.6',
        '182.5107.56': '2018.2.7',
        '182.5262.4': '2018.2.8',
        '183.4284.139': '2018.3',
        '183.4588.64': '2018.3.1',
        '183.4886.43': '2018.3.2',
        '183.5153.39': '2018.3.3',
        '183.5429.31': '2018.3.4',
        '183.5912.18': '2018.3.5',
        '183.6156.13': '2018.3.6',
        '183.6156.16': '2018.3.7',
        '191.6183.50': '2019.1',
        '191.6605.12': '2019.1.1',
        '191.7141.48': '2019.1.2',
        '191.7479.30': '2019.1.3',
        '191.8026.44': '2019.1.4',
        '192.5728.105': '2019.2',
        '192.6262.63': '2019.2.1',
        '192.6603.34': '2019.2.2',
        '192.6817.19': '2019.2.3',
        '192.7142.42': '2019.2.4',
        '192.7142.56': '2019.2.5',
        '192.7142.79': '2019.2.6',
        '193.5233.109': '2019.3',
        '193.5662.61': '2019.3.1',
        '193.6015.41': '2019.3.2',
        '193.6494.30': '2019.3.3',
        '193.6911.25': '2019.3.4',
        '193.7288.30': '2019.3.5',
        '201.6668.115': '2020.1',
        '201.7223.92': '2020.1.1',
        '201.7846.77': '2020.1.2',
        '201.8538.36': '2020.1.3',
        '201.8743.11': '2020.1.4',
        '201.8743.20': '2020.1.5',
        '202.6397.98': '2020.2',
        '202.6948.78': '2020.2.1',
        '202.7319.64': '2020.2.2',
        '202.7660.27': '2020.2.3',
        '202.8194.15': '2020.2.4',
        '202.8194.22': '2020.2.5',
        '203.5981.165': '2020.3',
        '203.6682.86': '2020.3.1',
        '203.6682.179': '2020.3.2',
        '203.7148.72': '2020.3.3',
        '203.7717.65': '2020.3.4',
        '203.7717.81': '2020.3.5',
        '211.6693.115': '2021.1',
        '211.7142.13': '2021.1.1',
        '211.7442.45': '2021.1.2',
        '211.7628.24': '2021.1.3',
        '212.4746.96': '2021.2',
        '212.5080.64': '2021.2.1',
        '212.5284.44': '2021.2.2',
        '212.5457.59': '2021.2.3',
    }

    def __init__(
            self,
            product_code: typing.Union[str, typing.Iterable[str]],
            product_info_path: str = 'product-info.json',
            build_path: str = 'build.txt',
    ) -> None:
        """Initialize new :class:`~CheckVersion` instance."""
        if isinstance(product_code, str):
            product_code = (product_code,)
        else:
            product_code = tuple(product_code)

        self.__product_code: 'typing_extensions.Final[typing.Tuple[str, ...]]' = product_code
        self.__product_info_path: 'typing_extensions.Final[str]' = product_info_path
        self.__build_path: 'typing_extensions.Final[str]' = build_path

    def __parse_product_info(self, root: str) -> typing.Optional[str]:
        """Parse `product-info.json` file for application version."""
        with contextlib.suppress(BaseException):
            stream: typing.TextIO
            data: typing.Mapping[str, typing.Any]
            with open(os.path.join(root, self.__product_info_path), 'rt', encoding='utf-8') as stream:
                data = json.load(stream)

            if data['productCode'] in self.__product_code:
                return data['version']

        return None

    def __parse_build(self, root: str) -> typing.Optional[str]:
        """Parse `build.txt` file for application version."""
        with contextlib.suppress(BaseException):
            stream: typing.TextIO
            match: typing.Optional[typing.Match[str]]
            with open(os.path.join(root, self.__build_path), 'rt', encoding='utf-8') as stream:
                match = self.build_pattern.fullmatch(stream.read().strip())

            if match and (match.group('product') in self.__product_code):
                build: str = match.group('build')
                return self.known_builds.get(build, f'build {build}')

        return None

    def __call__(
            self,
            parent: typing.Iterator[detectors.DetectedApplication],
    ) -> typing.Iterator[detectors.DetectedApplication]:
        """Iterate through detected applications."""
        application: detectors.DetectedApplication
        for application in parent:
            if not application.root:
                continue

            version: typing.Optional[str] = self.__parse_product_info(application.root)
            if version is None:
                version = self.__parse_build(application.root)

            if version is not None:
                yield application.replace(version=version)


class BasePyCharmApp(base.BaseInstallableApp):
    """Common parts for PyCharm applications."""

    def __init__(  # pylint: disable=too-many-arguments
            self,
            app_name: str,
            display_name: str,
            description: str,
            image_path: str,
            detector: 'detectors.Source',
            process_api: 'process.WorkerManager',
            config: 'user_config.UserConfig',
    ) -> None:
        """Initialize new :class:`~BasePyCharmApp` instance."""
        super().__init__(
            app_name=app_name,
            display_name=display_name,
            description=description,
            image_path=image_path,
            detector=detector,
            is_available=navigator_config.BITS_64,
            process_api=process_api,
            config=config,
            extra_arguments=tuple(),
        )

    def install_extensions(self) -> 'process.ProcessWorker':
        """Install app extensions."""
        return self._process_api.create_process_worker(['python', '--version'])

    def update_config(self, prefix: str) -> None:
        """Update user config to use selected Python prefix interpreter."""


class PyCharmProApp(BasePyCharmApp):
    """PyCharm Professional application."""

    def __init__(self, process_api: 'process.WorkerManager', config: 'user_config.UserConfig') -> None:
        """Initialize new :class:`~PyCharmProApp` instance."""
        detector: 'typing_extensions.Final[detectors.Source]' = detectors.Group(
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.mac_only(),

                detectors.check_known_mac_roots('PyCharm.app'),
                detectors.Group(
                    detectors.CheckKnownRoots(detectors.join(MAC_APPS, 'PyCharm-P')),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                    detectors.StepIntoRoot(equals='PyCharm.app'),
                ),

                detectors.AppendExecutable(
                    os.path.join('Contents', 'MacOS', 'pycharm'),
                ),

                CheckVersion(
                    product_code='PY',
                    product_info_path=os.path.join('Contents', 'Resources', 'product-info.json'),
                    build_path=os.path.join('Contents', 'Resources', 'build.txt'),
                ),
            ),
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.linux_only(),

                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(detectors.Linux.root, 'opt'),
                    ),
                    detectors.StepIntoRoot(starts_with='pycharm', reverse=True),
                ),
                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(LINUX_APPS, 'PyCharm-P'),
                    ),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                ),
                detectors.CheckKnownRoots(
                    detectors.join(detectors.Linux.root, 'snap', 'pycharm-professional', 'current'),
                    detectors.join(
                        detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-professional', 'current'
                    ),
                ),

                detectors.AppendExecutable(os.path.join('bin', 'pycharm.sh')),

                CheckVersion(
                    product_code='PY',
                ),
            ),
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.win_only(),

                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(detectors.Win.program_files_x64, 'JetBrains'),
                        detectors.join(detectors.Win.program_files_x86, 'JetBrains'),
                    ),
                    detectors.StepIntoRoot(starts_with='PyCharm', reverse=True),
                ),
                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(WIN_APPS, 'PyCharm-P'),
                    ),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                ),

                detectors.AppendExecutable(
                    os.path.join('bin', 'pycharm64.exe' if navigator_config.BITS_64 else 'pycharm32.exe'),
                    os.path.join('bin', 'pycharm32.exe'),
                    os.path.join('bin', 'pycharm.exe'),
                ),

                CheckVersion(
                    product_code='PY',
                ),
            ),
        )

        super().__init__(
            app_name='pycharm_pro',
            display_name='PyCharm Professional',
            description=(
                'A full-fledged IDE by JetBrains for both Scientific and Web Python development. Supports HTML, JS, '
                'and SQL.'
            ),
            image_path=images.PYCHARM_ICON_1024_PATH,
            detector=detector,
            process_api=process_api,
            config=config,
        )

    @staticmethod
    def install() -> None:
        """Install application."""
        webbrowser.open_new_tab('https://www.anaconda.com/pycharm_navigator')


class PyCharmCEApp(BasePyCharmApp):
    """PyCharm Community Edition application."""

    def __init__(self, process_api: 'process.WorkerManager', config: 'user_config.UserConfig') -> None:
        """Initialize new :class:`~PyCharmCEApp` instance."""
        detector: 'typing_extensions.Final[detectors.Source]' = detectors.Group(
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.mac_only(),

                detectors.check_known_mac_roots('PyCharm CE.app', 'PyCharm Edu.app'),
                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(MAC_APPS, 'PyCharm-C'),
                        detectors.join(MAC_APPS, 'PyCharm-E'),
                    ),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                    detectors.StepIntoRoot(equals=('PyCharm CE.app', 'PyCharm Edu.app')),
                ),

                detectors.AppendExecutable(
                    os.path.join('Contents', 'MacOS', 'pycharm'),
                ),

                CheckVersion(
                    product_code=('PC', 'PE'),
                    product_info_path=os.path.join('Contents', 'Resources', 'product-info.json'),
                    build_path=os.path.join('Contents', 'Resources', 'build.txt'),
                ),
            ),
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.linux_only(),

                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(detectors.Linux.root, 'opt'),
                    ),
                    detectors.StepIntoRoot(starts_with='pycharm', reverse=True),
                ),
                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(LINUX_APPS, 'PyCharm-C'),
                        detectors.join(LINUX_APPS, 'PyCharm-E'),
                    ),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                ),
                detectors.CheckKnownRoots(
                    detectors.join(detectors.Linux.root, 'snap', 'pycharm-community', 'current'),
                    detectors.join(
                        detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-community', 'current'
                    ),
                    detectors.join(detectors.Linux.root, 'snap', 'pycharm-educational', 'current'),
                    detectors.join(
                        detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-educational', 'current'
                    ),
                ),

                detectors.AppendExecutable(os.path.join('bin', 'pycharm.sh')),

                CheckVersion(
                    product_code=('PC', 'PE'),
                ),
            ),
            detectors.Group(
                detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config),
                detectors.win_only(),

                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(detectors.Win.program_files_x64, 'JetBrains'),
                        detectors.join(detectors.Win.program_files_x86, 'JetBrains'),
                    ),
                    detectors.StepIntoRoot(starts_with='PyCharm', reverse=True),
                ),
                detectors.Group(
                    detectors.CheckKnownRoots(
                        detectors.join(WIN_APPS, 'PyCharm-C'),
                        detectors.join(WIN_APPS, 'PyCharm-E'),
                    ),
                    detectors.StepIntoRoot(starts_with='ch-'),
                    detectors.StepIntoRoot(reverse=True),
                ),

                detectors.AppendExecutable(
                    os.path.join('bin', 'pycharm64.exe' if navigator_config.BITS_64 else 'pycharm32.exe'),
                    os.path.join('bin', 'pycharm32.exe'),
                    os.path.join('bin', 'pycharm.exe'),
                ),

                CheckVersion(
                    product_code=('PC', 'PE'),
                ),
            ),
        )

        super().__init__(
            app_name='pycharm_ce',
            display_name='PyCharm Community',
            description=(
                'An IDE by JetBrains for pure Python development. Supports code completion, listing, and debugging.'
            ),
            image_path=images.PYCHARM_CE_ICON_1024_PATH,
            detector=detector,
            process_api=process_api,
            config=config,
        )
