#! /usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (C) 2013 David Callé <davidc@framli.eu>
# Copyright (C) 2012 Matt Fischer <matthew.fischer@canonical.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.

from gi.repository import Unity, UnityExtras
from gi.repository import Gio, GLib
import urllib.parse
import urllib.request
import feedparser
import gettext
from datetime import datetime


APP_NAME = 'unity-scope-yahoostock'
LOCAL_PATH = '/usr/share/locale/'
gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
gettext.textdomain(APP_NAME)
_ = gettext.gettext

GROUP_NAME = 'com.canonical.Unity.Scope.News.Yahoostock'
UNIQUE_PATH = '/com/canonical/unity/scope/news/yahoostock'
SEARCH_HINT = _('Search Yahoostock')
NO_RESULTS_HINT = _('Sorry, there are no Stock Quotes that match your search.')
PROVIDER_CREDITS = _('Powered by Yahoo! Finance')
SVG_DIR = '/usr/share/icons/unity-icon-theme/places/svg/'
PROVIDER_ICON = SVG_DIR+'service-yahoofinance.svg'
DEFAULT_RESULT_ICON = SVG_DIR+'result-news.svg'
DEFAULT_RESULT_MIMETYPE = 'text/html'
DEFAULT_RESULT_TYPE = Unity.ResultType.DEFAULT
ICON_SIZE_RESULT = 133
ICON_SIZE_PREVIEW = 380

c1 = {'id'      :'recent',
      'name'    :_('Quotes'),
      'icon'    :SVG_DIR+'group-installed.svg',
      'renderer':Unity.CategoryRenderer.HORIZONTAL_TILE}
c2 = {'id'      :'top',
      'name'    :_('Headlines'),
      'icon'    :SVG_DIR+'group-installed.svg',
      'renderer':Unity.CategoryRenderer.HORIZONTAL_TILE}
CATEGORIES = [c1, c2]

FILTERS = []

m1 = {'id'   :'stock',
      'type' :'s',
      'field':Unity.SchemaFieldType.OPTIONAL}
EXTRA_METADATA = [m1]

# source for how this API works: http://www.gummy-stuff.org/Yahoo-data.htm
QUOTE_URL = "http://finance.yahoo.com/d/quotes.csv?s=%s&f=snl1d1t1vc"
# source for the news
NEWS_URL = "http://finance.yahoo.com/rss/headline?s=%s"
# callback URL, this loads the webpage for a ticker
CALLBACK_URL = "http://finance.yahoo.com/q?s=%s&ql=1"
# Test injection
FAKE_CALL = False

def search(search, filters):
    '''
    Any search method returning results as a list of tuples.
    Available tuple fields:
    uri (string)
    icon (string)
    title (string)
    comment (string)
    dnd_uri (string)
    mimetype (string)
    category (int)
    result_type (Unity ResultType)
    extras metadata fields (variant)
    '''
    results = []
    if not search:
        return results
    ys = Yahoostock()
    quotes = ys.getQuotes(urllib.parse.quote(search))
    for quote in quotes:
        volume_str = ys.getVolumeStr(quote['volume'])
        change_str = ys.getChangeStr(quote['change'])
        comment = "%s\n%s@%s\n%s\n%s" % (quote['name'],
                  quote['lastVal'], quote['lastTime'],
                  volume_str, change_str)
        print (quote['symbol'])
        if quote['name'] == 'Invalid Symbol' or quote['lastVal'] == '0.00':
            return results
        results.append({'uri':ys.getQuoteUrl(quote['symbol']),
                        'icon':'http://chart.finance.yahoo.com/t?s=%s&lang=en-US&region=US&width=%i&height=%i' % (quote['symbol'], ICON_SIZE_RESULT, ICON_SIZE_RESULT),
                        'title':quote['symbol'],
                        'comment':comment,
                        'stock':quote['symbol'],
                        'last_date':quote['lastDate'],
                        'evolution':quote['change']})
    return results

