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

"""
The configuration files are located on ``$HOME/.turses`` directory.

There is one mayor configuration file in turses:

    ``config``
        contains user preferences: colors, bindings, etc.

An one default token file:

    ``token``
        contains authentication token for the default user account

Each user account that is no the default one needs to be aliased and  has its
own token file ``alias.token``.

To create an aliased account:

.. code-block:: sh

    $ turses -a work

And, after authorizing ``turses`` to use that account,  a token file named
``work.token`` will be created. Optionally you can create a ``work.config``
file for a configuration specific to that account.

Now, when you execute again:

.. code-block:: sh

    $ turses -a work

you will be logged in with the previously stored credentials.

Here is an example with two accounts apart from the default one, aliased
to ``alice`` and ``bob``.

.. code-block:: sh

    ~
    |+.turses/
    | |-config
    | |-alice.config
    | |-token
    | |-alice.token
    | `-bob.token
    |+...
    |-...
    `


If you want to generate a configuration file, you can do so executing:

.. code-block:: sh

    $ turses -g /path/to/file
"""

from sys import exit
from ConfigParser import RawConfigParser
from os import getenv, path, mkdir, remove
from functools import partial
from gettext import gettext as _

from turses.utils import encode
from turses.meta import wrap_exceptions
from turses.api.base import get_authorization_tokens

# -- Defaults -----------------------------------------------------------------

# Key bindings

KEY_BINDINGS = {
    # motion
    'up':
         ('k', _('scroll up')),
    'down':
         ('j', _('scroll down')),
    'left':
        ('h', _('activate the timeline on the left')),
    'right':
        ('l', _('activate the timeline on the right')),
    'scroll_to_top':
        ('g', _('scroll to top')),
    'scroll_to_bottom':
        ('G', _('scroll to bottom')),

    # buffers
    'activate_first_buffer':
       ('a', _('activate first buffer')),
    'activate_last_buffer':
        ('e', _('activate last buffer')),
    'shift_buffer_beggining':
        ('ctrl a', _('shift active buffer to the beginning')),
    'shift_buffer_end':
        ('ctrl e', _('shift active buffer to the end')),
    'shift_buffer_left':
        ('<', _('shift active buffer one position to the left')),
    'shift_buffer_right':
        ('>', _('shift active buffer one position to the right')),
    'expand_visible_left':
        ('p', _('expand visible timelines one column to the left')),
    'expand_visible_right':
        ('n', _('expand visible timelines one column to the right')),
    'shrink_visible_left':
        ('P', _('shrink visible timelines one column from the left')),
    'shrink_visible_right':
        ('N', _('shrink visible timelines one column from the left')),
    'delete_buffer':
        ('d', _('delete buffer')),
    'mark_all_as_read':
        ('A', _('mark all tweets in the current timeline as read')),

    # tweets
    'tweet':
        ('t', _('compose a tweet')),
    'delete_tweet':
        ('X', _('delete focused status')),
    'reply':
        ('r', _('reply to focused status')),
    'retweet':
        ('R', _('retweet focused status')),
    'retweet_and_edit':
        ('E', _('open a editor for manually retweeting the focused status')),
    'retweet_and_fav':
        ('Y', _('mark focused tweet as favorite and retweet it')),
    'send_dm':
        ('D', _('compose a direct message')),
    'update':
        ('u', _('refresh the active timeline')),
    'update_all':
        ('S', _('refresh all the timelines')),
    'tweet_hashtag':
        ('H', _('compose a tweet with the same hashtags as the focused status')),
    'fav':
        ('b', _('mark focused tweet as favorite')),
    'delete_fav':
        ('ctrl b', _('remove tweet from favorites')),
    'follow_selected':
        ('f', _('follow selected status\' author')),
    'follow_user':
        ('F', _('follow user given in an editor')),
    'unfollow_selected':
        ('U', _('unfollow selected status\' author')),
    'unfollow_user':
        ('ctrl u', _('unfollow user given in an editor')),

    # timelines
    'home':
        ('.', _('open a home timeline')),
    'own_tweets':
        ('_', _('open a timeline with your tweets')),
    'favorites':
        ('B', _('open a timeline with your favorites')),
    'mentions':
        ('m', _('open a mentions timeline')),
    'DMs':
        ('M', _('open a direct message timeline')),
    'search':
        ('/', _('search for term and show resulting timeline')),
    'search_user':
        ('@', _('open a timeline with the tweets of the specified user')),
    'user_timeline':
        ('+', _('open a timeline with the tweets of the focused status\' author')),
    'thread':
        ('T', _('open the thread of the focused status')),
    'hashtags':
        ('L', _('open a search timeline with the hashtags of the focused status')),
    'retweets_of_me':
        ('I', _('open a timeline with your tweets that have been retweeted')),

    # info
    'user_info':
        ('i', _('show user\'s info')),

    # meta
    'help':
        ('?', _('show program help')),
    'reload_config':
        ('C', _('reload configuration')),

    # turses
    'quit':
        ('q', _('exit program')),
    'clear':
        ('c', _('clear status bar')),
    'openurl':
        ('o', _('open URLs of the focused status in a browser')),
    'open_status_url':
        ('O', _('open the focused status in a browser')),
    'redraw':
        ('ctrl l', _('redraw the screen')),
}

