#!/usr/bin/env python

# spud - keep track of photos
# Copyright (C) 2008-2013 Brian May
#
# This program 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/>.
from __future__ import absolute_import, print_function, unicode_literals

import argparse
import datetime
import os.path

import pytz
from requests import Session
from requests.exceptions import HTTPError
from six.moves import configparser


class BadRequestError(Exception):
    def __init__(self, json):
        self.json = json
        super().__init__()


class NotFoundError(Exception):
    pass


class Spud:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.session = Session()

    def login(self):
        url = self.base_url + '/session/login/'
        data = {
            'username': self.username,
            'password': self.password,
            'no_csrf': True,
        }
        response = self.session.post(url, data=data)
        response.raise_for_status()

    def check_errors(self, response):
        if response.status_code == 400:
            json = response.json()
            raise BadRequestError(json)
        if response.status_code == 404:
            json = response.json()
            raise NotFoundError(json['detail'])
        response.raise_for_status()

    def get(self, url, data):
        url = self.base_url + url
        response = self.session.get(url, params=data)
        self.check_errors(response)
        return response.json()

    def post(self, url, data, files=None):
        url = self.base_url + url
        headers = {}
        cookies = self.session.cookies
        if 'csrftoken' in cookies:
            headers["X-CSRFToken"] = cookies['csrftoken']
        response = self.session.post(
            url, data=data, headers=headers, files=files)
        self.check_errors(response)
        return response.json()

    def logout(self):
        self.post('/session/logout/', {})

    def get_session(self):
        return self.get('/session/', {})

    def get_albums(self, albums):
        results = []
        for album in albums:
            if '/' in album:
                parent, _, create_album = album.rpartition("/")
            else:
                parent = album
                create_album = None

            result = self.get_album(parent, create_album)
            results.append(result)

        return results

    def get_album(self, parent, create_album):
        try:
            parent_result = self.get('/albums/', {'obj': parent})
        except NotFoundError:
            raise RuntimeError("Cannot find album '%s'" % parent)

        assert parent_result["count"] == 1
        assert parent_result is not None
        parent_result = parent_result["results"][0]

        if create_album is not None:
            try:
                data = {
                    'q': create_album,
                    'instance': parent_result["id"],
                    'mode': "children",
                }
                result = self.get('/albums/', data)

                if result["count"] < 1:
                    raise NotFoundError()

                assert result["count"] == 1
                assert result["results"][0]["parent"] == parent_result["id"]
                result = result["results"][0]

            except NotFoundError:
                data = {
                    'title': create_album,
                    'parent': parent_result["id"],
                    'sort_name': 'date',
                    'sort_order': create_album,
                }
                result = self.post('/albums/', data)
                print("Created album %s" % result)
        else:
            result = parent_result

        return result["id"]

    def get_categorys(self, categorys):
        results = []
        for category in categorys:
            try:
                result = self.get('/albums/', {'obj': category})
            except NotFoundError:
                raise RuntimeError("Cannot find category '%s'" % category)

            assert result["count"] == 1
            results.append(result["results"][0]["id"])
        return results

    def get_place(self, place):
        try:
            result = self.get('/places/', {'obj': place})
        except NotFoundError:
            raise RuntimeError("Cannot find place '%s'" % place)

        assert result["count"] == 1
        return result["results"][0]["id"]

    def get_person(self, person):
        try:
            result = self.get('/persons/', {'obj': person})
        except NotFoundError:
            raise RuntimeError("Cannot find person '%s'" % person)

        assert result["count"] == 1
        return result["results"][0]["id"]

    def get_persons(self, persons):
        results = []
        for person in persons:
            if person == "unknown":
                results.append("unknown")
                continue

            results.append(self.get_person(person))
        return results


def parse_timezone(value):
    if value[0] == "+" or value[0] == "-":
        sign, offset = (value[0], value[1:])
        if len(offset) == 2:
            offset = int(offset) * 60
        elif len(offset) == 4:
            offset = (
                int(offset[0:2]) * 60 +
                int(offset[2:4]))
        else:
            raise RuntimeError("can't parse timezone %s" % value)
        if sign == '-':
            offset = -offset
        timezone = pytz.FixedOffset(offset)
        offset = None

    else:
        try:
            timezone = pytz.timezone(value)
        except pytz.UnknownTimeZoneError:
            raise RuntimeError("unknown timezone %s" % value)

    return timezone


def parse_timedelta(time):
    positive = 1
    if time[0] == "-":
        time = time[1:]
        positive = -1

    hh, mm, ss = time.split(":")
    hh = int(hh)
    mm = int(mm)
    ss = int(ss)

    return datetime.timedelta(seconds=positive*(hh*3600+mm*60+ss))


