#!/usr/bin/env python
# -*- coding: ascii -*-

"""
Brief module docstring
TODO: doc me
"""

__author__ = 'Daniel Kovacic (dkov01@gmail.com)'
__copyright__ = 'Copyright (c) 2015 Daniel Kovacic'
__license__ = '3-clause BSD'
__version__ = '0.0.1'

from enum import Enum
import io
from collections import namedtuple
import decimal


class ObjectStatus(Enum):
    free = 0
    in_use = 1


XrefTable = namedtuple('XrefTable', 'dictionary, sections')
XrefSection = namedtuple('XrefSection', 'start count')
XrefEntry = namedtuple('XrefEntry', 'obj gen offset status')
Trailer = namedtuple('Trailer', 'dictionary startxref')
StreamData = namedtuple('StreamData', 'dictionary value')


whitespace = [chr(0x00), chr(0x09), chr(0x0A), chr(0x0C), chr(0x0D), chr(0x20)]  # NUL HT LF FF CR SP
delimiter = ['(', ')', '[', ']', '<', '>', '{', '}', '\\', '%']
eol = ''.join([chr(0x0D), chr(0x0A)])


def iswhitespace(c):
    """Check if c is a whitespace character."""
    return c in whitespace


def isdelimiter(c):
    """Check if c is a pdf delimiter character."""
    return c in delimiter


def isregular(c):
    """Check if c is a pdf regular character."""
    return c not in whitespace and c not in delimiter


def isnewline(c):
    """Check if c is either \r or \n."""
    return c == chr(0x0A) or c == chr(0x0D)


class Object(object):
    """
    TODO: document me!
    """

    def __init__(self, value=None):
        self.value = value

    @property
    def value(self):
        return self.__value

    @value.setter
    def value(self, val):
        self.__value = val


class Boolean(Object):
    def __init__(self, value=None):
        super(Boolean, self).__init__()
        if value == 'true':
            self.value = True
        else:
            self.value = False

    def __str__(self):
        return str(self.value).lower()


class Numeric(Object):
    def __init__(self, value=None):
        super(Numeric, self).__init__()
        self.value = decimal.Decimal(value)

    def __str__(self):
        return self.value.__str__()


class LiteralString(Object):
    def __init__(self, value=None):
        super(LiteralString, self).__init__()
        self.value = str(value)

    def __str__(self):
        return '(' + self.value + ')'


class HexString(Object):
    def __init__(self, value=None):
        super(HexString, self).__init__()
        self.value = str(value)

    def __str__(self):
        return '<' + self.value.__str__() + '>'


class Name(Object):
    def __init__(self, value=None):
        super(Name, self).__init__()
        self.value = str(value)

    def __str__(self):
        return '/' + str(self.value)

    def __iter__(self):
        return self.value.__iter__()


class Array(Object):
    def __init__(self, value=None):
        super(Array, self).__init__()
        self.value = [] + value

    def __getitem__(self, item):
        return self.value.__getitem__(item)

    def __str__(self):
        val = ''
        for elem in self.value:
            val = val + ' ' + str(elem)
        return '[' + val.strip() + ']'


class Dictionary(Object):
    def __init__(self, value=None):
        super(Dictionary, self).__init__()
        self.value = dict(value)

    def __str__(self):
        val = ''
        for k, v in self.value.items():
            val = val + ' /' + str(k) + ' ' + str(v)
        return '<<' + val.strip() + '>>'

    def __iter__(self):
        return self.value.__iter__()

    def __contains__(self, item):
        k = []
        for key in self.value.keys():
            k.append(str(key))
        return item in k

    def __getitem__(self, item):
        return self.value.__getitem__(item)

    def data(self):
        return self.value

    def keys(self):
        return self.value.keys()


class Stream(Object):
    def __init__(self, dictionary, data):
        super(Stream, self).__init__()
        self.value = StreamData(dictionary, data)

    def __str__(self):
        return str(self.value.dictionary) + '\nstream\n' + self.value.value + '\nendstream'


class Indirect(Object):

    def __init__(self, index, generation, data):
        super(Indirect, self).__init__()
        self.id = index
        self.gen = generation
        self.value = data

    @property
    def id(self):
        return self.__id

    @id.setter
    def id(self, val):
        self.__id = val

    @property
    def gen(self):
        return self.__gen

    @gen.setter
    def gen(self, val):
        self.__gen = val

    def __str__(self):
        return str(self.id) + ' ' + str(self.gen) + ' obj\n' + str(self.value) + '\nendobj'