class Yahoostock:

    # splits a string by splitChar, but if splitChar is inside a quote,
    # it doesn't split.  This is required to fix companies which annoyingly
    # have commas in their name, for example:
    # "RHT","Red Hat, Inc. Com",47.40,"1/26/2012","4:01pm",47.82,48.49,47.08
    def _quotedSplit(self, line, splitChar):
        vals = []
        entry = ""
        insideQuote = False
        for char in line:
            entry += char
            if char == splitChar and insideQuote is False:
                # when we append, skip the comma
                vals.append(entry[:-1])
                entry = ""
            elif char == "\"":
                if insideQuote is True:
                    insideQuote = False
                else:
                    insideQuote = True
        #don't forget the last one, the line has no trailing comma!
        vals.append(entry)
        return vals

    def _isValid(self, quote):
        """Returns True if the quote is valid, False if not"""
        if quote["symbol"] == quote["name"]\
            and quote["lastVal"] == '0.00'\
            and quote["lastDate"] == quote["lastTime"]\
            and quote["lastDate"] == "N/A"\
            and quote["change"] == "N/A - N/A":
                return False
        return True

    def _parseLine(self, line):
        quote = {}
        vals = []
        vals = self._quotedSplit(line, ',')
        quote["symbol"] = vals[0].replace('\"', '')
        quote["name"] = vals[1].replace('\"', '')
        quote["lastVal"] = vals[2]
        quote["lastDate"] = vals[3].replace('\"', '')
        quote["lastTime"] = vals[4].replace('\"', '')
        quote["volume"] = vals[5]
        quote["change"] = vals[6].strip().replace('\"', '')
        if not self._isValid(quote):
            quote["name"] = "Invalid Symbol"
        return quote

    def _parseData(self, stream):
        quotes = []
        for line in stream.readlines():
            try:
                quote = self._parseLine(line.decode('utf-8'))
                quotes.append(quote)
            except Exception as error:
                print(error)
        return quotes

    # this expects to be passed in a string like this:
    # HPQ+A+RHT+GM  no spaces!
    def getQuotes(self, symbols):
        url = QUOTE_URL % (symbols)
        print(url)
        stream = urllib.request.urlopen(url)
        if stream is not None:
            if stream.getcode() is 200 or FAKE_CALL is True:
                return self._parseData(stream)
            else:
                print ("error: http returned %d" % (stream.getcode()))
                stream.close()
                return []

    def getQuoteUrl(self, symbol):
        return  CALLBACK_URL % (symbol)

    def getNewsForSymbol(self, symbol, maxitems):
        stories = []
        url = NEWS_URL % (symbol)
        d = feedparser.parse(url)
        if d:
            count = 0
            for entry in d.entries:
                try:
                    if "title" not in entry.keys():
                        continue
                    if "link" not in entry.keys():
                        continue
                    ## skip all pay stories which start with "[$$]" in the title
                    if entry.title.startswith("[$$]") or entry.link is None:
                        continue
                    story = {}
                    story["title"] = entry.title
                    story["link"] = entry.link
                    if "published" in entry.keys():
                        story["date"] = entry.published
                    elif "updated" in entry.keys():
                        story["date"] = entry.updated
                    else:
                        story["date"] = ""
                    if "description" in entry.keys():
                        story["description"] = entry.description
                    else:
                        story["description"] = ""
                    stories.append(story)
                    count = count + 1
                except Exception as e:
                    print ("Exception: ", e)
                if count >= maxitems:
                    break
        return stories

    def getChangeStr(self, change):
        if change is None or change is "" or change is "N/A" or change is "N/A - N/A":
            return ""
        change = change.replace("\"", "").strip()
        changeAmt, changePct = change.split(" - ", 2)
        try:
            changeFlt = round(float(changeAmt), 2)
            # hooray for unicode symbols
            # http://www.alanwood.net/unicode/arrows.html
            if changeFlt < 0:
                # down
                arrow = chr(8600)
            elif changeAmt > 0:
                # up
                arrow = chr(8599)
            else:
                # unch
                arrow = chr(8594)
        except Exception as e:
            print ("Exception: ", e)
            return ""
        return "%s  %s (%s)" % (arrow, changeFlt, changePct)

    def getVolumeStr(self, volume):
        if volume is None or volume is "" or volume is "N/A":
            return ""
        try:
            fVol = float(volume)
            if fVol > 1000000:
                return _("Vol: ") + str(round(fVol / 1000000, 3)) + "M"
            elif fVol > 10000:
                return _("Vol: ") + str(round(fVol / 1000, 3)) + "k"
            else:
                return _("Vol: ") + volume
        except Exception as e:
            print ("Exception: ", e)
            return ""



# Classes below this point establish communication
# with Unity, you probably shouldn't modify them.


