#! /usr/bin/env python3

import ezdxf
import re


class StringBuffer:
    """
    Store a string, an index and provide some operations.
    """

    def __init__(self, string: str):
        self._string = string
        self._index = 0
        self._stack = []

    def __enter__(self):
        self.enter()
        return self

    def __exit__(self, exc_type, _exc_value, _traceback):
        if exc_type is None:
            self.complete()
        else:
            self.backtrack()

    def advance(self, length: int):
        """
        Advance the index by length.
        """
        self._index = min(len(self._string), self._index + length)

    def backtrack(self):
        """
        Track back from parsing something.
        Pop index from stack and use it as the new current index.
        """
        self._index = self._stack[-1]
        del self._stack[-1]

    def check(self, s: str) -> bool:
        """
        Check if string s follows index.
        If so, advance by length of s and return True
        Otherwise, keep index and return False.
        """
        if self.peek(len(s)) == s:
            self.advance(len(s))
            return True
        return False

    def complete(self):
        """
        Complete parsing something.
        Pop index from stack and throw it away.
        """
        del self._stack[-1]

    def enter(self):
        """
        Enter parsing something.
        Push current index onto stack.
        """
        self._stack.append(self._index)

    def expect(self, s: str):
        """
        Check that string s follows index.
        If so, advance.
        If not, throw ValueError.
        """
        if not self.check(s):
            raise ValueError

    def get(self, length: int) -> str:
        """
        Return the next length characters and advance the index.
        """
        s = self.peek(length)
        self.advance(length)
        return s

    def getRe(self, ptrn: re.Pattern):
        """
        Return the matching the regular expression pattern and advance
        index or throw ValueError.
        """
        m = ptrn.match(self._string[self._index:])
        if not m:
            raise ValueError()
        self._index += len(m.group(0))
        return m

    def peek(self, length: int) -> str:
        """
        Return the next length characters without advancing the index.
        """
        return self._string[self._index:self._index + length]


class PcbBaseParser(StringBuffer):
    """
    Parser for the basic element of GNU PCB files.
    """

    re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)'
                          r'(?:[]eE[+-]?[0-9]\+)?')
    re_int = re.compile(r'^[+-]?[0-9]+')
    re_quoted = re.compile(r'^"([^"]*)"')
    re_space = re.compile(r'^\s+', re.MULTILINE)
    re_space_opt = re.compile(r'^\s*', re.MULTILINE)

    def parseCoord(self) -> float:
        """
        Parse a coordinate.
        """
        with self:
            f = self.parseFloat()
            if self.check('mil'):
                f *= 100  # native unit == .01mil
            elif self.check('mm'):
                f *= 1e5/25.4  # 25.4mm == 1inch == 1e5 * .01mil
            return f

    def parseFloat(self) -> float:
        """
        Parse a floating point value.
        """
        with self:
            return float(self.getRe(self.re_float).group(0))

    def parseInt(self) -> int:
        """
        Parse a decimal integer.
        """
        with self:
            return int(self.getRe(self.re_int).group(0))

    def parseQuoted(self):
        """
        Parse quoted string.
        """
        with self:
            return self.getRe(self.re_quoted).group(1)

    def parseSpace(self):
        """
        Parse at least one whitespace character or more.
        """
        with self:
            self.getRe(self.re_space)

    def parseSpaceOpt(self):
        """
        Parse any number of whitespace characters.
        """
        with self:
            self.getRe(self.re_space_opt)