class Reference(Object):

    def __init__(self, index, generation):
        super(Reference, self).__init__()
        self.id = index
        self.gen = generation
        self.value = (index, generation)

    @property
    def id(self):
        return self.__id

    @id.setter
    def id(self, val):
        self.__id = val

    @property
    def gen(self):
        return self.__gen

    @gen.setter
    def gen(self, val):
        self.__gen = val

    def __str__(self):
        return ''.join([str(self.id), str(self.gen), 'R'])


class Null(Object):

    def __init__(self):
        super(Null, self).__init__()

    def __str__(self):
        return 'null'


class Comment(Object):
    def __init__(self, comment):
        super(Comment, self).__init__()
        self.value = str(comment)

    def __str__(self):
        return '%' + self.value


NULL = Null()
EOF = Comment('%EOF')


class Token(Enum):
    comment = 1
    string_literal = 2
    string_hex = 3
    array_begin = 4
    array_end = 5
    dictionary_begin = 6
    dictionary_end = 7
    name = 8
    stream_begin = 9
    stream_end = 10
    unknown = 11
    null = 12
    numeric = 13
    indirect_begin = 14
    indirect_end = 15
    reference = 16
    xref = 17
    startxref = 18
    trailer = 19
    xref_n = 20
    xref_f = 21
    boolean = 22


class Parser(object):

    def __init__(self):
        self.handlers = {}
        self.handlers[Token.name] = self.handle_name

    def handle_name(self, objects, token_iterator):
        return objects

    def handle_null(self, token_iterator):
        pass

    def handle_numeric(self, token_iterator):
        pass

    def handle_boolean(self, token_iterator):
        pass

    def handle_string_literal(self, token_iterator):
        pass

    def handle_string_hex(self, token_iterator):
        pass

    def handle_comment(self, token_iterator):
        pass

    def handle_indirect(self, token_iterator):
        pass

    def handle_dictionary(self, token_iterator):
        pass

    def handle_array(self, token_iterator):
        pass

    def handle_reference(self, token_iterator):
        pass

    def handle_stream(self, token_iterator):
        pass

    def handle_trailer(self, token_iterator):
        pass

    def handle_xref(self, token_iterator):
        pass

    def handle_unknown(self, token_iterator):
        pass


def parse(tokens):
    objects = []
    sentinel = object()
    it = iter(tokens)
    while True:
        token = next(it, sentinel)
        if token is sentinel:
            break
        if token == Token.name:
            objects.append(Name(next(it)))
        elif token == Token.null:
            objects.append(NULL)
        elif token == Token.numeric:
            objects.append(Numeric(next(it)))
        elif token == Token.boolean:
            objects.append(Boolean(next(it)))
        elif token == Token.string_literal:
            objects.append(LiteralString(next(it)))
        elif token == Token.string_hex:
            objects.append(HexString(next(it)))
        elif token == Token.comment:
            objects.append(Comment(next(it)))
        elif token == Token.indirect_begin:
            inner_tokens = []
            while True:
                token = next(it, sentinel)
                if token is sentinel:
                    break
                if token == Token.indirect_end:
                    break
                inner_tokens.append(token)
            inner_objects = parse(inner_tokens)
            gen = objects.pop()
            index = objects.pop()
            objects.append(Indirect(index.value, gen.value, inner_objects[0]))
        elif token == Token.reference:
            gen = objects.pop()
            index = objects.pop()
            objects.append(Reference(index.value, gen.value))
        elif token == Token.array_begin:
            inner_tokens = []
            while True:
                token = next(it, sentinel)
                if token is sentinel:
                    break
                if token == Token.array_end:
                    break
                inner_tokens.append(token)
            inner_objects = parse(inner_tokens)
            data = []
            for inner_object in inner_objects:
                data.append(inner_object)
            objects.append(Array(data))
        elif token == Token.dictionary_begin:
            _parse_dictionary(it, objects)
        elif token == Token.stream_begin:
            stream_dict = objects.pop()
            stream_data = next(it)
            if next(it) != Token.stream_end:
                raise BaseException('uijuijui')
            objects.append(Stream(stream_dict, stream_data))
        elif token == Token.trailer:
            _parse_trailer(it, objects)
        elif token == Token.xref:
            xref = XrefTable({}, [])
            inner_tokens = []
            while True:
                token = next(it, sentinel)
                if token is sentinel:
                    break
                if token == Token.trailer:
                    break
                inner_tokens.append(token)
            inner_it = iter(inner_tokens)
            while True:
                if next(inner_it, sentinel) is sentinel:  # Token.numeric
                  break

                start_obj = int(next(inner_it))
                next(inner_it)  # Token.numeric
                num_obj = int(next(inner_it))
                xref.sections.append(XrefSection(start_obj, num_obj))
                for i in range(0, int(num_obj)):
                    next(inner_it)  # Token.numeric
                    offset = int(next(inner_it))
                    next(inner_it)  # Token.numeric
                    generation = int(next(inner_it))
                    status = next(inner_it)
                    if status == Token.xref_f:
                        status = ObjectStatus.free
                    else:
                        status = ObjectStatus.in_use
                    xref.dictionary[(start_obj + i, generation)] = XrefEntry((start_obj + i), generation, offset,  status.value)
            objects.append(xref)
            _parse_trailer(it, objects)
        elif token == Token.unknown:
            objects.append(Object(next(it)))

    return objects