# NOTE:
# The key binding categories are declared to order them in the configuration
# and in the help buffer. If you add a key binding, don't forget to include
# it in one of these categories.

MOTION_KEY_BINDINGS = [
    'up',
    'down',
    'left',
    'right',
    'scroll_to_top',
    'scroll_to_bottom',
]

BUFFERS_KEY_BINDINGS = [
    'activate_first_buffer',
    'activate_last_buffer',
    'shift_buffer_beggining',
    'shift_buffer_end',
    'shift_buffer_left',
    'shift_buffer_right',
    'expand_visible_left',
    'expand_visible_right',
    'shrink_visible_left',
    'shrink_visible_right',
    'delete_buffer',
    'mark_all_as_read',
]

TWEETS_KEY_BINDINGS = [
    'tweet',
    'delete_tweet',
    'reply',
    'retweet',
    'retweet_and_edit',
    'retweet_and_fav',
    'send_dm',
    'update',
    'update_all',
    'tweet_hashtag',
    'fav',
    'delete_fav',
    'follow_selected',
    'follow_user',
    'unfollow_selected',
    'unfollow_user',
    'user_info',
]

TIMELINES_KEY_BINDINGS = [
    'home',
    'own_tweets',
    'favorites',
    'mentions',
    'DMs',
    'search',
    'search_user',
    'user_timeline',
    'thread',
    'hashtags',
    'retweets_of_me',
]

META_KEY_BINDINGS = [
    'help',
    'reload_config',
]

TURSES_KEY_BINDINGS = [
    'clear',
    'quit',
    'openurl',
    'open_status_url',
    'redraw',
]

# Palette

# TODO: not hard coded
# valid colors for `urwid`s palette
VALID_COLORS = [
    'default',
    'black',
    'dark red',
    'dark green',
    'brown',
    'dark blue',
    'dark magenta',
    'dark cyan',
    'light gray',
    'dark gray',
    'light red',
    'light green',
    'yellow',
    'light blue',
    'light magenta',
    'light cyan',
    'white',
]


def validate_color(colorstring):
    return colorstring if colorstring in VALID_COLORS else ''

PALETTE = [
    #Tabs
    ['active_tab',  'white', 'dark blue'],
    ['visible_tab', 'yellow', 'dark blue'],
    ['inactive_tab', 'dark blue', ''],

    # Statuses
    ['header', 'light blue', ''],
    ['body', 'white', ''],
    ['focus', 'light red', ''],
    ['line', 'black', ''],
    ['unread', 'dark red', ''],
    ['read', 'dark blue', ''],
    ['favorited', 'yellow', ''],

    # Text
    ['highlight', 'dark red', ''],
    ['highlight_nick', 'light red', ''],
    ['attag', 'yellow', ''],
    ['hashtag', 'light red', ''],
    ['url', 'white', 'dark red'],

    # Messages
    ['error', 'white', 'dark red'],
    ['info', 'white', 'dark blue'],

    # Editor
    ['editor', 'white', 'dark blue'],
]

