#! /usr/bin/python3

# mythcal

# Copyright 2009-2014 Richard Fearn
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function
from builtins import input
from operator import itemgetter
from os.path import dirname, join
import configparser
from optparse import Values, OptionParser
from datetime import datetime
import pickle
import os
import sys
import time

from MythTV import MythBE
import pytz

from oauth2client.client import OAuth2WebServerFlow
from oauth2client.file import Storage

import httplib2
from apiclient.discovery import build
from oauth2client.file import Storage
from apiclient.errors import HttpError


BASEDIR = dirname(sys.argv[0])
CONFIG_FILE = join(BASEDIR, "mythcal.conf")
CACHE_FILE = join(BASEDIR, "mythcal.cache")
CREDENTIALS_FILE = join(BASEDIR, "mythcal.credentials")

VERSION = "0.27.0"

# From https://developers.google.com/accounts/docs/OAuth2:
#     "...a client secret, which you embed in the source code of your
#     application. (In this context, the client secret is obviously not treated
#     as a secret.)"
OAUTH_CLIENT_ID = '17959633059-62g87ggkkr0a1lub43egv3rgfp7afphq.apps.googleusercontent.com'
OAUTH_CLIENT_SECRET = 'wlkk_7uk8La2Ceqp9OY49qSH'
OAUTH_SCOPE = 'https://www.googleapis.com/auth/calendar'
OAUTH_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'

AUTH_MESSAGE_1 = """Visit this URL in your browser:

  %s

When you have granted mythcal access to your calendar, enter the code you are
given below.
"""

AUTH_MESSAGE_2 = "Done! You can now run mythcal without the `--auth` flag."

DATE_FORMAT = "%Y-%m-%d"
DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.000%z"

config = configparser.RawConfigParser()
config.read(CONFIG_FILE)

settings = Values({
        "mythtv": Values({
                "timezone": config.get("mythtv", "timezone")
        }),
        "calendar": Values({
                "id": config.get("calendar", "id"),
        })
})

parser = OptionParser()
parser.add_option("-a", "--auth",    action="store_true", help="authenticate (required before using mythcal for the first time)")
parser.add_option("-n", "--dry-run", action="store_true", help="perform a trial run; don't make any changes (implies -v)")
parser.add_option("-V", "--version", action="store_true", help="show version and exit")
parser.add_option("-v", "--verbose", action="store_true", help="increase verbosity")
(options, args) = parser.parse_args()

if options.version:
    print("mythcal %s" % VERSION)
    sys.exit(0)

if options.dry_run:
    options.verbose = True

if options.auth:

    flow = OAuth2WebServerFlow(
        client_id = OAUTH_CLIENT_ID,
        client_secret = OAUTH_CLIENT_SECRET,
        scope = OAUTH_SCOPE,
        redirect_uri = OAUTH_REDIRECT_URI
    )

    auth_uri = flow.step1_get_authorize_url()
    print(AUTH_MESSAGE_1 % auth_uri)

    auth_code = input("Code: ")
    print()

    credentials = flow.step2_exchange(auth_code)
    storage = Storage(CREDENTIALS_FILE)
    storage.put(credentials)
    print(AUTH_MESSAGE_2)
    sys.exit(0)

# check for existence of credentials
if not os.path.isfile(CREDENTIALS_FILE):
    print("mythcal: no credentials file. You need to run `mythcal --auth` first", file=sys.stderr)
    sys.exit(1)

# get pytz timezone object for local time zone
if settings.mythtv.timezone not in pytz.all_timezones:
    print("mythcal: timezone name '%s' is not recognised" % settings.mythtv.timezone, file=sys.stderr)
    sys.exit(1)
time_zone = pytz.timezone(settings.mythtv.timezone)

DATE_TIME_DEBUG = False

def mythtv_time_to_naive_utc_time(mythtv_time):

    """Convert MythTV time to naive UTC time"""

    if DATE_TIME_DEBUG:
        print("converting time...")

    # The timestamp we get from a MythTV datetime object is affected by its timezone...
    aware_local_mythtv_time = mythtv_time
    if DATE_TIME_DEBUG:
        print("  %s\t%s\t%d" % (aware_local_mythtv_time, aware_local_mythtv_time.timetuple(), aware_local_mythtv_time.timestamp()))

    # ...so we convert to UTC to ensure we get a POSIX timestamp from it
    aware_utc_mythtv_time = aware_local_mythtv_time.astimezone(pytz.utc)
    if DATE_TIME_DEBUG:
        print("  %s\t%s\t%d" % (aware_utc_mythtv_time, aware_utc_mythtv_time.timetuple(), aware_utc_mythtv_time.timestamp()))

    # Create a standard datetime that represents the same point in time...
    aware_utc_datetime = datetime.fromtimestamp(aware_utc_mythtv_time.timestamp(), pytz.utc)
    if DATE_TIME_DEBUG:
        print("  %s\t%s" % (aware_utc_datetime, aware_utc_datetime.timetuple()))

    # ...and drop the timezone
    naive_utc_datetime = aware_utc_datetime.replace(tzinfo=None)
    if DATE_TIME_DEBUG:
        print("  %s\t\t%s" % (naive_utc_datetime, naive_utc_datetime.timetuple()))

    return naive_utc_datetime