def _parse_trailer(it, objects):
    next(it)  # Token.dictionary_begin
    _parse_dictionary(it, objects)
    trailer_dict = objects.pop()
    next(it)  # Token.startxref
    next(it)  # Token.numeric
    trailer_startxref = next(it)
    next(it)  # Token.comment
    next(it)  # %%EOF
    trailer = Trailer(trailer_dict, trailer_startxref)
    objects.append(trailer)


def _parse_dictionary(it, objects):
    inner_tokens = []
    while True:
        sentinal = ()
        token = next(it, sentinal)
        if token is sentinal:
            break
        if token == Token.dictionary_end:
            break
        inner_tokens.append(token)
    inner_objects = parse(inner_tokens)
    data = {}
    inner_iter = iter(inner_objects)
    while True:
        key = next(inner_iter, sentinal)
        value = next(inner_iter, sentinal)
        if key is sentinal:
            break
        if value is sentinal:
            raise BaseException('uijuijui')
        if not isinstance(key, Name):
            raise BaseException('uijuijui')
        data[key.value] = value
    objects.append(Dictionary(data))


def lex(input_string):
    ios = io.StringIO(input_string)
    tokens = []
    unknown = False
    while True:
        c = ios.read(1)
        if not c:
            break

        if iswhitespace(c):
            continue

        if c == '%':
            tokens.append(Token.comment)
            tokens.append(_read_comment(ios))
        elif c == '(':
            tokens.append(Token.string_literal)
            tokens.append(_read_string_literal(ios))
        elif c == '<':
            c = ios.read(1)
            if c == '<':
                tokens.append(Token.dictionary_begin)
            else:
                _unread(ios, 1)
                tokens.append(Token.string_hex)
                tokens.append(_read_string_hex(ios))
        elif c == '>':
            c = ios.read(1)
            if c == '>':
                tokens.append(Token.dictionary_end)
            else:
                raise BaseException('uijuiui!')
        elif c == '[':
            tokens.append(Token.array_begin)
        elif c == ']':
            tokens.append(Token.array_end)
        elif c == '/':
            tokens.append(Token.name)
            tokens.append(_read_name(ios))
        elif c in '+-.1234567890':
            tokens.append(Token.numeric)
            _unread(ios, 1)
            tokens.append(_read_numeric(ios))
        elif c.isalpha():
            _unread(ios, 1)
            s = _read_name(ios)
            if s == 'endobj':
                tokens.append(Token.indirect_end)
            elif s == 'obj':
                tokens.append(Token.indirect_begin)
            elif s == 'R':
                tokens.append(Token.reference)
            elif s == 'stream':
                tokens.append(Token.stream_begin)
                tokens.append(_read_stream_data(ios))
                tokens.append(Token.stream_end)
            elif s == 'null':
                tokens.append(Token.null)
            elif s == 'trailer':
                tokens.append(Token.trailer)
            elif s == 'xref':
                tokens.append(Token.xref)
            elif s == 'startxref':
                tokens.append(Token.startxref)
            elif s == 'n':
                tokens.append(Token.xref_n)
            elif s == 'f':
                tokens.append(Token.xref_f)
            elif s in ['true', 'false']:
                tokens.append(Token.boolean)
                tokens.append(s)
            else:
                tokens.append(Token.unknown)
                tokens.append(s)
        else:
            _unread(ios, 1)
            unknown = True

        if unknown:
            tokens.append(Token.unknown)
            tokens.append(_read_unknown(ios))
            unknown = False
            # raise BaseException('uijuijui!')
    return tokens


def _read_unknown(ios):
    lexeme = []
    while True:
        c = ios.read(1)
        if not c:
            break
        if not isregular(c):
            break
        lexeme.append(c)
    return ''.join(lexeme)