# Styles

STYLES = {
    # TODO: make time string configurable
    'reply_indicator': '@',
    'retweet_indicator': 'RT',
    'header_template': ' {username}{retweeted}{retweeter} - {time}{reply}{retweet_count} ',
    'dm_template': ' {sender_screen_name} => {recipient_screen_name} - {time} ',
    'tab_template': '{timeline_name} [{unread}]',
    'box_around_status': True,
    'status_divider': False,
    'status_bar': True,
    'status_divider_char': '─',
    'editor_horizontal_align': 'center',
    'editor_vertical_align': 'bottom',
    'url_format': 'display',
    'statuses_in_user_info': 3,
}

# Debug

LOGGING_LEVEL = 3

# Twitter
UPDATE_FREQUENCY = 300
USE_HTTPS = True

TWITTER = {
  'update_frequency': UPDATE_FREQUENCY,
  'use_https': USE_HTTPS,
}

# Environment

HOME = getenv('HOME')

# -- Configuration ------------------------------------------------------------

DEFAULT_SESSION = 'defaults'

# Default config path
CONFIG_DIR = '.turses'
CONFIG_PATH = path.join(HOME, CONFIG_DIR)
DEFAULT_CONFIG_FILE = path.join(CONFIG_PATH, 'config')
DEFAULT_TOKEN_FILE = path.join(CONFIG_PATH, 'token')
LOG_FILE = path.join(CONFIG_PATH, 'log')

LEGACY_CONFIG_DIR = '.config/turses'
LEGACY_CONFIG_PATH = path.join(HOME, LEGACY_CONFIG_DIR)
LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_PATH, 'turses.cfg')
LEGACY_TOKEN_FILE = path.join(LEGACY_CONFIG_PATH, 'turses.tok')

# Names of the sections in the configuration
SECTION_DEFAULT_TIMELINES = 'timelines'
SECTION_KEY_BINDINGS = 'bindings'
SECTION_PALETTE = 'colors'
SECTION_STYLES = 'styles'
SECTION_DEBUG = 'debug'
SECTION_TWITTER = 'twitter'

# Names of the sections in the token file
SECTION_TOKEN = 'token'


def print_deprecation_notice():
    print "NOTE:"
    print
    print "The configuration file in %s has been deprecated." % LEGACY_CONFIG_FILE
    print "A new configuration directory is being generated in %s." % CONFIG_PATH
    print


def invert_command_map(bindings):
    """
    Invert configuration keybindings to make reverse lookups faster
    """
    command_map = {}
    for command, (key, _) in bindings.iteritems():
        command_map[key] = command
    return command_map