def convert_program(prog):
    """Converts a MythTV Program object to a dictionary"""
    return {
        "title": prog.title,
        "subtitle": prog.subtitle,
        "channel": prog.channame,
        "start": int(prog.starttime.timestamp()),
        "end": int(prog.endtime.timestamp()),
        "description": prog.description
    }


def get_recordings_from_backend():
    """Gets current and upcoming recordings from MythTV"""

    if options.verbose:
        print("Getting recordings from MythTV backend...")

    mythtv = MythBE()

    upcoming = mythtv.getUpcomingRecordings()
    upcoming = list(upcoming) # convert listiterator to list

    if options.verbose:
        print("    found %d upcoming recording(s)" % len(upcoming))

    upcoming = [convert_program(p) for p in upcoming]
    upcoming.sort(key=itemgetter("start"))

    current = []
    for recorder in mythtv.getRecorderList():
        if mythtv.isRecording(recorder):
            prog = mythtv.getCurrentRecording(recorder)
            # don't include live programmes (issue 16)
            if prog.get("recgroup") != "LiveTV":
                current.append(prog)
    if options.verbose:
        print("    found %d current recording(s)" % len(current))

    current = [convert_program(p) for p in current]
    current.sort(key=itemgetter("start"))

    return {"current": current, "future": upcoming}

# get recordings from MythTV backend
recordings = get_recordings_from_backend()

# load recording list from last time
last_recordings = None
if os.path.exists(CACHE_FILE):
    if options.verbose:
        print("Reading cache file...")
    with open(CACHE_FILE, "rb") as f:
        try:
            last_recordings = pickle.load(f)
        except EOFError as ex:
            print("Failed: %s" % (ex,), file=sys.stderr)
    if options.verbose:
        print("Done.")
else:
    if options.verbose:
        print("Cache file does not exist.")

def submit_batch_request(request, url):
    response_feed = calendar_service.ExecuteBatch(request, url)
    # for entry in response_feed.entry:
    #     print "%s; status %s; reason %s" % (entry.batch_id.text, entry.batch_status.code, entry.batch_status.reason)

def delete_existing_events():
    """Deletes all events from the calendar"""

    if options.verbose:
        print("Deleting existing entries...")
    result = service.events().list(calendarId = settings.calendar.id).execute()
    while result:
        for item in result["items"]:
            if options.verbose:
                print("    will delete \"%s\"" % item["summary"])
            if not options.dry_run:
                service.events().delete(calendarId = settings.calendar.id, eventId = item["id"]).execute()
        if "nextPageToken" in result:
            result = service.events().list(calendarId = settings.calendar.id, pageToken = result["nextPageToken"]).execute()
        else:
            result = None
    if options.verbose:
        print("Existing entries deleted.")

# update calendar, and output new recording list, if different
if recordings != last_recordings:

    # get calendar service and log in
    if options.verbose:
        print("Logging into Google Calendar...")
    storage = Storage(CREDENTIALS_FILE)
    credentials = storage.get()
    http = httplib2.Http()
    http = credentials.authorize(http)
    service = build("calendar", "v3", http=http)
    if options.verbose:
        print("Done.")

    # Get all calendars
    if options.verbose:
        print("Getting calendar...")
    try:
        result = service.calendars().get(calendarId = settings.calendar.id).execute()
    except HttpError as e:
        print("mythcal: could not get calendar with id '%s':" % settings.calendar.id, file=sys.stderr)
        print(e.content, file=sys.stderr)
        sys.exit(1)
    if options.verbose:
        print("Done.")

    delete_existing_events()


    def create_all_day_event(title, start, end, content=None):
        event = {
            "summary": title,
            "start": {
                "date": time.strftime(DATE_FORMAT, start),
            },
            "end": {
                "date": time.strftime(DATE_FORMAT, end),
            },
            "description": content
        }
        return event


    def create_programme_event(prog):
        return {
            "summary": ("%(title)s: %(subtitle)s (%(channel)s)" if prog.get("subtitle") else "%(title)s (%(channel)s)") % prog,
            "start": {
                "dateTime": datetime.fromtimestamp(prog["start"], tz=pytz.utc).strftime(DATE_TIME_FORMAT),
            },
            "end": {
                "dateTime": datetime.fromtimestamp(prog["end"], tz=pytz.utc).strftime(DATE_TIME_FORMAT),
            },
            "description": prog["description"],
        }


    if options.verbose:
        print("Adding new entries...")

    # add an event for current/future recordings
    for prog in recordings["current"] + recordings["future"]:
        if options.verbose:
            print("    will add \"%s\"" % prog["title"])
        if not options.dry_run:
            event = create_programme_event(prog)
            service.events().insert(calendarId = settings.calendar.id, body=event).execute()

    # add 'last updated' event
    last_update_text = "MythTV updated %s" % time.strftime("%H:%M", time.localtime())
    if options.verbose:
        print("    will add \"%s\"" % last_update_text)
    if not options.dry_run:
        event = create_all_day_event(title=last_update_text, start=time.gmtime(), end=time.gmtime(time.time() + 24*60*60))
        service.events().insert(calendarId = settings.calendar.id, body=event).execute()

    if options.verbose:
        print("New entries added.")

    # update last recording list
    if options.verbose:
        print("Updating cache...")
    if not options.dry_run:
        with open(CACHE_FILE, "wb") as f:
            pickle.dump(recordings, f)

    if options.verbose:
        print("Done.")

else:
    if options.verbose:
        print("Recordings have not changed; not updating calendar.")

if options.verbose:
    print("Finished.")
