"""Migrating IPython < 4.0 to Jupyter

This *copies* configuration and resources to their new locations in Jupyter

Migrations:

- .ipython/
  - nbextensions -> JUPYTER_DATA_DIR/nbextensions
  - kernels ->  JUPYTER_DATA_DIR/kernels
- .ipython/profile_default/
  - static/custom -> .jupyter/custom
  - nbconfig -> .jupyter/nbconfig
  - security/
    - notebook_secret, notebook_cookie_secret, nbsignatures.db -> JUPYTER_DATA_DIR
  - ipython_{notebook,nbconvert,qtconsole}_config.py -> .jupyter/jupyter_{name}_config.py


"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

import os
import re
import shutil
from datetime import datetime

from traitlets.config import PyFileConfigLoader, JSONFileConfigLoader
from traitlets.log import get_logger

from .utils import ensure_dir_exists

from .paths import jupyter_config_dir, jupyter_data_dir
from .application import JupyterApp

pjoin = os.path.join

migrations = {
    pjoin('{ipython_dir}', 'nbextensions'): pjoin('{jupyter_data}', 'nbextensions'),
    pjoin('{ipython_dir}', 'kernels'): pjoin('{jupyter_data}', 'kernels'),
    pjoin('{profile}', 'nbconfig'): pjoin('{jupyter_config}', 'nbconfig'),
}

custom_src_t = pjoin('{profile}', 'static', 'custom')
custom_dst_t = pjoin('{jupyter_config}', 'custom')

for security_file in ('notebook_secret', 'notebook_cookie_secret', 'nbsignatures.db'):
    src = pjoin('{profile}', 'security', security_file)
    dst = pjoin('{jupyter_data}', security_file)
    migrations[src] = dst

config_migrations = ['notebook', 'nbconvert', 'qtconsole']

regex = re.compile

config_substitutions = {
    regex(r'\bIPythonQtConsoleApp\b'): 'JupyterQtConsoleApp',
    regex(r'\bIPythonWidget\b'): 'JupyterWidget',
    regex(r'\bRichIPythonWidget\b'): 'RichJupyterWidget',
    regex(r'\bIPython\.html\b'): 'notebook',
    regex(r'\bIPython\.nbconvert\b'): 'nbconvert',
}


def get_ipython_dir():
    """Return the IPython directory location.

    Not imported from IPython because the IPython implementation
    ensures that a writable directory exists,
    creating a temporary directory if not.
    We don't want to trigger that when checking if migration should happen.

    We only need to support the IPython < 4 behavior for migration,
    so importing for forward-compatibility and edge cases is not important.
    """
    return os.environ.get('IPYTHONDIR', os.path.expanduser('~/.ipython'))


def migrate_dir(src, dst):
    """Migrate a directory from src to dst"""
    log = get_logger()
    if not os.listdir(src):
        log.debug("No files in %s" % src)
        return False
    if os.path.exists(dst):
        if os.listdir(dst):
            # already exists, non-empty
            log.debug("%s already exists" % dst)
            return False
        else:
            os.rmdir(dst)
    log.info("Copying %s -> %s" % (src, dst))
    ensure_dir_exists(os.path.dirname(dst))
    shutil.copytree(src, dst, symlinks=True)
    return True


def migrate_file(src, dst, substitutions=None):
    """Migrate a single file from src to dst
    
    substitutions is an optional dict of {regex: replacement} for performing replacements on the file.
    """
    log = get_logger()
    if os.path.exists(dst):
        # already exists
        log.debug("%s already exists" % dst)
        return False
    log.info("Copying %s -> %s" % (src, dst))
    ensure_dir_exists(os.path.dirname(dst))
    shutil.copy(src, dst)
    if substitutions:
        with open(dst) as f:
            text = f.read()
        for pat, replacement in substitutions.items():
            text = pat.sub(replacement, text)
        with open(dst, 'w') as f:
            f.write(text)
    return True


def migrate_one(src, dst):
    """Migrate one item
    
    dispatches to migrate_dir/_file
    """
    log = get_logger()
    if os.path.isfile(src):
        return migrate_file(src, dst)
    elif os.path.isdir(src):
        return migrate_dir(src, dst)
    else:
        log.debug("Nothing to migrate for %s" % src)
        return False


def migrate_static_custom(src, dst):
    """Migrate non-empty custom.js,css from src to dst
    
    src, dst are 'custom' directories containing custom.{js,css}
    """
    log = get_logger()
    migrated = False
    
    custom_js = pjoin(src, 'custom.js')
    custom_css = pjoin(src, 'custom.css')
    # check if custom_js is empty:
    custom_js_empty = True
    if os.path.isfile(custom_js):
        with open(custom_js) as f:
            js = f.read().strip()
            for line in js.splitlines():
                if not (
                    line.isspace()
                    or line.strip().startswith(('/*', '*', '//'))
                ):
                    custom_js_empty = False
                    break
    
    # check if custom_css is empty:
    custom_css_empty = True
    if os.path.isfile(custom_css):
        with open(custom_css) as f:
            css = f.read().strip()
            custom_css_empty = css.startswith('/*') and css.endswith('*/')
    
    if custom_js_empty:
        log.debug("Ignoring empty %s" % custom_js)
    if custom_css_empty:
        log.debug("Ignoring empty %s" % custom_css)
    
    if custom_js_empty and custom_css_empty:
        # nothing to migrate
        return False
    ensure_dir_exists(dst)
    
    if not custom_js_empty or not custom_css_empty:
        ensure_dir_exists(dst)
    
    if not custom_js_empty:
        if migrate_file(custom_js, pjoin(dst, 'custom.js')):
            migrated = True
    if not custom_css_empty:
        if migrate_file(custom_css, pjoin(dst, 'custom.css')):
            migrated = True
    
    return migrated


def migrate_config(name, env):
    """Migrate a config file
    
    Includes substitutions for updated configurable names.
    """
    log = get_logger()
    src_base = pjoin('{profile}', 'ipython_{name}_config').format(name=name, **env)
    dst_base = pjoin('{jupyter_config}', 'jupyter_{name}_config').format(name=name, **env)
    loaders = {
        '.py': PyFileConfigLoader,
        '.json': JSONFileConfigLoader,
    }
    migrated = []
    for ext in ('.py', '.json'):
        src = src_base + ext
        dst = dst_base + ext
        if os.path.exists(src):
            cfg = loaders[ext](src).load_config()
            if cfg:
                if migrate_file(src, dst, substitutions=config_substitutions):
                    migrated.append(src)
            else:
                # don't migrate empty config files
                log.debug("Not migrating empty config file: %s" % src)
    return migrated


def migrate():
    """Migrate IPython configuration to Jupyter"""
    env = {
        'jupyter_data': jupyter_data_dir(),
        'jupyter_config': jupyter_config_dir(),
        'ipython_dir': get_ipython_dir(),
        'profile': os.path.join(get_ipython_dir(), 'profile_default'),
    }
    migrated = False
    for src_t, dst_t in migrations.items():
        src = src_t.format(**env)
        dst = dst_t.format(**env)
        if os.path.exists(src):
            if migrate_one(src, dst):
                migrated = True
    
    for name in config_migrations:
        if migrate_config(name, env):
            migrated = True
    
    custom_src = custom_src_t.format(**env)
    custom_dst = custom_dst_t.format(**env)
    
    if os.path.exists(custom_src):
        if migrate_static_custom(custom_src, custom_dst):
            migrated = True
    
    # write a marker to avoid re-running migration checks
    ensure_dir_exists(env['jupyter_config'])
    with open(os.path.join(env['jupyter_config'], 'migrated'), 'w') as f:
        f.write(datetime.utcnow().isoformat())
    
    return migrated



class JupyterMigrate(JupyterApp):
    name = 'jupyter-migrate'
    description = """
    Migrate configuration and data from .ipython prior to 4.0 to Jupyter locations.
    
    This migrates:
    
    - config files in the default profile
    - kernels in ~/.ipython/kernels
    - notebook javascript extensions in ~/.ipython/extensions
    - custom.js/css to .jupyter/custom
    
    to their new Jupyter locations.
    
    All files are copied, not moved.
    If the destinations already exist, nothing will be done.
    """
    
    def start(self):
        if not migrate():
            self.log.info("Found nothing to migrate.")


main = JupyterMigrate.launch_instance


if __name__ == '__main__':
    main()