class Configuration(object):
    """
    Generate and parse configuration files. When instantiated, it loads the
    defaults.

    Calling :func:`Configuration.parse_args` with an
    :class:`argparse.ArgumentParser` instance will modify the instance to match
    the options provided by the command line arguments.

    Calling :func:`turses.config.Configuration.load` on this class' instances
    reads the preferences from the user configuration files. If no
    configuration or token files are found, this class will take care of
    creating them.

    Offers backwards compatibility with the Tyrs configuration.
    """

    def __init__(self):
        """
        Create a `Configuration` taking into account the arguments
        from the command line interface (if any).
        """
        # load defaults
        self.twitter = TWITTER
        self.key_bindings = KEY_BINDINGS
        self.key_mappings = invert_command_map(self.key_bindings)
        self.palette = PALETTE
        self.styles = STYLES
        self.logging_level = LOGGING_LEVEL
        self.session = DEFAULT_SESSION

        # config and token files
        self.config_file = DEFAULT_CONFIG_FILE
        self.token_file = DEFAULT_TOKEN_FILE

        # debug mode
        self.debug = False

        # create the config directory if it does not exist
        if not path.isdir(CONFIG_PATH):
            try:
                mkdir(CONFIG_PATH)
            except:
                print encode(_('Error creating config directory in %s' % CONFIG_DIR))
                self.exit_with_code(3)

    def parse_args(self, cli_args):
        """Interprets the arguments provided by `cli_args`."""
        if cli_args is None:
            return

        if cli_args.generate_config:
            self.generate_config_file(config_file=cli_args.generate_config,)
            self.exit_with_code(0)

        # path to configuration file
        if cli_args.config:
            self.config_file = cli_args.config
        elif cli_args.account:
            self.config_file = path.join(CONFIG_PATH, '%s.config' % cli_args.account)

        # path to token file
        if cli_args.account:
            self.token_file = path.join(CONFIG_PATH, '%s.token' % cli_args.account)

        # session
        if cli_args.session:
            self.session = cli_args.session

        # debug mode
        self.debug = getattr(cli_args, 'debug', False)

    def load(self):
        """
        Loads configuration from files.
        """
        self._init_config()
        self._init_token()

    def _init_config(self):
        if path.isfile(LEGACY_CONFIG_FILE):
            self._parse_legacy_config_file()
            print_deprecation_notice()
            remove(LEGACY_CONFIG_FILE)
        elif path.isfile(self.config_file):
            self.parse_config_file(self.config_file)
        else:
            self.generate_config_file(self.config_file)
        self.key_mappings = invert_command_map(self.key_bindings)

    def _add_section_twitter(self, conf):
        # Twitter
        if not conf.has_section(SECTION_TWITTER):
            conf.add_section(SECTION_TWITTER)
        if not conf.has_option(SECTION_TWITTER, 'update_frequency'):
            conf.set(SECTION_TWITTER, 'update_frequency', UPDATE_FREQUENCY)
        if not conf.has_option(SECTION_TWITTER, 'use_https'):
            conf.set(SECTION_TWITTER, 'use_https', USE_HTTPS)

    def _add_section_key_bindings(self, conf):
        # Key bindings
        if not conf.has_section(SECTION_KEY_BINDINGS):
            conf.add_section(SECTION_KEY_BINDINGS)
        binding_lists = [MOTION_KEY_BINDINGS,
                         BUFFERS_KEY_BINDINGS,
                         TWEETS_KEY_BINDINGS,
                         TIMELINES_KEY_BINDINGS,
                         META_KEY_BINDINGS,
                         TURSES_KEY_BINDINGS, ]
        for binding_list in binding_lists:
            for binding in binding_list:
                key = self.key_bindings[binding][0]
                if conf.has_option(SECTION_KEY_BINDINGS, binding):
                    continue
                conf.set(SECTION_KEY_BINDINGS, binding, key)

    def _add_section_palette(self, conf):
        # Color
        if not conf.has_section(SECTION_PALETTE):
            conf.add_section(SECTION_PALETTE)
        for label in PALETTE:
            label_name, fg, bg = label[0], label[1], label[2]

            # fg
            if conf.has_option(SECTION_PALETTE, label_name) and \
                validate_color(conf.get(SECTION_PALETTE, label_name)):
                pass
            else:
                conf.set(SECTION_PALETTE, label_name, fg)

            #bg
            label_name_bg = label_name + '_bg'
            if conf.has_option(SECTION_PALETTE, label_name_bg) and \
                validate_color(conf.get(SECTION_PALETTE, label_name_bg)):
                pass
            else:
                conf.set(SECTION_PALETTE, label_name_bg, bg)

    def _add_section_styles(self, conf):
        # Styles
        if not conf.has_section(SECTION_STYLES):
            conf.add_section(SECTION_STYLES)
        for style in STYLES:
            if conf.has_option(SECTION_STYLES, style):
                continue
            conf.set(SECTION_STYLES, style, self.styles[style])

    def _add_section_debug(self, conf):
        # Debug
        if not conf.has_section(SECTION_DEBUG):
            conf.add_section(SECTION_DEBUG)
        if conf.has_option(SECTION_DEBUG, 'logging_level'):
            return
        conf.set(SECTION_DEBUG, 'logging_level', LOGGING_LEVEL)

    def _init_token(self):
        if path.isfile(LEGACY_TOKEN_FILE):
            self.parse_token_file(LEGACY_TOKEN_FILE)
            remove(LEGACY_TOKEN_FILE)
            if (hasattr(self, 'oauth_token') and
                hasattr(self, 'oauth_token_secret')):
                self.generate_token_file(self.token_file,
                                         self.oauth_token,
                                         self.oauth_token_secret)
        elif not path.isfile(self.token_file):
            self.authorize_new_account()
        else:
            self.parse_token_file(self.token_file)

    def _parse_legacy_config_file(self):
        """
        Parse a legacy configuration file.
        """
        conf = RawConfigParser()
        conf.read(LEGACY_CONFIG_FILE)

        styles = self.styles.copy()

        if conf.has_option('params', 'dm_template'):
            styles['dm_template'] = conf.get('params', 'dm_template')

        if conf.has_option('params', 'header_template'):
            styles['header_template'] = conf.get('params', 'header_template')

        self.styles.update(styles)

        if conf.has_option('params', 'logging_level'):
            self.logging_level = conf.getint('params', 'logging_level')

        for binding in self.key_bindings:
            if conf.has_option('keys', binding):
                custom_key = conf.get('keys', binding)
                self._set_key_binding(binding, custom_key)

        palette_labels = [color[0] for color in PALETTE]
        for label in palette_labels:
            if conf.has_option('colors', label):
                custom_fg = conf.get('colors', label)
                self._set_color(label, custom_fg)

    def _parse_legacy_token_file(self):
        conf = RawConfigParser()
        conf.read(LEGACY_TOKEN_FILE)

        if conf.has_option(SECTION_TOKEN, 'oauth_token'):
            self.oauth_token = conf.get(SECTION_TOKEN, 'oauth_token')

        if conf.has_option(SECTION_TOKEN, 'oauth_token'):
            self.oauth_token_secret = conf.get(SECTION_TOKEN, 'oauth_token_secret')

    def _set_color(self, color_label, custom_fg=None, custom_bg=None):
        for color in self.palette:
            label, fg, bg = color[0], color[1], color[2]
            if label == color_label:
                color[1] = custom_fg if validate_color(custom_fg) is not None else fg
                color[2] = custom_bg if validate_color(custom_bg) is not None else bg

    def _set_key_binding(self, binding, new_key):
        if not binding in self.key_bindings:
            return

        key, description = self.key_bindings[binding]
        new_key_binding = new_key, description
        self.key_bindings[binding] = new_key_binding

    def generate_config_file(self, config_file):
        kwargs = {
            'config_file': config_file,
            'on_error': partial(self._config_generation_error, config_file),
        }

        if not path.isfile(config_file):
            kwargs.update({
                'on_success': partial(self._config_generation_success,
                                      config_file)
            })

        self._generate_config_file(**kwargs)

    @wrap_exceptions
    def _generate_config_file(self, config_file):
        conf = RawConfigParser()

        self._add_section_twitter(conf)
        self._add_section_key_bindings(conf)
        self._add_section_palette(conf)
        self._add_section_styles(conf)
        self._add_section_debug(conf)

        with open(config_file, 'wb') as config:
            conf.write(config)

    def _config_generation_success(self, config_file):
        print encode(_('Generated configuration file in %s')) % config_file

    def _config_generation_error(self, config_file):
        print encode(_('Unable to generate configuration file in %s')) % config_file
        self.exit_with_code(2)

    def generate_token_file(self,
                            token_file,
                            oauth_token,
                            oauth_token_secret):
        self.oauth_token = oauth_token
        self.oauth_token_secret = oauth_token_secret

        conf = RawConfigParser()
        conf.add_section(SECTION_TOKEN)
        conf.set(SECTION_TOKEN, 'oauth_token', oauth_token)
        conf.set(SECTION_TOKEN, 'oauth_token_secret', oauth_token_secret)

        with open(token_file, 'wb') as tokens:
            conf.write(tokens)

        print encode(_('your account has been saved'))

    def parse_config_file(self, config_file):
        conf = RawConfigParser()
        conf.read(config_file)

        self._parse_twitter(conf)
        self._parse_key_bindings(conf)
        self._parse_palette(conf)
        self._parse_styles(conf)
        self._parse_debug(conf)

    def _parse_twitter(self, conf):
        if conf.has_option(SECTION_TWITTER, 'update_frequency'):
            self.twitter['update_frequency'] = conf.getint(SECTION_TWITTER, 'update_frequency')
        if conf.has_option(SECTION_TWITTER, 'use_https'):
            self.twitter['use_https'] = conf.getboolean(SECTION_TWITTER, 'use_https')

    def _parse_key_bindings(self, conf):
        for binding in self.key_bindings:
            if conf.has_option(SECTION_KEY_BINDINGS, binding):
                custom_key = conf.get(SECTION_KEY_BINDINGS, binding)
                self._set_key_binding(binding, custom_key)

    def _parse_palette(self, conf):
        # Color
        for label in self.palette:
            label_name, fg, bg = label[0], label[1], label[2]
            if conf.has_option(SECTION_PALETTE, label_name):
                fg = conf.get(SECTION_PALETTE, label_name)
            if conf.has_option(SECTION_PALETTE, label_name + '_bg'):
                bg = conf.get(SECTION_PALETTE, label_name + '_bg')
            self._set_color(label_name, fg, bg)

    def _parse_styles(self, conf):
        for style in self.styles:
            if conf.has_option(SECTION_STYLES, style):
                if any([style == 'box_around_status',
                        style == 'status_divider',
                        style == 'status_bar']):
                    self.styles[style] = conf.getboolean(SECTION_STYLES, style)
                elif (style == 'editor_horizontal_align' and
                      style in ['left', 'center', 'right']):
                    self.styles[style] = conf.get(SECTION_STYLES, style)
                elif (style == 'url_format' and
                      style in ['shortened', 'original', 'display']):
                    self.styles[style] = conf.get(SECTION_STYLES, style)
                elif style == 'statuses_in_user_info':
                    self.styles[style] = conf.getint(SECTION_STYLES, style)
                else:
                    self.styles[style] = unicode(conf.get(SECTION_STYLES, style),
                                                 'utf-8')

    def _parse_debug(self, conf):
        if conf.has_option(SECTION_DEBUG, 'logging_level'):
            self.logging_level = conf.getint(SECTION_DEBUG, 'logging_level')

    def parse_token_file(self, token_file):
        conf = RawConfigParser()
        conf.read(token_file)

        if conf.has_option(SECTION_TOKEN, 'oauth_token'):
            self.oauth_token = conf.get(SECTION_TOKEN, 'oauth_token')
        if conf.has_option(SECTION_TOKEN, 'oauth_token_secret'):
            self.oauth_token_secret = conf.get(SECTION_TOKEN, 'oauth_token_secret')

    def authorize_new_account(self):
        access_tokens = get_authorization_tokens()
        if access_tokens:
            access_token = access_tokens['oauth_token']
            access_token_secret = access_tokens['oauth_token_secret']

            self.oauth_token = access_token
            self.generate_token_file(self.token_file,
                                     access_token,
                                     access_token_secret)
        else:
            # TODO: exit codes
            self.exit_with_code(2)

    def reload(self):
        self.parse_config_file(self.config_file)
        self.key_mappings = invert_command_map(self.key_bindings)

    def exit_with_code(self, code):
        """Invoke `sys.exit` with the given status `code`."""
        # This is here because makes testing exit codes easier
        exit(code)


# configuration singleton
configuration = Configuration()
