# Copyright (c) 2017 Alain Pernelle
# This file is part of MyApp.
#
# MyApp is free software: you can redistribute it and/or modify
# it under the terms of the GNU General 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>. 

import collections
import datetime
import errno
import glob
import logging
import math
import os.path
import re
import shlex
import subprocess
import urlparse

from tornado.ioloop import IOLoop

import diskctl
import myappctl
#import powerctl
import settings
import tasks
#import uploadservices
import utils
#import v4l2ctl


_MAIN_CONFIG_FILE_NAME = 'app.conf'
_ACTIONS = ['lock', 'unlock', 'light_on', 'light_off', 'alarm_on', 'alarm_off', 'up', 'right', 'down', 'left', 'zoom_in', 'zoom_out',
        'preset1', 'preset2', 'preset3', 'preset4', 'preset5', 'preset6', 'preset7', 'preset8', 'preset9']

_main_config_cache = None
_additional_section_funcs = []
_additional_config_funcs = []
_additional_structure_cache = {}
_monitor_command_cache = {}


def additional_section(func):
    _additional_section_funcs.append(func)


def additional_config(func):
    _additional_config_funcs.append(func)


#import wifictl  # @UnusedImport
import tzctl  # @UnusedImport


def get_main(as_lines=False):
    global _main_config_cache
    
    if not as_lines and _main_config_cache is not None:
        return _main_config_cache
    
    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
    
    logging.debug('reading main config from file %(path)s...' % {'path': config_file_path})
    
    lines = None
    try:
        file = open(config_file_path, 'r')
    
    except IOError as e:
        if e.errno == errno.ENOENT:  # file does not exist
            logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
            
            lines = []
        
        else:
            logging.error('could not open main config file %(path)s: %(msg)s' % {
                    'path': config_file_path, 'msg': unicode(e)})
            
            raise
    
    if lines is None:
        try:
            lines = [l[:-1] for l in file.readlines()]
        
        except Exception as e:
            logging.error('could not read main config file %(path)s: %(msg)s' % {
                    'path': config_file_path, 'msg': unicode(e)})
            
            raise
        
        finally:
            file.close()
    
    if as_lines:
        return lines
    
    main_config = _conf_to_dict(lines,
    no_convert=['@admin_username', '@admin_password', '@normal_username', '@normal_password'])


    _main_config_cache = main_config
    
    return main_config


def set_main(main_config):
    global _main_config_cache
    
    main_config = dict(main_config)
    for n, v in _main_config_cache.iteritems():
        main_config.setdefault(n, v)
    _main_config_cache = main_config
    
    main_config = dict(main_config)
##    _set_additional_config(main_config)

    config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
    
    # read the actual configuration from file
    lines = get_main(as_lines=True)
    
    # write the configuration to file
    logging.debug('writing main config to %(path)s...' % {'path': config_file_path})
    
    try:
        file = open(config_file_path, 'w')
    
    except Exception as e:
        logging.error('could not open main config file %(path)s for writing: %(msg)s' % {
                'path': config_file_path, 'msg': unicode(e)})
        
        raise
    
    lines = _dict_to_conf(lines, main_config, list_names=['thread'])
    
    try:
        file.writelines([utils.make_str(l) + '\n' for l in lines])
    
    except Exception as e:
        logging.error('could not write main config file %(path)s: %(msg)s' % {
                'path': config_file_path, 'msg': unicode(e)})
        
        raise
    
    finally:
        file.close()

def main_ui_to_dict(ui):
    data = {
        '@normal_username': ui['main']['normal_username'],
        '@normal_password': ui['main']['normal_password'],
        '@show_advanced': ui['main']['show_advanced'],
        '@admin_username': ui['main']['admin_username'],
        '@admin_password': ui['main']['admin_password']
    }
    return data


def main_dict_to_ui(data):
    ui = {
        'normal_username': data['@normal_username'],
        'normal_password': data['@normal_password'],
        'show_advanced': data['@show_advanced'],
        'admin_username': data['@admin_username'],
        'admin_password': data['@admin_password']
    }

    # additional configs

    return ui


def invalidate_monitor_commands():
    _monitor_command_cache.clear()



def backup():
    logging.debug('generating config backup file')

    if len(os.listdir(settings.CONF_PATH)) > 100:
        logging.debug('config path "%s" appears to be a system-wide config directory, performing a selective backup' % settings.CONF_PATH)
        cmd = ['tar', 'zc', 'app.conf']
        cmd += map(os.path.basename, glob.glob(os.path.join(settings.CONF_PATH, 'thread-*.conf')))
        try:
            content = subprocess.check_output(cmd, cwd=settings.CONF_PATH)
            logging.debug('backup file created (%s bytes)' % len(content))
            
            return content
            
        except Exception as e:
            logging.error('backup failed: %s' % e, exc_info=True)
            
            return None

    else:
        logging.debug('config path "%s" appears to be a motion-specific config directory, performing a full backup' % settings.CONF_PATH)

        try:
            content = subprocess.check_output(['tar', 'zc', '.'], cwd=settings.CONF_PATH)
            logging.debug('backup file created (%s bytes)' % len(content))
            
            return content
            
        except Exception as e:
            logging.error('backup failed: %s' % e, exc_info=True)
            
            return None