class MySearch (Unity.ScopeSearchBase):
    def __init__(self, search_context):
        super (MySearch, self).__init__()
        self.set_search_context (search_context)

    def do_run (self):
        '''
        Adds results to the model
        '''
        try:
            result_set = self.search_context.result_set
            for i in search(self.search_context.search_query,
                            self.search_context.filter_state):
                if not 'uri' in i or not i['uri'] or i['uri'] == '':
                    continue
                if not 'icon' in i or not i['icon'] or i['icon'] == '':
                    i['icon'] = DEFAULT_RESULT_ICON
                if not 'mimetype' in i or not i['mimetype'] or i['mimetype'] == '':
                    i['mimetype'] = DEFAULT_RESULT_MIMETYPE
                if not 'result_type' in i or not i['result_type'] or i['result_type'] == '':
                    i['result_type'] = DEFAULT_RESULT_TYPE
                if not 'category' in i or not i['category'] or i['category'] == '':
                    i['category'] = 0
                if not 'title' in i or not i['title']:
                    i['title'] = ''
                if not 'comment' in i or not i['comment']:
                    i['comment'] = ''
                if not 'dnd_uri' in i or not i['dnd_uri'] or i['dnd_uri'] == '':
                    i['dnd_uri'] = i['uri']
                result_set.add_result(**i)
        except Exception as error:
            print (error)

class Preview (Unity.ResultPreviewer):

    def do_run(self):
        graph = Gio.FileIcon.new(Gio.file_new_for_uri(self.result.icon_hint.replace('=%i' % ICON_SIZE_RESULT, '=%i'% ICON_SIZE_PREVIEW)))
        corp = self.result.comment.split('\n')[0]
        value = self.result.comment.split('\n')[1].split('@')[0]
        time = self.result.comment.split('\n')[1].split('@')[1]
        volume = self.result.comment.split('\n')[2].split("Vol: ")[1]
        trend = self.result.metadata['evolution'].get_string()
        trends = trend.split(' ')
        date = self.result.metadata['last_date'].get_string()
        delta = datetime.today() - datetime.strptime(date, '%m/%d/%Y')
        if delta.days == 1:
            date = _('Yesterday')
        elif delta.days > 1:
            date = _('%i days ago' % delta.days)
        else:
            date = None
        preview = Unity.GenericPreview.new(corp, '', graph)
        if date:
            preview.props.subtitle = '%s, %s' % (date, time)
        else:
            preview.props.subtitle = time
        if len(trends) > 1:
            if trends[0] != trends[-1]:
                trend = '%s (%s)' % (trends[0], trends[-1])
            else:
                trend = trends[0]
            if trend.startswith('-'):
                change_arrow = "\u2B0A"
                trend = '%s %s' % (change_arrow, trend[1:])
            else:
                change_arrow = "\u2B08"
                trend = '%s %s' % (change_arrow, trend[1:])
            preview.add_info(Unity.InfoHint.new("evolution", _("Evolution"), None, trend))
        preview.add_info(Unity.InfoHint.new("value", _("Value"), None, value))
        preview.add_info(Unity.InfoHint.new("volume", _("Volume"), None, volume))
        preview.add_info(Unity.InfoHint.new("symbol", _("Symbol"), None, self.result.title))
        icon = Gio.FileIcon.new (Gio.file_new_for_path(PROVIDER_ICON))
        view_action = Unity.PreviewAction.new("open", _("View"), icon)
        preview.add_action(view_action)
        return preview

class Scope (Unity.AbstractScope):
    def __init__(self):
        Unity.AbstractScope.__init__(self)

    def do_get_search_hint (self):
        return SEARCH_HINT

    def do_get_schema (self):
        '''
        Adds specific metadata fields
        '''
        schema = Unity.Schema.new ()
        if EXTRA_METADATA:
            for m in EXTRA_METADATA:
                schema.add_field(m['id'], m['type'], m['field'])
        #FIXME should be REQUIRED for credits
        schema.add_field('provider_credits', 's', Unity.SchemaFieldType.OPTIONAL)
        return schema

    def do_get_categories (self):
        '''
        Adds categories
        '''
        cs = Unity.CategorySet.new ()
        if CATEGORIES:
            for c in CATEGORIES:
                cat = Unity.Category.new (c['id'], c['name'],
                                          Gio.ThemedIcon.new(c['icon']),
                                          c['renderer'])
                cs.add (cat)
        return cs

    def do_get_filters (self):
        '''
        Adds filters
        '''
        fs = Unity.FilterSet.new ()
#        if FILTERS:
#
        return fs

    def do_get_group_name (self):
        return GROUP_NAME

    def do_get_unique_name (self):
        return UNIQUE_PATH

    def do_create_search_for_query (self, search_context):
        se = MySearch (search_context)
        return se

    def do_create_previewer(self, result, metadata):
        rp = Preview()
        rp.set_scope_result(result)
        rp.set_search_metadata(metadata)
        return rp

def load_scope():
    return Scope()
