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

###############################################################################
# Copyright (C) 2014 by gempa GmbH                                            #
#                                                                             #
# All Rights Reserved.                                                        #
#                                                                             #
# NOTICE: All information contained herein is, and remains                    #
# the property of gempa GmbH and its suppliers, if any. The intellectual      #
# and technical concepts contained herein are proprietary to gempa GmbH       #
# and its suppliers.                                                          #
# Dissemination of this information or reproduction of this material          #
# is strictly forbidden unless prior written permission is obtained           #
# from gempa GmbH.                                                            #
#                                                                             #
# Author: Stephan Herrnkind                                                   #
# Email: herrnkind@gempa.de                                                   #
#                                                                             #
# Requests images via HTTP and stores them into CAPS server                   #
#                                                                             #
# All images are downloaded at once at regular interval. If available, the    #
# file modification time is read from the 'Last-Modififed' HTTP header. The   #
# sampling time of the packet sent to CAPS is either set to the time          #
# extracted from EXIF infomartion of from the file modification time.         #
# Packets are only sent if the current sampling time exceed the previous one. #
#                                                                             #
###############################################################################

from __future__ import absolute_import, division, print_function

import sys
import os
import fcntl
import time
import traceback

import http.client
import mimetypes

from datetime import datetime, timezone
from dateutil.tz import tzlocal

import pytz

from seiscomp import logging, client

from gempa import CAPS

###############################################################################


class StreamInfo:

    # -------------------------------------------------------------------------
    def __init__(self, filename, lastMod, lastSent):
        self.filename = filename
        self.lastMod = lastMod
        self.lastSent = lastSent