def restore(content):
    global _main_config_cache
    global _additional_structure_cache
    
    logging.info('restoring config from backup file')

    cmd = ['tar', 'zxC', settings.CONF_PATH]

    try:
        p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        msg = p.communicate(content)[0]
        if msg:
            logging.error('failed to restore configuration: %s' % msg)
            return False

        logging.debug('configuration restored successfully')

        if settings.ENABLE_REBOOT:
            def later():
                powerctl.reboot()

            io_loop = IOLoop.instance()
            io_loop.add_timeout(datetime.timedelta(seconds=2), later)

        else:
            invalidate()

        return {'reboot': settings.ENABLE_REBOOT}

    except Exception as e:
        logging.error('failed to restore configuration: %s' % e, exc_info=True)

        return None


def invalidate():
    global _main_config_cache
    global _additional_structure_cache

    logging.debug('invalidating config cache')    
    _main_config_cache = None
    _additional_structure_cache = {}

def _value_to_python(value):
    value_lower = value.lower()
    if value_lower == 'off':
        return False
    
    elif value_lower == 'on':
        return True
    
    try:
        return int(value)
    
    except ValueError:
        try:
            return float(value)
        
        except ValueError:
            return value


def _python_to_value(value):
    if value is True:
        return 'on'
    
    elif value is False:
        return 'off'
    
    elif isinstance(value, (int, float)):
        return str(value)
    
    else:
        return value


def _conf_to_dict(lines, list_names=[], no_convert=[]):
    data = utils.OrderedDict()
    
    for line in lines:
        line = line.strip()
        if len(line) == 0:  # empty line
            continue
        
        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
        if match:
            name, value = match.groups()[:2]
        
        elif line.startswith('#') or line.startswith(';'): # comment line
            continue

        else:
            parts = line.split(None, 1)
            if len(parts) == 1: # empty value
                parts.append('')

            (name, value) = parts
            
            value = value.strip()

        if name not in no_convert:
            value = _value_to_python(value)
        
        if name in list_names:
            data.setdefault(name, []).append(value)
        
        else:
            data[name] = value

    return data


def _dict_to_conf(lines, data, list_names=[]):
    conf_lines = []
    remaining = utils.OrderedDict(data)
    processed = set()
    
    # parse existing lines and replace the values
    
    for line in lines:
        line = line.strip()
        if len(line) == 0:  # empty line
            conf_lines.append(line)
            continue

        match = re.match('^\#\s*(\@\w+)\s*(.*)', line)
        if match: # @line
            (name, value) = match.groups()[:2]
        
        elif line.startswith('#') or line.startswith(';'):  # simple comment line
            conf_lines.append(line)
            continue
        
        else:
            parts = line.split(None, 1)
            if len(parts) == 2:
                (name, value) = parts
            
            else:
                (name, value) = parts[0], ''
            
        if name in processed:
            continue # name already processed
        
        processed.add(name)
        
        if name in list_names:
            new_value = data.get(name)
            if new_value is not None:
                for v in new_value:
                    if v is None:
                        continue

                    line = name + ' ' + _python_to_value(v)
                    conf_lines.append(line)
            
            else:
                line = name + ' ' + value
                conf_lines.append(line)

        else:
            new_value = data.get(name)
            if new_value is not None:
                value = _python_to_value(new_value)
                line = name + ' ' + value
                conf_lines.append(line)

        remaining.pop(name, None)
    
    # add the remaining config values not covered by existing lines
    
    if len(remaining) and len(lines):
        conf_lines.append('') # add a blank line
    
    for (name, value) in remaining.iteritems():
        
        if name in list_names:
            for v in value:
                if v is None:
                    continue

                line = name + ' ' + _python_to_value(v)
                conf_lines.append(line)

        else:
            line = name + ' ' + _python_to_value(value)
            conf_lines.append(line)
            
    # build the final config lines
    conf_lines.sort(key=lambda l: not l.startswith('@'))
    
    lines = []
    for i, line in enumerate(conf_lines):
        # squeeze successive blank lines
        if i > 0 and len(line.strip()) == 0 and len(conf_lines[i - 1].strip()) == 0:
            continue
        
        if line.startswith('@'):
            line = '# ' + line
        
        elif i > 0 and conf_lines[i - 1].startswith('@'):
            lines.append('') # add a blank line between @lines and the rest
        
        lines.append(line)
        
    return lines

    
def get_additional_structure(camera, separators=False):
    if _additional_structure_cache.get((camera, separators)) is None:
        logging.debug('loading additional config structure for %s, %s separators' % (
                'camera' if camera else 'main',
                'with' if separators else 'without'))

        # gather sections
        sections = utils.OrderedDict()
        for func in _additional_section_funcs:
            result = func()
            if not result:
                continue
            
            if result.get('reboot') and not settings.ENABLE_REBOOT:
                continue
            
            if bool(result.get('camera')) != bool(camera):
                continue
            
            result['name'] = func.func_name
            sections[func.func_name] = result
            
            logging.debug('additional config section: %s' % result['name'])
    
        configs = utils.OrderedDict()
        for func in _additional_config_funcs:
            result = func()
            if not result:
                continue
            
            if result.get('reboot') and not settings.ENABLE_REBOOT:
                continue
            
            if bool(result.get('camera')) != bool(camera):
                continue

            if result['type'] == 'separator' and not separators:
                continue

            result['name'] = func.func_name
            configs[func.func_name] = result
    
            section = sections.setdefault(result.get('section'), {})
            section.setdefault('configs', []).append(result)
            
            logging.debug('additional config item: %s' % result['name'])

        _additional_structure_cache[(camera, separators)] = sections, configs

    return _additional_structure_cache[(camera, separators)]