def import_photo(
        config, spud, src_filename,
        photographer, place, albums, categorys,
        src_timezone, dst_timezone, offset, dry_run):

    # check source file
    if not os.path.exists(src_filename):
        raise RuntimeError("source photo doesn't exist at %s" % (src_filename))

    m = media.get_media(src_filename)
    try:
        print("rotating %s..." % src_filename)
        m.rotate("auto")
    except NotImplementedError:
        pass

    print("processing %s..." % src_filename)
    exif = m.get_normalized_exif()

    # set everything without commiting anything
    photo = {}
    photo['photographer_pk'] = photographer
    photo['places_pk'] = place
    photo['albums_pk'] = albums
    photo['categorys_pk'] = categorys
    photo['level'] = 1

    camera = None
    if 'camera_model' in exif:
        camera = exif['camera_model']

    # get time
    dt = m.get_datetime()
    print("src time %s" % dt)

    if dt is None:
        dt = datetime.datetime.fromtimestamp(os.path.getmtime(src_filename))
        print("src time from file %s" % dt)

    if src_timezone is None and camera is not None \
            and config.has_section(camera):
        name = config.get(camera, "timezone")
        if name is not None:
            src_timezone = parse_timezone(name)

    if src_timezone is None:
        name = config.get('defaults', 'timezone')
        src_timezone = parse_timezone(name)

    dt = src_timezone.localize(dt)
    print("src time with timezone %s (%s)" % (dt, src_timezone))

    # adjust time for destination timezone
    if dst_timezone is None:
        name = config.get('defaults', 'timezone')
        dst_timezone = parse_timezone(name)

    dt = dt.astimezone(dst_timezone)
    print("adjusted with dst timezone %s (%s)" % (dt, dst_timezone))

    # add manual offsets
    td = datetime.timedelta(minutes=offset)
    dt += td
    print("adjusted with offset %s (%s)" % (dt, td))

    if camera is not None and config.has_section(camera):
        td = parse_timedelta(config.get(camera, 'offset'))
        dt += td
        print("adjusted with camera offset %s (%s)" % (dt, td))

    # save the time
    photo['utc_offset'] = int(dt.utcoffset().total_seconds() / 60)
    photo['datetime'] = dt.astimezone(pytz.utc).replace(tzinfo=None)

    if dry_run:
        print("would import %s"
              % (src_filename))
        return

    print("uploading photo %s" % photo)

    try:
        photo = spud.post(
            '/photos/',
            data=photo,
            files={'photo': open(src_filename, 'rb')}
        )
    except BadRequestError as e:
        json = e.json
        errors = json["non_field_errors"]
        if len(errors) == 1 and errors[0].startswith("File already exists at"):
            splitted = errors[0].split(" ")
            id = int(splitted[-1].rstrip("."))
            print("skipping %s as already uploaded as %d"
                  % (src_filename, id))
        else:
            raise
    else:
        print("imported %s as %d"
              % (src_filename, photo['id']))

    return photo


def main():
    parser = argparse.ArgumentParser(
        description='Upload photos to spud')
    parser.add_argument(
        '--album', action="append", default=[],
        help='Add photos to album. If album1/album2 syntax, '
        'will create album1 under album2.')
    parser.add_argument(
        '--album_prefix',
        help='Create album dirname under prefix.')
    parser.add_argument(
        '--category', action="append", default=[],
        help='Add photos to category')
    parser.add_argument(
        '--place',
        help='Add photos to place')
    parser.add_argument(
        '--photographer',
        help='Add photographer to photos.')
    # parser.add_argument(
    #     '--person', action="append", default=[],
    #     help='Add person to photos')
    parser.add_argument(
        '--config',
        default=os.path.join(os.getenv('HOME'), '.spud.conf'),
        help='Configuration file')
    parser.add_argument(
        '--dry_run', action="store_true",
        help='Dry run')
    parser.add_argument(
        'files', nargs='+',
        help='Files to add')
    args = parser.parse_args()

    with open(args.config):
        pass

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

    username = config.get("spud", "username")
    password = config.get("spud", "password")
    url = config.get("spud", "url")

    spud = Spud(
        base_url=url,
        username=username,
        password=password,
    )

    try:
        spud.login()
        # session = spud.get_session()
        albums = spud.get_albums(args.album)
        categorys = spud.get_categorys(args.category)

        place = None
        if args.place:
            place = spud.get_place(args.place)

        # persons = spud.get_persons(args.person)

        photographer = None
        if args.photographer:
            photographer = spud.get_person(args.photographer)

        src_timezone = None
        dst_timezone = None
        offset = 0

        for filename in args.files:
            print()
            print(filename)
            print("-"*20)
            albums_copy = albums

            if args.album_prefix:

                dirname = os.path.dirname(filename)
                dirname = os.path.basename(dirname)

                album_id = spud.get_album(args.album_prefix, dirname)

                albums_copy = list(albums)
                albums_copy.append(album_id)

            import_photo(
                config, spud, filename,
                photographer, place, albums_copy, categorys,
                src_timezone, dst_timezone, offset, args.dry_run)

        spud.logout()

    except HTTPError as e:
        print("HTTP error:", e)
        # json = e.response.json()
        # if json is not None:
        #     if 'detail' in json:
        #         print("Server error: %s" % json['detail'])
        #     else:
        #         print("Server error: %s" % json)
        #     exit(1)
        # else:
        # print("Server error:", e.msg)
        # print("Error:", e.msg.content)
        # raise

    return


if __name__ == "__main__":
    main()