class PcbFootprintParser(PcbBaseParser):
    """
    Parser for a GNU PCB footprint file.
    """

    def parseBody(self, contents: list):
        """
        Parse a body of a block.
        contents: list of lambdas with options for recursive descent
        """
        with self:
            self.parseOpen()
            while True:
                # end of body
                try:
                    self.parseClose()
                    break
                except ValueError:
                    pass
                # try options
                for c in contents:
                    try:
                        c()
                        break  # option succeded, go to next content
                    except ValueError:
                        pass  # an option failed, try next option
                # all options failed
                else:
                    raise ValueError()

    def parseClose(self):
        """
        Parse a closing parenthesis.
        """
        with self:
            self.parseSpaceOpt()
            self.expect(')')

    def parseElementBlock(self):
        """
        Parse a GNU PCB element block.
        """
        with self:
            self.parseElementClause()
            self.parseBody([
                lambda: self.parseElementLineClause(),
                lambda: self.parsePadClause()
            ])


    def parseElementClause(self):
        """
        Parse a GNU PCB element clause.
        """
        with self:
            self.parseSpaceOpt()
            self.expect('Element')
            self.parseSpaceOpt()
            self.expect('[')
            self.parseSpaceOpt()
            s_flags = self.parseQuoted()
            self.parseSpace()
            desc = self.parseQuoted()
            self.parseSpace()
            name = self.parseQuoted()
            self.parseSpace()
            value = self.parseQuoted()
            self.parseSpace()
            m_x = self.parseCoord()
            self.parseSpace()
            m_y = self.parseCoord()
            self.parseSpace()
            t_x = self.parseCoord()
            self.parseSpace()
            t_y = self.parseCoord()
            self.parseSpace()
            t_dir = self.parseInt()
            self.parseSpace()
            t_scale = self.parseInt()
            self.parseSpace()
            t_s_flags = self.parseQuoted()
            self.parseSpaceOpt()
            self.expect(']')

    def parseElementLineClause(self):
        """
        Parse a GNU PCB element line clause.
        """
        with self:
            self.parseSpaceOpt()
            self.expect('ElementLine')
            self.parseSpaceOpt()
            self.expect('[')
            self.parseSpaceOpt()
            r_x1 = self.parseCoord()
            self.parseSpace()
            r_y1 = self.parseCoord()
            self.parseSpace()
            r_x2 = self.parseCoord()
            self.parseSpace()
            r_y2 = self.parseCoord()
            self.parseSpace()
            thickness = self.parseCoord()
            self.parseSpaceOpt()
            self.expect(']')

    def parseOpen(self):
        """
        Parse an opening parenthesis.
        """
        with self:
            self.parseSpaceOpt()
            self.expect('(')

    def parsePadClause(self):
        """
        Parse a GNU PCB pad clause.
        """
        with self:
            self.parseSpaceOpt()
            self.expect('Pad')
            self.parseSpaceOpt()
            self.expect('[')
            self.parseSpaceOpt()
            r_x1 = self.parseCoord()
            self.parseSpace()
            r_y1 = self.parseCoord()
            self.parseSpace()
            r_x2 = self.parseCoord()
            self.parseSpace()
            r_y2 = self.parseCoord()
            self.parseSpace()
            thickness = self.parseCoord()
            self.parseSpace()
            clearance = self.parseCoord()
            self.parseSpace()
            mask = self.parseCoord()
            self.parseSpace()
            name = self.parseQuoted()
            self.parseSpace()
            number = self.parseQuoted()
            self.parseSpace()
            s_flags = self.parseQuoted()
            self.parseSpaceOpt()
            self.expect(']')


class PcbFootprint:
    """
    A footprint of GNU PCB.
    """

    def __init__(self):
        self.reset()

    def readPcb(self, file_name: str):
        """
        Read a PCB footprint file.
        """
        self.reset()
        with open(file_name, 'r') as f:
            s = f.read()
        p = PcbFootprintParser(s)
        p.parseElementBlock()

    def reset(self):
        pass


def write_dxf(file_name: str):
    doc = ezdxf.new('R12')
    msp = doc.modelspace()
    msp.add_circle((1, 2), radius=3)
    doc.saveas(file_name)


def main():
    fp = PcbFootprint()
    fp.readPcb('../pcb_footpr/SMD_1206')


if __name__ == '__main__':
    main()