###############################################################################
class WebCam(client.Application):

    # -------------------------------------------------------------------------
    def __init__(self, argc, argv):
        client.Application.__init__(self, argc, argv)
        self.setMessagingEnabled(False)
        self.setDatabaseEnabled(False, False)

        self.host = "www.geonet.org.nz"
        self.port = 443
        self.ssl = True
        self.baseURL = "/volcano/cameras/latest/"
        self.baseDir = "/tmp/webcam"
        self.lockFile = os.path.join(self.baseDir, "lock")
        self.interval = 60  # fetch interval in seconds

        self.capsHost = "localhost"
        self.capsPort = 18003
        self.capsSSL = False

        # name of the time zone of the EXIF date, set to None if EXIF date is
        # in UTC, else e.g. 'Pacific/Auckland'
        self.exifTZName = None
        self.exifTZ = None
        self.localTZ = tzlocal()

        self.lastModFmt = "%a, %d %b %Y %H:%M:%S GMT"

        self.pushCMD = [
            "test2caps",
            "--read-from",
            "",
            "--format",
            "",
            "--id",
            "",
            "--begin",
            "",
            "--type",
            "ANY",
            "--interval",
            "0/1",
            "--verbosity",
            "0",
            "-H",
            "localhost",
            "-p",
            "18003",
        ]

        # map stream ids to filenames
        self.streams = {}
        self.streamConfig = {
            "NZ.RAOU..CAM": "raoulisland.jpg",
            "NZ.TEKA..CAM": "tekaha.jpg",
            "NZ.WHAK..CAM": "whakatane.jpg",
            "NZ.TONG..CAM": "tongariro.jpg",
            "NZ.TONG.TEMAARI.CAM": "tongarirotemaaricrater.jpg",
            "NZ.NGAU..CAM": "ngauruhoe.jpg",
            "NZ.RUAP.NORTH.CAM": "ruapehunorth.jpg",
            "NZ.RUAP.SOUTH.CAM": "ruapehusouth.jpg",
            "NZ.RUAP.RUHOE.CAM": "ruapehungauruhoe.jpg",
            "NZ.TARA..CAM": "taranaki.jpg",
        }

    # -------------------------------------------------------------------------
    # add last modification and last sent time to stream map as
    # (utc datetime objects)
    def initStreams(self):
        logging.info("initializing streams:")
        for sid, filename in self.streamConfig.items():
            path = os.path.join(self.baseDir, filename)

            modTime = self.readMTime(path)

            self.streams[sid] = StreamInfo(filename, modTime, None)
            logging.info(
                f"{sid}: file: {filename}, modified: "
                f"{'-' if modTime is None else modTime.isoformat()}"
            )

    # -------------------------------------------------------------------------
    # read mtime from file name
    # return datetime object converted to UTC
    def readMTime(self, path):
        # check file existence
        if not os.path.isfile(path):
            return None

        try:
            dt = datetime.fromtimestamp(os.path.getmtime(path))
            # convert to UTC
            if self.localTZ is not None:
                dt += self.localTZ.utcoffset(dt)

            return dt
        except Exception as e:
            logging.error(f"could not extract mtime from file '{path}': {e}")

        return None

    # -------------------------------------------------------------------------
    # read date from EXIF header of specified file
    # return datetime object converted to UTC
    def readEXIFDate(self, path):
        datekey = "EXIF DateTimeOriginal"
        datefmt = "%Y:%m:%d %H:%M:%S"

        # check file existence
        if not os.path.isfile(path):
            return None, f"no such file: {path}"

        try:
            import exifread  # pylint: disable=C0415,E0401
        except BaseException:
            return None, "could not import exifread python module"

        # read EXIF data
        try:
            with open(path, "rb") as f:
                tags = exifread.process_file(f)
        except IOError as e:
            return None, f"could not read EXIF header: {e}"

        # check for date key
        if datekey not in list(tags.keys()):
            return (
                None,
                f"could not find date key '{datekey}' in EXIF header of "
                f"file : {path}",
            )

        # parse date
        datestr = str(tags[datekey])
        try:
            dt = datetime.strptime(datestr, datefmt)
        except ValueError as e:
            return None, f"could not parse EXIF date of file '{path}': {e}"

        # convert to UTC
        if self.exifTZ is not None:
            dt += self.exifTZ.utcoffset(dt)

        return dt, None

    # -------------------------------------------------------------------------
    def fetch(self):
        headers = {
            "Connection": "keep-alive",
            "User-agent": (
                "Linux / Firefox 26: Mozilla/5.0 "
                "(X11; Linux x86_64; rv:26.0) "
                "Gecko/20100101 Firefox/26.0"
            ),
        }
        modSinceHeader = "If-Modified-Since"
        lastModHeader = "Last-Modified"

        logging.info(
            f"connecting to {'https' if self.ssl else 'http'}://{self.host}:{self.port}"
        )
        try:
            if self.ssl:
                con = http.client.HTTPSConnection(self.host, self.port)
            else:
                con = http.client.HTTPConnection(self.host, self.port)
        except Exception as e:
            logging.info(f"connection error: {format(e)}")
            return False

        updated = False

        i = 0
        for sid, info in self.streams.items():
            # deactivate keep-alive for last request
            i += 1
            if i == len(self.streams):
                headers.pop("Connection")

            # request image only if it was modified since the last request
            lastModStr = None
            if info.lastMod is None:
                if modSinceHeader in headers:
                    headers.pop(modSinceHeader)
            else:
                lastModStr = info.lastMod.strftime(self.lastModFmt)
                headers[modSinceHeader] = lastModStr

            # request data
            url = self.baseURL + info.filename
            logging.info(f"fetching {sid}:")
            msg = f"    GET {url}"
            if lastModStr is not None:
                msg += " ({modSinceHeader}: {lastModStr})"
            logging.info(msg)

            try:
                con.request("GET", url, headers=headers)
            except BaseException:
                logging.error(f"could not send request: {sys.exc_info()[0]}")
                break
            resp = con.getresponse()

            logging.info(f"{resp.status} {resp.reason}")
            if resp.status != 200:
                resp.read()
                continue

            # write file
            path = os.path.join(self.baseDir, info.filename)
            try:
                with open(path, "wb+") as f:
                    f.write(resp.read())
            except OSError as e:
                logging.error(f"could not write image file '{path}': {e}")
                continue

            info.lastMod = datetime.now(timezone.utc)
            updated = True

            # read modification time from HTTP header
            lastModStr = resp.getheader(lastModHeader, None)
            logging.info(
                f"last modified: {(lastModStr if lastModStr is not None else 'UNKNOWN')}"
            )

            if lastModStr is None:
                continue

            try:
                info.lastMod = datetime.strptime(lastModStr, self.lastModFmt)
            except Exception as e:
                logging.error(f"error paring last-modified string: {e}")
                continue

            # update file modification time if last-modified header was present
            try:
                epoch = datetime(1970, 1, 1)
                atime = (datetime.now(timezone.utc) - epoch).total_seconds()
                mtime = (info.lastMod - epoch).total_seconds()
                os.utime(path, (atime, mtime))
            except Exception as e:
                logging.error(f"could not update file modification time: {e}")

        con.close()
        return updated

    # -------------------------------------------------------------------------
    def push(self):
        output = CAPS.Plugin("imageimporter")
        output.setHost(self.capsHost)
        output.setPort(self.capsPort)
        output.setSSLEnabled(self.capsSSL)
        output.setBufferSize(1 << 30)

        for sid, info in self.streams.items():
            logging.info(f"pushing {sid}:")

            path = os.path.join(self.baseDir, info.filename)
            if not os.path.isfile(path):
                logging.error("file not found")
                continue
            if info.lastMod is None:
                logging.error("unknown modification time")
                continue

            samplingTime, msg = self.readEXIFDate(path)
            if samplingTime is None:
                logging.warning(f"{msg}")
                samplingTime = info.lastMod

            # check if image was updated
            if info.lastSent is not None and info.lastSent >= samplingTime:
                logging.info("no update detected")
                continue

            # determine mime type
            mtype = mimetypes.guess_type(path)
            if mtype[0] is None:
                logging.error(f"{sid}: failed to read MIME type of file: {path}")
                continue
            mtype = mtype[0]
            mtype = mtype[mtype.find("/") + 1 :].upper()

            # push image to caps server
            timestamp = CAPS.Time(
                samplingTime.year,
                samplingTime.month,
                samplingTime.day,
                samplingTime.hour,
                samplingTime.minute,
                samplingTime.second,
                samplingTime.microsecond,
            )

            try:
                net, sta, loc, cha = sid.split(".", 3)
            except BaseException:
                logging.error("invalid stream id")
                continue

            try:
                with open(path, "rb") as f:
                    data = f.read()
            except Exception as e:
                logging.error(f"could not read image file: {e}")
                continue

            try:
                logging.info(
                    f"timestamp: {timestamp.iso()}, mime: {mtype}, "
                    f"size: {len(data)}, data: {type(data)}"
                )
                output.push(net, sta, loc, cha, timestamp, 1, 0, mtype, data)
            except Exception as e:
                logging.error(f"CAPS communication error: {e}")
                logging.error(traceback.format_exc())
                output.close()
                break

        output.close()

    # -------------------------------------------------------------------------
    def run(self):
        # create tmp dir if it does not exist yet
        if not os.path.isdir(self.baseDir):
            try:
                os.makedirs(self.baseDir)
            except OSError as e:
                logging.error(f"could not create temporary directory: {format(e)}")
                return 5

        # try to lock file
        f = None
        try:
            f = open(self.lockFile, "w")
        except IOError as e:
            logging.error(f"could not open lock file for writing: {format(e)}")
            return 1

        try:
            fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError:
            logging.error("could not acquire lock, application already running?")
            return 2

        if not self.streamConfig:
            logging.error("no streams configured")
            return 3

        self.initStreams()

        # initialize time zone if specified
        if self.exifTZName is not None:
            try:
                self.exifTZ = pytz.timezone(self.exifTZName)
            except pytz.exceptions.UnknownTimeZoneError:
                logging.error(f"could not retrieve timezone: {self.exifTZName}")
                return 4

        # main loop
        first = True
        while True:
            if self.isExitRequested():
                break
            start = datetime.now(timezone.utc)

            if self.fetch() or first:
                self.push()
                first = False

            # schedule next run
            delta = datetime.now(timezone.utc) - start
            sleep = self.interval - delta.total_seconds()
            if sleep <= 0:
                logging.warning("warning: could not schedule next fetch in time")
            else:
                logging.info(f"schedule next fetch in {sleep:.2f} seconds")
                for _i in range(0, int(sleep)):
                    time.sleep(1)
                    if self.isExitRequested():
                        break
        return True


# -----------------------------------------------------------------------------
if __name__ == "__main__":
    app = WebCam(len(sys.argv), sys.argv)
    sys.exit(app())


# vim: ts=4 noet
