#
# Copyright (c) 2018,2019 Eric Faurot <eric@faurot.net>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

import collections
import contextlib
import hashlib
import os
import time


import udon.path


ContentInfo = collections.namedtuple('ContentInfo', ['size', 'timestamp', 'offset', 'sha256', 'headers' ])


def reader(path):
    fp = open(path, "rb")
    try:
        info = {}
        headers = []
        while True:
            line = fp.readline()
            if line == b'\n':
                break
            key, value = line.split(b':', 1)
            value = value.strip()
            if key == b'Timestamp':
                info['timestamp'] = int(value)
            elif key == b'Size':
                info['size'] = int(value)
            elif key == b'Checksum-SHA256':
                info['sha256'] = value.decode()
            headers.append((key.decode(), value.decode('utf-8')))
        fp.info = ContentInfo(headers = headers, offset = fp.tell(), **info)
        assert os.fstat(fp.fileno()).st_size == fp.info.size + fp.info.offset
    except:
        fp.close()
        raise
    return fp


@contextlib.contextmanager
def writer(path, expect_size = None, tmpdir = None):
    with udon.path.overwriting(path, tmpdir = tmpdir) as fp:
        wrt = _ContentWriter(fp, expect_size = expect_size)
        yield wrt
        wrt.close()


class _ContentWriter:

    RESERVED_HDRS = set(("Checksum-SHA256",
                         "Size",
                         "Timestamp"))
    MAX_KEY_LEN = 128
    MAX_VALUE_LEN = 2 ** 14
    MAX_SIZE = 2 ** 48

    _headers_done = False
    size = 0
    wpos = 0

    def __init__(self, fp, expect_size = None):
        self.expect_size = expect_size
        self.cksum = hashlib.sha256()
        self.timestamp = int(time.time())
        self.fp = fp
        self._headers = {}

    def write(self, data):
        if not self._headers_done:
            self._end_headers()
        self.cksum.update(data)
        self.fp.write(data)
        self.size += len(data)

    def write_header(self, hdr, value):
        assert not self._headers_done
        if not self._headers:
            self._write_internal_headers()

        if not isinstance(hdr, str):
            raise TypeError("Key must be a string")
        if len(hdr) >= self.MAX_KEY_LEN:
            raise ValueError("Key too large")
        if ":" in hdr:
            raise ValueError("Key must not contain ':'")
        if "\n" in hdr:
            raise ValueError("Key must not contain '\n'")
        if hdr in self.RESERVED_HDRS:
            raise ValueError("Reserved header")
        self._write_header(hdr, value)

    def update_header(self, hdr, value):
        offset, size = self._headers[hdr]
        value = self._coerce_value(value)
        missing = size - len(value)
        if missing < 0:
            raise ValueError("Updated value too large for key \"%s\" (%d/%d)" % (hdr, len(value), size))
        self.fp.seek(offset)
        if missing:
            self.fp.write(b' ' * missing)
        self.fp.write(value)

    def close(self):
        if not self._headers_done:
            self._end_headers()
        self.update_header("Checksum-SHA256", self.cksum.hexdigest())
        self.update_header("Size", self.size)
        self.fp.close()
        if self.expect_size not in (None, self.size):
            raise ValueError("Content has incorrect size")

    def _coerce_value(self, value):
        if not isinstance(value, bytes):
            value = str(value).encode('utf-8')
        if b'\n' in value:
            raise ValueError("Value must not contain '\n'")
        if len(value) >= self.MAX_VALUE_LEN:
            raise ValueError("Value too large")
        return value

    def _write_internal_headers(self):
        self._write_header("Checksum-SHA256", self.cksum.hexdigest())
        self._write_header("Size", self.MAX_SIZE)
        self._write_header("Timestamp", self.timestamp)

    def _write_header(self, header, value):
        hdr = b"%s: " % header.encode('utf-8')
        val = self._coerce_value(value)
        self._headers[header] = self.wpos + len(hdr), len(val)
        self.fp.write(hdr)
        self.fp.write(val)
        self.fp.write(b'\n')
        self.wpos += len(hdr) + len(val) + 1

    def _end_headers(self):
        if not self._headers:
            self._write_internal_headers()
        self.fp.write(b"\n")
        self.wpos += 1
        self._headers_done = True