def _read_numeric(ios):
    lexeme = []
    period = False
    sign = False

    while True:
        c = ios.read(1)
        if not c:
            break
        if c not in '-+.0123456789':
            break
        if c == '.':
            if period:
                _unread(ios, 1)
                break
            else:
                period = True
        if c in '+-':
            if sign:
                _unread(ios, 1)
                break
            else:
                sign = True
        lexeme.append(c)
    return ''.join(lexeme)


def _read_stream_data(ios):
    lexeme = []
    ios.read(1)  # skip newline
    while True:
        c = ios.read(1)
        if not c:
            raise BaseException('uijuiui')
        elif c == 'e':
            s = ios.read(8)
            if s == 'ndstream':
                break
            else:
                _unread(ios, 8)
        lexeme.append(c)

    lexeme.pop()
    return ''.join(lexeme)


def _read_name(ios):
    lexeme = []
    while True:
        c = ios.read(1)
        if not c:
            break
        if not isregular(c):
            _unread(ios, 1)
            break
        lexeme.append(c)
    return ''.join(lexeme)


def _read_string_literal(ios):
    lexeme = []
    parantheses = 0
    while True:
        c = ios.read(1)
        if not c:
            raise BaseException('uijuijui')
        if c == '(':
            parantheses += 1
        elif c == ')':
            if parantheses == 0:
                break
            else:
                parantheses -= 1

        lexeme.append(c)
    return ''.join(lexeme)


def _read_string_hex(ios):
    lexeme = []
    allowed_chars = '0123456789abcdefABCDEF'
    while True:
        c = ios.read(1)
        if not c:
            raise BaseException('uijuijui')
        if c == '>':
            break
        if iswhitespace(c):
            continue
        if c not in allowed_chars:
            raise BaseException('uijuijui!')

        lexeme.append(c)

    if len(lexeme) % 2 == 1:
        lexeme.append('0')
    return ''.join(lexeme)


def _read_comment(ios):
    lexeme = []
    while True:
        c = ios.read(1)
        if not c:
            break
        if isnewline(c):
            break

        lexeme.append(c)

    return ''.join(lexeme)


def _unread(ios, times):
    pos = ios.tell()
    ios.seek(pos - times, 0)


import unittest


class TestLex(unittest.TestCase):
    def test_lex_comment(self):
        tokens = lex('% comment { /% ) blah blah blah\n')
        self.assertEqual(tokens, [Token.comment, ' comment { /% ) blah blah blah'])

    def test_lex_array(self):
        tokens = lex('[]')
        self.assertEqual(tokens, [Token.array_begin, Token.array_end])

    def test_lex_dictionary(self):
        tokens = lex('<<>>')
        self.assertEqual(tokens, [Token.dictionary_begin, Token.dictionary_end])

    def test_lex_string_literal(self):
        self.assertEqual(lex('()'), [Token.string_literal, ''])
        self.assertEqual(lex('(())'), [Token.string_literal, '()'])
        with self.assertRaises(BaseException):
            self.assertEqual(lex('(() '), [])

    def test_lex_string_hex(self):
        self.assertEqual(lex('<>'), [Token.string_hex, ''])
        self.assertEqual(lex('<00>'), [Token.string_hex, '00'])
        self.assertEqual(lex('<00 \n 0D>'), [Token.string_hex, '000D'])
        self.assertEqual(lex('<1>'), [Token.string_hex, '10'])
        with self.assertRaises(BaseException):
            lex('<0T')

    def test_lex_name(self):
        self.assertEqual(lex('/Test'), [Token.name, 'Test'])

    def test_lex_stream(self):
        self.assertEqual(lex('stream\n\nendstream'), [Token.stream_begin, '', Token.stream_end])

    def test_lex_null(self):
        self.assertEqual(lex('null'), [Token.null])

    def test_lex_numeric(self):
        self.assertEqual(lex('123'), [Token.numeric, '123'])
        self.assertEqual(lex('+123'), [Token.numeric, '+123'])
        self.assertEqual(lex('-123'), [Token.numeric, '-123'])
        self.assertEqual(lex('+.1'), [Token.numeric, '+.1'])
        self.assertEqual(lex('+0.0'), [Token.numeric, '+0.0'])
        self.assertEqual(lex('+1-1'), [Token.numeric, '+1', Token.numeric, '-1'])
        with self.assertRaises(BaseException):
            self.assertEqual(lex('+.1.0'), [Token.numeric, '+.1'])

    def test_lex_indirect(self):
        self.assertEqual(lex('obj'), [Token.indirect_begin])
        self.assertEqual(lex('endobj'), [Token.indirect_end])


