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

# copyright 2014 Hamilton Kibbe <ham@hamiltonkib.be>
# copyright 2014 Paulo Henrique Silva <ph.silva@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" This module provides RS-274-X AM macro modifiers parsing.
"""

from .am_eval import OpCode, eval_macro

import string


class Token:
    ADD = "+"
    SUB = "-"
    MULT = ("x", "X") # compatibility as many gerber writes do use non compliant X
    DIV = "/"
    OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
    LEFT_PARENS = "("
    RIGHT_PARENS = ")"
    EQUALS = "="
    EOF = "EOF"


def token_to_opcode(token):
    if token == Token.ADD:
        return OpCode.ADD
    elif token == Token.SUB:
        return OpCode.SUB
    elif token in Token.MULT:
        return OpCode.MUL
    elif token == Token.DIV:
        return OpCode.DIV
    else:
        return None


def precedence(token):
    if token == Token.ADD or token == Token.SUB:
        return 1
    elif token in Token.MULT or token == Token.DIV:
        return 2
    else:
        return 0


def is_op(token):
    return token in Token.OPERATORS


class Scanner:
    def __init__(self, s):
        self.buff = s
        self.n = 0

    def eof(self):
        return self.n == len(self.buff)

    def peek(self):
        if not self.eof():
            return self.buff[self.n]

        return Token.EOF

    def ungetc(self):
        if self.n > 0:
            self.n -= 1

    def getc(self):
        if self.eof():
            return ""

        c = self.buff[self.n]
        self.n += 1
        return c

    def readint(self):
        n = ""
        while not self.eof() and (self.peek() in string.digits):
            n += self.getc()
        return int(n)

    def readfloat(self):
        n = ""
        while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
            n += self.getc()
        # weird case where zero is ommited inthe last modifider, like in ',0.'
        if n == ".":
            return 0
        return float(n)

    def readstr(self, end="*"):
        s = ""
        while not self.eof() and self.peek() != end:
            s += self.getc()
        return s.strip()


def print_instructions(instructions):
    for opcode, argument in instructions:
        print("%s %s" % (OpCode.str(opcode), str(argument) if argument is not None else ""))


def read_macro(macro):
    instructions = []

    for block in macro.split("*"):

        is_primitive = False
        is_equation = False

        found_equation_left_side = False
        found_primitive_code = False

        equation_left_side = 0
        primitive_code = 0

        unary_minus_allowed = False
        unary_minus = False

        if Token.EQUALS in block:
            is_equation = True
        else:
            is_primitive = True

        scanner = Scanner(block)

        # inlined here for compactness and convenience
        op_stack = []

        def pop():
            return op_stack.pop()

        def push(op):
            op_stack.append(op)

        def top():
            return op_stack[-1]

        def empty():
            return len(op_stack) == 0

        while not scanner.eof():

            c = scanner.getc()

            if c == ",":
                found_primitive_code = True

                # add all instructions on the stack to finish last modifier
                while not empty():
                    instructions.append((token_to_opcode(pop()), None))

                unary_minus_allowed = True

            elif c in Token.OPERATORS:
                if c == Token.SUB and unary_minus_allowed:
                    unary_minus = True
                    unary_minus_allowed = False
                    continue

                while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
                    instructions.append((token_to_opcode(pop()), None))

                push(c)

            elif c == Token.LEFT_PARENS:
                push(c)

            elif c == Token.RIGHT_PARENS:
                while not empty() and top() != Token.LEFT_PARENS:
                    instructions.append((token_to_opcode(pop()), None))

                if empty():
                    raise ValueError("unbalanced parentheses")

                # discard "("
                pop()

            elif c.startswith("$"):
                n = scanner.readint()

                if is_equation and not found_equation_left_side:
                    equation_left_side = n
                else:
                    instructions.append((OpCode.LOAD, n))

            elif c == Token.EQUALS:
                found_equation_left_side = True

            elif c == "0":
                if is_primitive and not found_primitive_code:
                    instructions.append((OpCode.PUSH, scanner.readstr("*")))
                    found_primitive_code = True
                else:
                    # decimal or integer disambiguation
                    if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
                        instructions.append((OpCode.PUSH, 0))

            elif c in "123456789.":
                scanner.ungetc()

                if is_primitive and not found_primitive_code:
                    primitive_code = scanner.readint()
                else:
                    n = scanner.readfloat()
                    if unary_minus:
                        unary_minus = False
                        n *= -1

                    instructions.append((OpCode.PUSH, n))
            else:
                # whitespace or unknown char
                pass

        # add all instructions on the stack to finish last modifier (if any)
        while not empty():
            instructions.append((token_to_opcode(pop()), None))

        # at end, we either have a primitive or a equation
        if is_primitive and found_primitive_code:
            instructions.append((OpCode.PRIM, primitive_code))

        if is_equation:
            instructions.append((OpCode.STORE, equation_left_side))

    return instructions

if __name__ == '__main__':
    import sys

    instructions = read_macro(sys.argv[1])

    print("insructions:")
    print_instructions(instructions)

    print("eval:")
    for primitive in eval_macro(instructions):
        print(primitive)