class TestParse(unittest.TestCase):

    def test_parse_name(self):
        tokens = [Token.name, 'Test']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Name))
        self.assertEqual('Test', objects[0].value)

        tokens = [Token.name]
        with self.assertRaises(BaseException):
            parse(tokens)

    def test_parse_null(self):
        tokens = [Token.null]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Null))

    def test_parse_numeric(self):
        tokens = [Token.numeric, '5']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Numeric))
        self.assertEqual(decimal.Decimal(5), objects[0].value)

        tokens = [Token.numeric]
        with self.assertRaises(BaseException):
            parse(tokens)

    def test_parse_boolean(self):
        tokens = [Token.boolean, 'true']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Boolean))
        self.assertEqual(True, objects[0].value)

        tokens = [Token.boolean]
        with self.assertRaises(BaseException):
            parse(tokens)

        self.assertTrue(parse([Token.boolean, 'true'])[0].value)
        self.assertFalse(parse([Token.boolean, 'false'])[0].value)
        self.assertFalse(parse([Token.boolean, 'True'])[0].value)
        self.assertFalse(parse([Token.boolean, 'other'])[0].value)

    def test_parse_string_literal(self):
        tokens = [Token.string_literal, 'This is a string.']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], LiteralString))
        self.assertEqual('This is a string.', objects[0].value)

    def test_parse_string_hex(self):
        tokens = [Token.string_hex, '0002050D']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], HexString))
        self.assertEqual('0002050D', objects[0].value)

    def test_parse_comment(self):
        tokens = [Token.comment, 'This is a comment.']
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Comment))
        self.assertEqual('This is a comment.', objects[0].value)

    def test_parse_indirect(self):
        tokens = [Token.numeric, '1', Token.numeric, '0', Token.indirect_begin, Token.unknown, '', Token.indirect_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Indirect))
        self.assertEqual(1, objects[0].id)
        self.assertEqual(0, objects[0].gen)
        self.assertTrue(isinstance(objects[0].value, Object))

    def test_parse_indirect_array(self):
        tokens = [Token.numeric, '1', Token.numeric, '0', Token.indirect_begin, Token.array_begin, Token.numeric, '1', Token.numeric, '2', Token.array_end, Token.indirect_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Indirect))
        self.assertEqual(1, objects[0].id)
        self.assertEqual(0, objects[0].gen)
        self.assertTrue(isinstance(objects[0].value, Array))

    def test_parse_indirect_dictionary(self):
        tokens = [Token.numeric, '1', Token.numeric, '0', Token.indirect_begin, Token.dictionary_begin, Token.name, 'Length', Token.numeric, '1', Token.dictionary_end, Token.indirect_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Indirect))
        self.assertEqual(1, objects[0].id)
        self.assertEqual(0, objects[0].gen)
        self.assertTrue(isinstance(objects[0].value, Dictionary))

    def test_parse_array(self):
        tokens = [Token.array_begin, Token.numeric, '1', Token.numeric, '2', Token.array_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Array))
        data = objects[0].value
        self.assertTrue(isinstance(data[0], Numeric))
        self.assertTrue(isinstance(data[1], Numeric))
        self.assertEqual(1, data[0].value)
        self.assertEqual(2, data[1].value)

    def test_parse_dictionary(self):
        tokens = [Token.dictionary_begin, Token.name, 'Length', Token.numeric, '1', Token.dictionary_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Dictionary))
        data = objects[0].value
        self.assertTrue(isinstance(data['Length'], Numeric))
        self.assertTrue('Length' in data)
        self.assertEqual(1, data['Length'].value)

        with self.assertRaises(BaseException):
            tokens = [Token.dictionary_begin, Token.Numeric, '1', Token.numeric, '1', Token.dictionary_end]
            parse(tokens)

        with self.assertRaises(BaseException):
            tokens = [Token.dictionary_begin, Token.Name, 'Length', Token.dictionary_end]
            parse(tokens)

    def test_parse_reference(self):
        tokens = [Token.numeric, '1', Token.numeric, '0', Token.reference]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Reference))
        self.assertEqual(1, objects[0].id)
        self.assertEqual(0, objects[0].gen)

    def test_parse_stream(self):
        tokens = [Token.dictionary_begin, Token.name, 'Length', Token.numeric, '1', Token.dictionary_end, Token.stream_begin, 'asdfasdfasdfasdfsdf', Token.stream_end]
        objects = parse(tokens)
        self.assertTrue(isinstance(objects[0], Stream))
        self.assertTrue(isinstance(objects[0].value.dictionary, Dictionary))
        self.assertTrue(isinstance(objects[0].value.value, str))
        self.assertEqual('asdfasdfasdfasdfsdf', objects[0].value.value)


if __name__ == '__main__':
    unittest.main()
